GitLab Repo

amachine.am_fast

  1from __future__ import annotations
  2
  3import sys
  4from pathlib import Path
  5import json
  6from typing import Any
  7
  8import pyarrow as pa
  9import pyarrow.parquet as pq
 10import numpy as np
 11import numpy.typing as npt
 12
 13from automata.fa.dfa import DFA
 14
 15_build_dir = Path(__file__).resolve().parent / "build"
 16
 17if str(_build_dir) not in sys.path:
 18    sys.path.insert(0, str(_build_dir))
 19
 20from ._am_fast import ( 
 21    generate_cpp, 
 22    block_entropy_convergence_cpp, 
 23    strongly_connected_components_cpp,
 24    minify_dfa_cpp
 25)
 26
 27from .json_utils import save_json
 28
 29def block_entropy_convergence(
 30    h_mu: float,
 31    n_states: int,
 32    n_symbols : int,
 33    convergence_tol: float,
 34    precision: float,
 35    eps: float,
 36    branches: list[tuple[float, list[float]]],
 37    trans: list[list[tuple[int, float, int]]],
 38    max_branches: int = 30_000_000
 39) -> Any :
 40    return block_entropy_convergence_cpp(
 41        h_mu            = float( h_mu ),
 42        n_states        = n_states,
 43        n_symbols       = n_symbols,
 44        convergence_tol = convergence_tol,
 45        precision       = float(precision),
 46        eps             = eps,
 47        branches        = branches,
 48        trans           = trans,
 49        max_branches    = max_branches
 50    )
 51
 52def strongly_connected_components( T ) :
 53    return strongly_connected_components_cpp( T )
 54
 55def generate_data(
 56    n_gen          : int,
 57    start_state    : int,
 58    transitions    : list[list[tuple[int, float, int]]],
 59    alphabet       : list[str],
 60    include_states : bool = False,
 61    random_seed    : int  = 42,
 62) -> dict[str, Any ]:
 63
 64    n_states  = len(transitions)
 65    n_symbols = len(alphabet)
 66
 67    if n_states > 65536:
 68        raise ValueError(f"Max states supported is 65536, got {n_states}")
 69    if n_symbols > 65536:
 70        raise ValueError(f"Max alphabet size supported is 65536, got {n_symbols}")
 71
 72    symbol_indices, state_indices = generate_cpp(
 73        n_gen             = n_gen,
 74        start_state_index = start_state,
 75        transitions       = transitions,
 76        include_states    = include_states,
 77        random_seed       = random_seed,
 78    )
 79
 80    res: dict[str, Any ] = {
 81        "symbol_index": np.from_dlpack(symbol_indices)
 82    }
 83
 84    if include_states:
 85        res["state_index"] = np.from_dlpack(state_indices)
 86
 87    return res
 88
 89def save_data(
 90    data             : dict[str, Any],
 91    file_prefix      : str,
 92    alphabet         : list[str],
 93    n_states         : int,
 94    start_state      : int,
 95    random_seed      : int,
 96    row_size         : int | None = None,
 97    machine_metadata : dict[str, Any] | None = None,
 98) -> None:
 99
100    def _uint_type(n: int) -> pa.DataType:
101        return pa.uint8() if n <= 256 else pa.uint16()
102
103    sym_type   = _uint_type(len(alphabet))
104    state_type = _uint_type(n_states)
105
106    columns: dict[str, pa.Array] = {
107        "symbol_index": pa.array(data["symbol_index"]).cast(sym_type)
108    }
109
110    final_state=start_state
111
112    if "state_index" in data:
113        state_data  = data["state_index"]
114        final_state = int(state_data[-1])
115        columns["state_index"] = pa.array(state_data[:-1]).cast(state_type)
116
117    iso_shifts = data.get("isomorphic_shifts", {})
118    iso_final_states: dict[int, int] = {}
119
120    for shift, shifted in iso_shifts.items():
121        columns[f"symbol_index_isoshift_{shift}"] = pa.array(shifted["symbol_index"]).cast(sym_type)
122        if "state_index" in shifted:
123            iso_state_data = shifted["state_index"]
124            iso_final_states[shift] = int(iso_state_data[-1])
125            columns[f"state_index_isoshift_{shift}"]= pa.array(iso_state_data[:-1]).cast(state_type)
126
127    parquet_meta: dict[str, Any] = {
128        "alphabet"          : alphabet,
129        "machine_metadata"  : machine_metadata or {},
130        "start_state"       : start_state,
131        "random_seed"       : random_seed,
132        "isomorphic_shifts" : sorted(iso_shifts.keys()),
133    }
134
135    if "state_index" in data:
136        parquet_meta["final_state"] = final_state
137
138    if iso_final_states:
139        parquet_meta["isomorphic_final_states"] = iso_final_states
140
141    if "belief_states" in data:
142        
143        belief_data  = data["belief_states"]
144        belief_dim   = belief_data.shape[1]
145
146        columns["belief_state"] = pa.FixedSizeListArray.from_arrays(
147            pa.array(belief_data[:-1].ravel(), type=pa.float32()),
148            belief_dim,
149        )
150
151        parquet_meta["belief_state_dim"]   = belief_dim
152        parquet_meta["final_belief_state"] = belief_data[-1].tolist()
153
154    table = pa.table(columns)
155    table = table.replace_schema_metadata({
156        **(table.schema.metadata or {}),
157        "am_metadata": json.dumps(parquet_meta),
158    })
159
160    pq.write_table(table, f"{file_prefix}.parquet", row_group_size=row_size )
161    save_json(parquet_meta, f"{file_prefix}.json")
162
163
164def minify_cpp(dfa: DFA, retain_names: bool = True) -> DFA:
165    state_list = list(dfa.states)
166    state_idx = {s: i for i, s in enumerate(state_list)}
167    symbol_list = sorted(dfa.input_symbols)
168    symbol_idx = {sym: i for i, sym in enumerate(symbol_list)}
169
170    adj = [[] for _ in range(len(state_list))]
171    for state, paths in dfa.transitions.items():
172        u = state_idx[state]
173        for sym, nxt in paths.items():
174            v = state_idx.get(nxt, -1)
175            if v != -1:
176                adj[u].append((symbol_idx[sym], v))
177
178    is_final = [s in dfa.final_states for s in state_list]
179    init_idx = state_idx[dfa.initial_state]
180
181    # result names in res now match the C++ struct fields exactly
182    res = minify_dfa_cpp(len(state_list), adj, init_idx, is_final)
183
184    if res.is_empty_language:
185        return dfa.__class__.empty_language(dfa.input_symbols)
186
187    if retain_names:
188        class_members = {}
189        for old_idx, nc in enumerate(res.eq_class):
190            if nc >= 0:
191                class_members.setdefault(nc, []).append(state_list[old_idx])
192        class_map = {nc: frozenset(m) for nc, m in class_members.items()}
193    else:
194        class_map = {nc: nc for nc in range(res.n_classes)}
195
196    new_states = set(class_map.values())
197    new_initial = class_map[res.new_initial]
198    new_final = {class_map[c] for c, f in enumerate(res.class_is_final) if f}
199
200    new_trans = {}
201    for nc in range(res.n_classes):
202        new_trans[class_map[nc]] = {
203            symbol_list[trans[0]]: class_map[trans[1]]
204            for trans in res.class_trans[nc]
205        }
206
207    return dfa.__class__(
208        states=new_states,
209        input_symbols=dfa.input_symbols,
210        transitions=new_trans,
211        initial_state=new_initial,
212        final_states=new_final,
213        allow_partial=any(len(t) < len(symbol_list) for t in new_trans.values())
214    )
def block_entropy_convergence( h_mu: float, n_states: int, n_symbols: int, convergence_tol: float, precision: float, eps: float, branches: list[tuple[float, list[float]]], trans: list[list[tuple[int, float, int]]], max_branches: int = 30000000) -> Any:
30def block_entropy_convergence(
31    h_mu: float,
32    n_states: int,
33    n_symbols : int,
34    convergence_tol: float,
35    precision: float,
36    eps: float,
37    branches: list[tuple[float, list[float]]],
38    trans: list[list[tuple[int, float, int]]],
39    max_branches: int = 30_000_000
40) -> Any :
41    return block_entropy_convergence_cpp(
42        h_mu            = float( h_mu ),
43        n_states        = n_states,
44        n_symbols       = n_symbols,
45        convergence_tol = convergence_tol,
46        precision       = float(precision),
47        eps             = eps,
48        branches        = branches,
49        trans           = trans,
50        max_branches    = max_branches
51    )
def strongly_connected_components(T):
53def strongly_connected_components( T ) :
54    return strongly_connected_components_cpp( T )
def generate_data( n_gen: int, start_state: int, transitions: list[list[tuple[int, float, int]]], alphabet: list[str], include_states: bool = False, random_seed: int = 42) -> dict[str, typing.Any]:
56def generate_data(
57    n_gen          : int,
58    start_state    : int,
59    transitions    : list[list[tuple[int, float, int]]],
60    alphabet       : list[str],
61    include_states : bool = False,
62    random_seed    : int  = 42,
63) -> dict[str, Any ]:
64
65    n_states  = len(transitions)
66    n_symbols = len(alphabet)
67
68    if n_states > 65536:
69        raise ValueError(f"Max states supported is 65536, got {n_states}")
70    if n_symbols > 65536:
71        raise ValueError(f"Max alphabet size supported is 65536, got {n_symbols}")
72
73    symbol_indices, state_indices = generate_cpp(
74        n_gen             = n_gen,
75        start_state_index = start_state,
76        transitions       = transitions,
77        include_states    = include_states,
78        random_seed       = random_seed,
79    )
80
81    res: dict[str, Any ] = {
82        "symbol_index": np.from_dlpack(symbol_indices)
83    }
84
85    if include_states:
86        res["state_index"] = np.from_dlpack(state_indices)
87
88    return res
def save_data( data: dict[str, typing.Any], file_prefix: str, alphabet: list[str], n_states: int, start_state: int, random_seed: int, row_size: int | None = None, machine_metadata: dict[str, typing.Any] | None = None) -> None:
 90def save_data(
 91    data             : dict[str, Any],
 92    file_prefix      : str,
 93    alphabet         : list[str],
 94    n_states         : int,
 95    start_state      : int,
 96    random_seed      : int,
 97    row_size         : int | None = None,
 98    machine_metadata : dict[str, Any] | None = None,
 99) -> None:
100
101    def _uint_type(n: int) -> pa.DataType:
102        return pa.uint8() if n <= 256 else pa.uint16()
103
104    sym_type   = _uint_type(len(alphabet))
105    state_type = _uint_type(n_states)
106
107    columns: dict[str, pa.Array] = {
108        "symbol_index": pa.array(data["symbol_index"]).cast(sym_type)
109    }
110
111    final_state=start_state
112
113    if "state_index" in data:
114        state_data  = data["state_index"]
115        final_state = int(state_data[-1])
116        columns["state_index"] = pa.array(state_data[:-1]).cast(state_type)
117
118    iso_shifts = data.get("isomorphic_shifts", {})
119    iso_final_states: dict[int, int] = {}
120
121    for shift, shifted in iso_shifts.items():
122        columns[f"symbol_index_isoshift_{shift}"] = pa.array(shifted["symbol_index"]).cast(sym_type)
123        if "state_index" in shifted:
124            iso_state_data = shifted["state_index"]
125            iso_final_states[shift] = int(iso_state_data[-1])
126            columns[f"state_index_isoshift_{shift}"]= pa.array(iso_state_data[:-1]).cast(state_type)
127
128    parquet_meta: dict[str, Any] = {
129        "alphabet"          : alphabet,
130        "machine_metadata"  : machine_metadata or {},
131        "start_state"       : start_state,
132        "random_seed"       : random_seed,
133        "isomorphic_shifts" : sorted(iso_shifts.keys()),
134    }
135
136    if "state_index" in data:
137        parquet_meta["final_state"] = final_state
138
139    if iso_final_states:
140        parquet_meta["isomorphic_final_states"] = iso_final_states
141
142    if "belief_states" in data:
143        
144        belief_data  = data["belief_states"]
145        belief_dim   = belief_data.shape[1]
146
147        columns["belief_state"] = pa.FixedSizeListArray.from_arrays(
148            pa.array(belief_data[:-1].ravel(), type=pa.float32()),
149            belief_dim,
150        )
151
152        parquet_meta["belief_state_dim"]   = belief_dim
153        parquet_meta["final_belief_state"] = belief_data[-1].tolist()
154
155    table = pa.table(columns)
156    table = table.replace_schema_metadata({
157        **(table.schema.metadata or {}),
158        "am_metadata": json.dumps(parquet_meta),
159    })
160
161    pq.write_table(table, f"{file_prefix}.parquet", row_group_size=row_size )
162    save_json(parquet_meta, f"{file_prefix}.json")
def minify_cpp( dfa: automata.fa.dfa.DFA, retain_names: bool = True) -> automata.fa.dfa.DFA:
165def minify_cpp(dfa: DFA, retain_names: bool = True) -> DFA:
166    state_list = list(dfa.states)
167    state_idx = {s: i for i, s in enumerate(state_list)}
168    symbol_list = sorted(dfa.input_symbols)
169    symbol_idx = {sym: i for i, sym in enumerate(symbol_list)}
170
171    adj = [[] for _ in range(len(state_list))]
172    for state, paths in dfa.transitions.items():
173        u = state_idx[state]
174        for sym, nxt in paths.items():
175            v = state_idx.get(nxt, -1)
176            if v != -1:
177                adj[u].append((symbol_idx[sym], v))
178
179    is_final = [s in dfa.final_states for s in state_list]
180    init_idx = state_idx[dfa.initial_state]
181
182    # result names in res now match the C++ struct fields exactly
183    res = minify_dfa_cpp(len(state_list), adj, init_idx, is_final)
184
185    if res.is_empty_language:
186        return dfa.__class__.empty_language(dfa.input_symbols)
187
188    if retain_names:
189        class_members = {}
190        for old_idx, nc in enumerate(res.eq_class):
191            if nc >= 0:
192                class_members.setdefault(nc, []).append(state_list[old_idx])
193        class_map = {nc: frozenset(m) for nc, m in class_members.items()}
194    else:
195        class_map = {nc: nc for nc in range(res.n_classes)}
196
197    new_states = set(class_map.values())
198    new_initial = class_map[res.new_initial]
199    new_final = {class_map[c] for c, f in enumerate(res.class_is_final) if f}
200
201    new_trans = {}
202    for nc in range(res.n_classes):
203        new_trans[class_map[nc]] = {
204            symbol_list[trans[0]]: class_map[trans[1]]
205            for trans in res.class_trans[nc]
206        }
207
208    return dfa.__class__(
209        states=new_states,
210        input_symbols=dfa.input_symbols,
211        transitions=new_trans,
212        initial_state=new_initial,
213        final_states=new_final,
214        allow_partial=any(len(t) < len(symbol_list) for t in new_trans.values())
215    )