sofic 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sofic/__init__.py +185 -0
- sofic/automata/__init__.py +207 -0
- sofic/automata/_config_simulation.py +40 -0
- sofic/automata/active.py +611 -0
- sofic/automata/alergia.py +222 -0
- sofic/automata/algorithms.py +376 -0
- sofic/automata/atomaton.py +58 -0
- sofic/automata/base.py +161 -0
- sofic/automata/buchi.py +23 -0
- sofic/automata/buchi_simulation.py +67 -0
- sofic/automata/canonical_dual.py +18 -0
- sofic/automata/canonical_extraction.py +122 -0
- sofic/automata/dfa.py +85 -0
- sofic/automata/dfasat.py +195 -0
- sofic/automata/edsm.py +219 -0
- sofic/automata/enumeration.py +44 -0
- sofic/automata/icdfa.py +421 -0
- sofic/automata/idfa.py +363 -0
- sofic/automata/languages/__init__.py +39 -0
- sofic/automata/languages/_quotient_utils.py +64 -0
- sofic/automata/languages/atoms.py +31 -0
- sofic/automata/languages/automaton_ops.py +243 -0
- sofic/automata/languages/base.py +67 -0
- sofic/automata/languages/operations.py +78 -0
- sofic/automata/languages/quotients.py +66 -0
- sofic/automata/languages/residuals.py +25 -0
- sofic/automata/learning.py +79 -0
- sofic/automata/nfa.py +39 -0
- sofic/automata/nwa.py +343 -0
- sofic/automata/nwa_simulation.py +56 -0
- sofic/automata/observation.py +40 -0
- sofic/automata/papni.py +301 -0
- sofic/automata/regex.py +128 -0
- sofic/automata/rfsa.py +35 -0
- sofic/automata/rpni.py +193 -0
- sofic/automata/subsequential.py +201 -0
- sofic/automata/transducer_operations.py +350 -0
- sofic/automata/transducer_simulation.py +150 -0
- sofic/automata/transducers.py +365 -0
- sofic/automata/unifilar.py +107 -0
- sofic/automata/vpa.py +1373 -0
- sofic/automata/vpa_simulation.py +53 -0
- sofic/base.py +153 -0
- sofic/core.py +47 -0
- sofic/examples/__init__.py +86 -0
- sofic/examples/epsilon_machines.py +1089 -0
- sofic/examples/processes.py +1491 -0
- sofic/examples/shifts.py +144 -0
- sofic/exceptions.py +33 -0
- sofic/generators/__init__.py +115 -0
- sofic/generators/_word_measures.py +94 -0
- sofic/generators/alternative_complexity.py +104 -0
- sofic/generators/base.py +327 -0
- sofic/generators/bidirectional_construction.py +717 -0
- sofic/generators/bidirectional_epsilon_machine.py +689 -0
- sofic/generators/block_convergence.py +668 -0
- sofic/generators/block_entropy.py +578 -0
- sofic/generators/channel_measures.py +75 -0
- sofic/generators/conversions.py +182 -0
- sofic/generators/directional_flow.py +245 -0
- sofic/generators/edge_emissions.py +36 -0
- sofic/generators/edge_machine.py +178 -0
- sofic/generators/epsilon_construction.py +193 -0
- sofic/generators/epsilon_inference.py +703 -0
- sofic/generators/epsilon_machine.py +557 -0
- sofic/generators/epsilon_transducer.py +168 -0
- sofic/generators/epsilon_transducer_construction.py +185 -0
- sofic/generators/epsilon_transducer_inference.py +499 -0
- sofic/generators/hmm_inference.py +719 -0
- sofic/generators/information_diagram.py +428 -0
- sofic/generators/lumping.py +447 -0
- sofic/generators/markov.py +100 -0
- sofic/generators/mealy.py +156 -0
- sofic/generators/measures.py +257 -0
- sofic/generators/minimal_generative_model.py +821 -0
- sofic/generators/mixed_state.py +250 -0
- sofic/generators/mixed_state_construction.py +163 -0
- sofic/generators/moore.py +75 -0
- sofic/generators/nmachine.py +78 -0
- sofic/generators/nmachine_construction.py +70 -0
- sofic/generators/pfa.py +100 -0
- sofic/generators/prob.py +291 -0
- sofic/generators/process_equivalence.py +207 -0
- sofic/generators/quasi_inference.py +74 -0
- sofic/generators/quasi_realization.py +97 -0
- sofic/generators/reversal.py +66 -0
- sofic/generators/stack_hmm.py +426 -0
- sofic/generators/stack_inference.py +509 -0
- sofic/generators/stationary.py +134 -0
- sofic/generators/stochastic.py +65 -0
- sofic/generators/synchronization.py +407 -0
- sofic/generators/topological_epsilon_enumeration.py +349 -0
- sofic/generators/words.py +226 -0
- sofic/graph.py +135 -0
- sofic/indexing.py +31 -0
- sofic/inference/__init__.py +45 -0
- sofic/inference/bayesian/__init__.py +68 -0
- sofic/inference/bayesian/comparison.py +199 -0
- sofic/inference/bayesian/counts.py +219 -0
- sofic/inference/bayesian/diversity.py +254 -0
- sofic/inference/bayesian/epsilon.py +270 -0
- sofic/inference/bayesian/hdp_hmm.py +340 -0
- sofic/inference/bayesian/markov.py +294 -0
- sofic/inference/bayesian/pymc_backend.py +71 -0
- sofic/inference/bayesian/stack_hmm.py +215 -0
- sofic/inference/model_selection.py +365 -0
- sofic/inference/spectral.py +564 -0
- sofic/operations.py +16 -0
- sofic/properties.py +339 -0
- sofic/serialization.py +450 -0
- sofic/shifts/__init__.py +48 -0
- sofic/shifts/algorithms.py +84 -0
- sofic/shifts/base.py +49 -0
- sofic/shifts/cover_construction.py +76 -0
- sofic/shifts/covers.py +47 -0
- sofic/shifts/dyck_algorithms.py +100 -0
- sofic/shifts/dyck_enumeration.py +275 -0
- sofic/shifts/markov_dyck.py +172 -0
- sofic/shifts/parry_construction.py +82 -0
- sofic/shifts/sft.py +104 -0
- sofic/shifts/sft_construction.py +52 -0
- sofic/shifts/sliding_block_code.py +156 -0
- sofic/shifts/sofic.py +111 -0
- sofic/shifts/sofic_dyck.py +110 -0
- sofic/shifts/sofic_relation.py +64 -0
- sofic/shifts/textile.py +104 -0
- sofic/shifts/tmc.py +46 -0
- sofic/shifts/tmc_construction.py +58 -0
- sofic/shifts/topological_anatomy.py +150 -0
- sofic/states.py +27 -0
- sofic/testing/__init__.py +8 -0
- sofic/testing/strategies.py +154 -0
- sofic/viz/__init__.py +16 -0
- sofic/viz/_context.py +345 -0
- sofic/viz/_edge.py +216 -0
- sofic/viz/_format.py +89 -0
- sofic/viz/_labels.py +34 -0
- sofic/viz/_names.py +17 -0
- sofic/viz/_rational.py +20 -0
- sofic/viz/_tikz_compile.py +177 -0
- sofic/viz/_tikz_format.py +122 -0
- sofic/viz/_tikz_layout.py +218 -0
- sofic/viz/assets/vaucanson.tikz +71 -0
- sofic/viz/graphviz.py +158 -0
- sofic/viz/idiagram.py +350 -0
- sofic/viz/tikz.py +381 -0
- sofic-0.1.0.dist-info/METADATA +444 -0
- sofic-0.1.0.dist-info/RECORD +150 -0
- sofic-0.1.0.dist-info/WHEEL +4 -0
- sofic-0.1.0.dist-info/licenses/LICENSE.txt +29 -0
sofic/serialization.py
ADDED
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""YAML serialization for sofic state-machine models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from functools import cache
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import networkx as nx
|
|
12
|
+
import numpy as np
|
|
13
|
+
import yaml
|
|
14
|
+
|
|
15
|
+
from sofic.base import StateMachine
|
|
16
|
+
from sofic.graph import (
|
|
17
|
+
ATTR_EMISSION,
|
|
18
|
+
ATTR_EMISSION_DIST,
|
|
19
|
+
ATTR_KIND,
|
|
20
|
+
ATTR_SYMBOL,
|
|
21
|
+
EPSILON,
|
|
22
|
+
KIND_CALL,
|
|
23
|
+
KIND_INTERNAL,
|
|
24
|
+
KIND_RETURN,
|
|
25
|
+
TransitionGraph,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
SCHEMA = "sofic.model"
|
|
29
|
+
VERSION = 1
|
|
30
|
+
_TYPE_KEY = "__sofic_type__"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class _ModelSpec:
|
|
35
|
+
cls: type[StateMachine]
|
|
36
|
+
fields: tuple[str, ...]
|
|
37
|
+
builder: str = "default"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def model_to_yaml(model: StateMachine) -> str:
|
|
41
|
+
"""Return a YAML representation of ``model``."""
|
|
42
|
+
return yaml.safe_dump(model_to_dict(model), sort_keys=False)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def model_from_yaml(text: str, *, validate: bool = True) -> StateMachine:
|
|
46
|
+
"""Reconstruct a sofic model from YAML text."""
|
|
47
|
+
loaded = yaml.safe_load(text)
|
|
48
|
+
if not isinstance(loaded, dict):
|
|
49
|
+
raise TypeError("sofic model YAML must load to a mapping")
|
|
50
|
+
return model_from_dict(loaded, validate=validate)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def from_yaml(text: str, *, validate: bool = True) -> StateMachine:
|
|
54
|
+
"""Alias for :func:`model_from_yaml`."""
|
|
55
|
+
return model_from_yaml(text, validate=validate)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_yaml(path: str | Path, *, validate: bool = True) -> StateMachine:
|
|
59
|
+
"""Read a sofic model from a YAML file."""
|
|
60
|
+
return model_from_yaml(Path(path).read_text(encoding="utf-8"), validate=validate)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def model_to_dict(model: StateMachine) -> dict[str, Any]:
|
|
64
|
+
"""Return a safe-YAML-compatible mapping for ``model``."""
|
|
65
|
+
if not isinstance(model, StateMachine):
|
|
66
|
+
raise TypeError(f"expected a StateMachine, got {type(model).__name__}")
|
|
67
|
+
registry = _registry_by_type()
|
|
68
|
+
spec = registry.get(type(model))
|
|
69
|
+
if spec is None:
|
|
70
|
+
raise TypeError(f"YAML serialization is not registered for {type(model).__qualname__}")
|
|
71
|
+
return {
|
|
72
|
+
"schema": SCHEMA,
|
|
73
|
+
"version": VERSION,
|
|
74
|
+
"class": _class_path(type(model)),
|
|
75
|
+
"graph": _graph_to_data(model.graph),
|
|
76
|
+
"metadata": _encode(_metadata_for(model, spec)),
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def model_from_dict(data: Mapping[str, Any], *, validate: bool = True) -> StateMachine:
|
|
81
|
+
"""Reconstruct a sofic model from a decoded YAML mapping."""
|
|
82
|
+
if data.get("schema") != SCHEMA:
|
|
83
|
+
raise ValueError(f"unsupported sofic YAML schema {data.get('schema')!r}")
|
|
84
|
+
if data.get("version") != VERSION:
|
|
85
|
+
raise ValueError(f"unsupported sofic YAML version {data.get('version')!r}")
|
|
86
|
+
class_path = data.get("class")
|
|
87
|
+
if not isinstance(class_path, str):
|
|
88
|
+
raise TypeError("sofic model YAML requires a string class path")
|
|
89
|
+
spec = _registry_by_path().get(class_path)
|
|
90
|
+
if spec is None:
|
|
91
|
+
raise ValueError(f"unregistered sofic model class {class_path!r}")
|
|
92
|
+
|
|
93
|
+
graph = _graph_from_data(data.get("graph", {}), validate=validate)
|
|
94
|
+
metadata = _decode(data.get("metadata", {}), validate=validate)
|
|
95
|
+
if not isinstance(metadata, dict):
|
|
96
|
+
raise TypeError("sofic model metadata must decode to a mapping")
|
|
97
|
+
|
|
98
|
+
model = _build_model(spec, graph, metadata)
|
|
99
|
+
if validate:
|
|
100
|
+
model.validate()
|
|
101
|
+
return model
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _metadata_for(model: StateMachine, spec: _ModelSpec) -> dict[str, Any]:
|
|
105
|
+
return {field: getattr(model, field) for field in spec.fields}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _build_model(spec: _ModelSpec, graph: TransitionGraph, metadata: dict[str, Any]) -> StateMachine:
|
|
109
|
+
if spec.builder == "composite_vpa":
|
|
110
|
+
return spec.cls(operation=metadata["operation"], operands=metadata["operands"])
|
|
111
|
+
if spec.builder == "hidden_hmm":
|
|
112
|
+
return spec.cls(graph=graph, observation_alphabet=_observation_alphabet(graph), **metadata)
|
|
113
|
+
if spec.builder == "pfa":
|
|
114
|
+
return spec.cls(graph=graph, output_alphabet=_edge_emission_alphabet(graph), **metadata)
|
|
115
|
+
if spec.builder == "stack_hmm":
|
|
116
|
+
return spec.cls(graph=graph, **_stack_alphabets(graph), **metadata)
|
|
117
|
+
if spec.builder == "sft":
|
|
118
|
+
has_spec = bool(metadata.pop("_has_forbidden_word_spec"))
|
|
119
|
+
forbidden = metadata.pop("_forbidden_words")
|
|
120
|
+
return spec.cls(
|
|
121
|
+
forbidden_words=forbidden if has_spec else None,
|
|
122
|
+
graph=graph,
|
|
123
|
+
**metadata,
|
|
124
|
+
)
|
|
125
|
+
return spec.cls(graph=graph, **metadata)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _edge_emission_alphabet(graph: TransitionGraph) -> frozenset[Any]:
|
|
129
|
+
return frozenset(
|
|
130
|
+
transition.data[ATTR_EMISSION] for transition in graph.transitions() if ATTR_EMISSION in transition.data
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _state_emission_alphabet(graph: TransitionGraph) -> frozenset[Any]:
|
|
135
|
+
symbols: set[Any] = set()
|
|
136
|
+
for state in graph.states():
|
|
137
|
+
distribution = graph.state_attrs(state).get(ATTR_EMISSION_DIST)
|
|
138
|
+
if isinstance(distribution, Mapping):
|
|
139
|
+
symbols.update(distribution)
|
|
140
|
+
return frozenset(symbols)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _observation_alphabet(graph: TransitionGraph) -> frozenset[Any]:
|
|
144
|
+
return _edge_emission_alphabet(graph) | _state_emission_alphabet(graph)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _stack_alphabets(graph: TransitionGraph) -> dict[str, frozenset[Any]]:
|
|
148
|
+
calls: set[Any] = set()
|
|
149
|
+
returns: set[Any] = set()
|
|
150
|
+
internals: set[Any] = set()
|
|
151
|
+
for transition in graph.transitions():
|
|
152
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
153
|
+
if symbol is None:
|
|
154
|
+
continue
|
|
155
|
+
kind = transition.data.get(ATTR_KIND)
|
|
156
|
+
if kind == KIND_CALL:
|
|
157
|
+
calls.add(symbol)
|
|
158
|
+
elif kind == KIND_RETURN:
|
|
159
|
+
returns.add(symbol)
|
|
160
|
+
elif kind == KIND_INTERNAL:
|
|
161
|
+
internals.add(symbol)
|
|
162
|
+
symbol_alphabet = frozenset(calls | returns | internals)
|
|
163
|
+
return {
|
|
164
|
+
"call_alphabet": frozenset(calls),
|
|
165
|
+
"return_alphabet": frozenset(returns),
|
|
166
|
+
"internal_alphabet": frozenset(internals),
|
|
167
|
+
"symbol_alphabet": symbol_alphabet,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _graph_to_data(graph: TransitionGraph) -> dict[str, Any]:
|
|
172
|
+
nodes = [{"id": _encode(state), "attrs": _encode(dict(attrs))} for state, attrs in graph.nx.nodes(data=True)]
|
|
173
|
+
edges = [
|
|
174
|
+
{
|
|
175
|
+
"source": _encode(source),
|
|
176
|
+
"target": _encode(target),
|
|
177
|
+
"key": _encode(key),
|
|
178
|
+
"attrs": _encode(dict(attrs)),
|
|
179
|
+
}
|
|
180
|
+
for source, target, key, attrs in graph.nx.edges(keys=True, data=True)
|
|
181
|
+
]
|
|
182
|
+
return {"nodes": nodes, "edges": edges}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _graph_from_data(data: Any, *, validate: bool = True) -> TransitionGraph:
|
|
186
|
+
if not isinstance(data, Mapping):
|
|
187
|
+
raise TypeError("sofic model graph must be a mapping")
|
|
188
|
+
graph = nx.MultiDiGraph()
|
|
189
|
+
for node in data.get("nodes", []):
|
|
190
|
+
if not isinstance(node, Mapping):
|
|
191
|
+
raise TypeError("graph node records must be mappings")
|
|
192
|
+
state = _decode(node["id"], validate=validate)
|
|
193
|
+
attrs = _decode(node.get("attrs", {_TYPE_KEY: "dict", "items": []}), validate=validate)
|
|
194
|
+
if not isinstance(attrs, dict):
|
|
195
|
+
raise TypeError("graph node attrs must decode to a dict")
|
|
196
|
+
graph.add_node(state, **attrs)
|
|
197
|
+
for edge in data.get("edges", []):
|
|
198
|
+
if not isinstance(edge, Mapping):
|
|
199
|
+
raise TypeError("graph edge records must be mappings")
|
|
200
|
+
source = _decode(edge["source"], validate=validate)
|
|
201
|
+
target = _decode(edge["target"], validate=validate)
|
|
202
|
+
key = _decode(edge["key"], validate=validate)
|
|
203
|
+
attrs = _decode(edge.get("attrs", {_TYPE_KEY: "dict", "items": []}), validate=validate)
|
|
204
|
+
if not isinstance(attrs, dict):
|
|
205
|
+
raise TypeError("graph edge attrs must decode to a dict")
|
|
206
|
+
graph.add_edge(source, target, key=key, **attrs)
|
|
207
|
+
return TransitionGraph(graph)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _encode(value: Any) -> Any:
|
|
211
|
+
from sofic.generators.mixed_state import MixedState
|
|
212
|
+
|
|
213
|
+
if value is EPSILON:
|
|
214
|
+
return {_TYPE_KEY: "epsilon"}
|
|
215
|
+
if isinstance(value, MixedState):
|
|
216
|
+
return {_TYPE_KEY: "mixed_state", "belief": [_encode(item) for item in value.belief]}
|
|
217
|
+
if isinstance(value, StateMachine):
|
|
218
|
+
return {_TYPE_KEY: "model", "value": model_to_dict(value)}
|
|
219
|
+
if isinstance(value, np.ndarray):
|
|
220
|
+
return {
|
|
221
|
+
_TYPE_KEY: "ndarray",
|
|
222
|
+
"dtype": str(value.dtype),
|
|
223
|
+
"shape": list(value.shape),
|
|
224
|
+
"data": _encode(value.tolist()),
|
|
225
|
+
}
|
|
226
|
+
if isinstance(value, np.generic):
|
|
227
|
+
return _encode(value.item())
|
|
228
|
+
if value is None or isinstance(value, str | bool | int | float):
|
|
229
|
+
return value
|
|
230
|
+
if isinstance(value, tuple):
|
|
231
|
+
return {_TYPE_KEY: "tuple", "items": [_encode(item) for item in value]}
|
|
232
|
+
if isinstance(value, frozenset):
|
|
233
|
+
return {_TYPE_KEY: "frozenset", "items": [_encode(item) for item in _stable_iterable(value)]}
|
|
234
|
+
if isinstance(value, set):
|
|
235
|
+
return {_TYPE_KEY: "set", "items": [_encode(item) for item in _stable_iterable(value)]}
|
|
236
|
+
if isinstance(value, list):
|
|
237
|
+
return [_encode(item) for item in value]
|
|
238
|
+
if isinstance(value, Mapping):
|
|
239
|
+
return {
|
|
240
|
+
_TYPE_KEY: "dict",
|
|
241
|
+
"items": [{"key": _encode(key), "value": _encode(item_value)} for key, item_value in value.items()],
|
|
242
|
+
}
|
|
243
|
+
raise TypeError(f"cannot YAML-serialize value of type {type(value).__qualname__}: {value!r}")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _decode(value: Any, *, validate: bool = True) -> Any:
|
|
247
|
+
from sofic.generators.mixed_state import MixedState
|
|
248
|
+
|
|
249
|
+
if isinstance(value, list):
|
|
250
|
+
return [_decode(item, validate=validate) for item in value]
|
|
251
|
+
if not isinstance(value, dict):
|
|
252
|
+
return value
|
|
253
|
+
tag = value.get(_TYPE_KEY)
|
|
254
|
+
if tag is None:
|
|
255
|
+
return {key: _decode(item_value, validate=validate) for key, item_value in value.items()}
|
|
256
|
+
if tag == "epsilon":
|
|
257
|
+
return EPSILON
|
|
258
|
+
if tag == "mixed_state":
|
|
259
|
+
return MixedState(tuple(float(_decode(item, validate=validate)) for item in value["belief"]))
|
|
260
|
+
if tag == "model":
|
|
261
|
+
return model_from_dict(value["value"], validate=validate)
|
|
262
|
+
if tag == "ndarray":
|
|
263
|
+
array = np.asarray(_decode(value["data"], validate=validate), dtype=value["dtype"])
|
|
264
|
+
return array.reshape(tuple(value["shape"]))
|
|
265
|
+
if tag == "tuple":
|
|
266
|
+
return tuple(_decode(item, validate=validate) for item in value["items"])
|
|
267
|
+
if tag == "frozenset":
|
|
268
|
+
return frozenset(_decode(item, validate=validate) for item in value["items"])
|
|
269
|
+
if tag == "set":
|
|
270
|
+
return {_decode(item, validate=validate) for item in value["items"]}
|
|
271
|
+
if tag == "dict":
|
|
272
|
+
return {
|
|
273
|
+
_decode(item["key"], validate=validate): _decode(item["value"], validate=validate)
|
|
274
|
+
for item in value["items"]
|
|
275
|
+
}
|
|
276
|
+
raise ValueError(f"unknown sofic YAML value tag {tag!r}")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _stable_iterable(values: set[Any] | frozenset[Any]) -> list[Any]:
|
|
280
|
+
return sorted(values, key=repr)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _class_path(cls: type[Any]) -> str:
|
|
284
|
+
return f"{cls.__module__}.{cls.__qualname__}"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@cache
|
|
288
|
+
def _registry_by_path() -> dict[str, _ModelSpec]:
|
|
289
|
+
return {_class_path(spec.cls): spec for spec in _specs()}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@cache
|
|
293
|
+
def _registry_by_type() -> dict[type[StateMachine], _ModelSpec]:
|
|
294
|
+
return {spec.cls: spec for spec in _specs()}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _spec(cls: type[StateMachine], fields: tuple[str, ...], builder: str = "default") -> _ModelSpec:
|
|
298
|
+
return _ModelSpec(cls=cls, fields=fields, builder=builder)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@cache
|
|
302
|
+
def _specs() -> tuple[_ModelSpec, ...]:
|
|
303
|
+
from sofic.automata.atomaton import Atomaton, AtomicAutomaton, MaximizedPrimeAtomaton
|
|
304
|
+
from sofic.automata.buchi import BuchiAutomaton
|
|
305
|
+
from sofic.automata.dfa import DFA
|
|
306
|
+
from sofic.automata.nfa import NFA
|
|
307
|
+
from sofic.automata.nwa import NestedWordAutomaton
|
|
308
|
+
from sofic.automata.rfsa import CanonicalRFSA, ResidualFiniteStateAutomaton
|
|
309
|
+
from sofic.automata.subsequential import SubsequentialTransducer, WeightedFiniteStateTransducer
|
|
310
|
+
from sofic.automata.transducers import MealyMachine, MooreMachine
|
|
311
|
+
from sofic.automata.unifilar import UnifilarAutomaton
|
|
312
|
+
from sofic.automata.vpa import (
|
|
313
|
+
CallDrivenAutomaton,
|
|
314
|
+
CanonicalVisiblyPushdownAutomaton,
|
|
315
|
+
CompositeVisiblyPushdownAutomaton,
|
|
316
|
+
DeterministicVisiblyPushdownAutomaton,
|
|
317
|
+
MultipleEntryVisiblyPushdownAutomaton,
|
|
318
|
+
SingleEntryVisiblyPushdownAutomaton,
|
|
319
|
+
VisiblyPushdownAutomaton,
|
|
320
|
+
)
|
|
321
|
+
from sofic.generators.base import HiddenMarkovModel, QuasiStochasticModel, StochasticModel
|
|
322
|
+
from sofic.generators.bidirectional_epsilon_machine import BidirectionalEpsilonMachine
|
|
323
|
+
from sofic.generators.epsilon_machine import EpsilonMachine
|
|
324
|
+
from sofic.generators.epsilon_transducer import EpsilonTransducer
|
|
325
|
+
from sofic.generators.markov import MarkovChain
|
|
326
|
+
from sofic.generators.mealy import MealyHMM
|
|
327
|
+
from sofic.generators.mixed_state import MixedStatePresentation
|
|
328
|
+
from sofic.generators.moore import MooreHMM
|
|
329
|
+
from sofic.generators.nmachine import NMachine
|
|
330
|
+
from sofic.generators.pfa import ProbabilisticFiniteAutomaton
|
|
331
|
+
from sofic.generators.quasi_realization import QuasiRealization
|
|
332
|
+
from sofic.generators.stack_hmm import HiddenMarkovStackModel
|
|
333
|
+
from sofic.shifts.base import SymbolicModel
|
|
334
|
+
from sofic.shifts.covers import LeftFischerCover, LeftKriegerCover, RightFischerCover, RightKriegerCover
|
|
335
|
+
from sofic.shifts.markov_dyck import MarkovDyckShift
|
|
336
|
+
from sofic.shifts.sft import ShiftOfFiniteType
|
|
337
|
+
from sofic.shifts.sofic import SoficShift
|
|
338
|
+
from sofic.shifts.sofic_dyck import SoficDyckShift
|
|
339
|
+
from sofic.shifts.sofic_relation import SoficRelation
|
|
340
|
+
from sofic.shifts.tmc import TopologicalMarkovChain
|
|
341
|
+
|
|
342
|
+
labeled = ("input_alphabet", "initial_states", "accepting_states")
|
|
343
|
+
transducer = ("input_alphabet", "output_alphabet", "initial_states")
|
|
344
|
+
epsilon_transducer = ("input_alphabet", "output_alphabet", "initial_states", "initial_distribution")
|
|
345
|
+
symbolic = ("symbol_alphabet",)
|
|
346
|
+
stochastic = ("initial_distribution",)
|
|
347
|
+
hidden = ("initial_distribution",)
|
|
348
|
+
quasi = ("initial_quasidistribution",)
|
|
349
|
+
vpa = (
|
|
350
|
+
"input_alphabet",
|
|
351
|
+
"call_alphabet",
|
|
352
|
+
"return_alphabet",
|
|
353
|
+
"internal_alphabet",
|
|
354
|
+
"stack_alphabet",
|
|
355
|
+
"bottom_stack_symbol",
|
|
356
|
+
"initial_state",
|
|
357
|
+
"accepting_states",
|
|
358
|
+
)
|
|
359
|
+
cda = (
|
|
360
|
+
*vpa,
|
|
361
|
+
"modules",
|
|
362
|
+
"base_module",
|
|
363
|
+
"call_partition",
|
|
364
|
+
"call_entries",
|
|
365
|
+
)
|
|
366
|
+
nwa = (
|
|
367
|
+
"input_alphabet",
|
|
368
|
+
"call_alphabet",
|
|
369
|
+
"return_alphabet",
|
|
370
|
+
"internal_alphabet",
|
|
371
|
+
"hier_alphabet",
|
|
372
|
+
"bottom_hier_state",
|
|
373
|
+
"initial_state",
|
|
374
|
+
"accepting_states",
|
|
375
|
+
)
|
|
376
|
+
dyck = (
|
|
377
|
+
"symbol_alphabet",
|
|
378
|
+
"call_alphabet",
|
|
379
|
+
"return_alphabet",
|
|
380
|
+
"internal_alphabet",
|
|
381
|
+
"matched_edges",
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
_spec(NFA, labeled),
|
|
386
|
+
_spec(DFA, labeled),
|
|
387
|
+
_spec(BuchiAutomaton, labeled),
|
|
388
|
+
_spec(UnifilarAutomaton, labeled),
|
|
389
|
+
_spec(AtomicAutomaton, labeled),
|
|
390
|
+
_spec(Atomaton, labeled),
|
|
391
|
+
_spec(MaximizedPrimeAtomaton, labeled),
|
|
392
|
+
_spec(ResidualFiniteStateAutomaton, labeled),
|
|
393
|
+
_spec(CanonicalRFSA, labeled),
|
|
394
|
+
_spec(MealyMachine, transducer),
|
|
395
|
+
_spec(MooreMachine, transducer),
|
|
396
|
+
_spec(EpsilonTransducer, epsilon_transducer),
|
|
397
|
+
_spec(SubsequentialTransducer, (*transducer, "final_output")),
|
|
398
|
+
_spec(WeightedFiniteStateTransducer, (*transducer, "semiring")),
|
|
399
|
+
_spec(NestedWordAutomaton, nwa),
|
|
400
|
+
_spec(VisiblyPushdownAutomaton, vpa),
|
|
401
|
+
_spec(DeterministicVisiblyPushdownAutomaton, vpa),
|
|
402
|
+
_spec(CallDrivenAutomaton, cda),
|
|
403
|
+
_spec(MultipleEntryVisiblyPushdownAutomaton, (*cda, "entry_states")),
|
|
404
|
+
_spec(SingleEntryVisiblyPushdownAutomaton, (*cda, "entry_states")),
|
|
405
|
+
_spec(CanonicalVisiblyPushdownAutomaton, (*vpa, "summary_representatives")),
|
|
406
|
+
_spec(CompositeVisiblyPushdownAutomaton, ("operation", "operands"), builder="composite_vpa"),
|
|
407
|
+
_spec(StochasticModel, stochastic),
|
|
408
|
+
_spec(HiddenMarkovModel, hidden, builder="hidden_hmm"),
|
|
409
|
+
_spec(MarkovChain, stochastic),
|
|
410
|
+
_spec(MealyHMM, hidden, builder="hidden_hmm"),
|
|
411
|
+
_spec(MooreHMM, hidden, builder="hidden_hmm"),
|
|
412
|
+
_spec(EpsilonMachine, hidden, builder="hidden_hmm"),
|
|
413
|
+
_spec(BidirectionalEpsilonMachine, (*hidden, "forward_machine", "reverse_machine"), builder="hidden_hmm"),
|
|
414
|
+
_spec(
|
|
415
|
+
MixedStatePresentation,
|
|
416
|
+
(
|
|
417
|
+
*hidden,
|
|
418
|
+
"basis_states",
|
|
419
|
+
"initial_mixed_state",
|
|
420
|
+
"pure_states",
|
|
421
|
+
"recurrent_states",
|
|
422
|
+
"transient_states",
|
|
423
|
+
),
|
|
424
|
+
builder="hidden_hmm",
|
|
425
|
+
),
|
|
426
|
+
_spec(ProbabilisticFiniteAutomaton, ("initial_distribution",), builder="pfa"),
|
|
427
|
+
_spec(
|
|
428
|
+
HiddenMarkovStackModel,
|
|
429
|
+
(
|
|
430
|
+
"initial_distribution",
|
|
431
|
+
"matched_edges",
|
|
432
|
+
"allow_empty_stack_returns",
|
|
433
|
+
),
|
|
434
|
+
builder="stack_hmm",
|
|
435
|
+
),
|
|
436
|
+
_spec(QuasiStochasticModel, quasi),
|
|
437
|
+
_spec(NMachine, quasi, builder="hidden_hmm"),
|
|
438
|
+
_spec(QuasiRealization, (*quasi, "pi", "tau", "symbol_maps")),
|
|
439
|
+
_spec(SymbolicModel, symbolic),
|
|
440
|
+
_spec(SoficShift, symbolic),
|
|
441
|
+
_spec(SoficRelation, symbolic),
|
|
442
|
+
_spec(TopologicalMarkovChain, symbolic),
|
|
443
|
+
_spec(ShiftOfFiniteType, (*symbolic, "_forbidden_words", "_has_forbidden_word_spec"), builder="sft"),
|
|
444
|
+
_spec(SoficDyckShift, dyck),
|
|
445
|
+
_spec(MarkovDyckShift, dyck),
|
|
446
|
+
_spec(LeftFischerCover, symbolic),
|
|
447
|
+
_spec(RightFischerCover, symbolic),
|
|
448
|
+
_spec(LeftKriegerCover, symbolic),
|
|
449
|
+
_spec(RightKriegerCover, symbolic),
|
|
450
|
+
)
|
sofic/shifts/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Symbolic shifts and subshifts."""
|
|
2
|
+
|
|
3
|
+
from sofic.shifts.base import SymbolicModel
|
|
4
|
+
from sofic.shifts.covers import (
|
|
5
|
+
LeftFischerCover,
|
|
6
|
+
LeftKriegerCover,
|
|
7
|
+
RightFischerCover,
|
|
8
|
+
RightKriegerCover,
|
|
9
|
+
)
|
|
10
|
+
from sofic.shifts.dyck_enumeration import (
|
|
11
|
+
DyckGraphString,
|
|
12
|
+
count_dyck_graph_strings,
|
|
13
|
+
dyck_graph_string_to_shift,
|
|
14
|
+
iter_dyck_graph_strings,
|
|
15
|
+
iter_sofic_dyck_topologies,
|
|
16
|
+
shift_to_dyck_graph_string,
|
|
17
|
+
)
|
|
18
|
+
from sofic.shifts.markov_dyck import MarkovDyckShift
|
|
19
|
+
from sofic.shifts.sft import ShiftOfFiniteType
|
|
20
|
+
from sofic.shifts.sliding_block_code import SlidingBlockCode, full_shift
|
|
21
|
+
from sofic.shifts.sofic import SoficShift
|
|
22
|
+
from sofic.shifts.sofic_dyck import SoficDyckShift
|
|
23
|
+
from sofic.shifts.sofic_relation import SoficRelation
|
|
24
|
+
from sofic.shifts.textile import TextileSystem
|
|
25
|
+
from sofic.shifts.tmc import TopologicalMarkovChain
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"DyckGraphString",
|
|
29
|
+
"count_dyck_graph_strings",
|
|
30
|
+
"dyck_graph_string_to_shift",
|
|
31
|
+
"iter_dyck_graph_strings",
|
|
32
|
+
"iter_sofic_dyck_topologies",
|
|
33
|
+
"LeftFischerCover",
|
|
34
|
+
"LeftKriegerCover",
|
|
35
|
+
"MarkovDyckShift",
|
|
36
|
+
"RightFischerCover",
|
|
37
|
+
"RightKriegerCover",
|
|
38
|
+
"ShiftOfFiniteType",
|
|
39
|
+
"SlidingBlockCode",
|
|
40
|
+
"SoficShift",
|
|
41
|
+
"shift_to_dyck_graph_string",
|
|
42
|
+
"SoficDyckShift",
|
|
43
|
+
"SoficRelation",
|
|
44
|
+
"SymbolicModel",
|
|
45
|
+
"TextileSystem",
|
|
46
|
+
"TopologicalMarkovChain",
|
|
47
|
+
"full_shift",
|
|
48
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Shift presentations: factor languages, trimming, entropy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from collections.abc import Hashable, Iterator
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from sofic.graph import ATTR_SYMBOL
|
|
12
|
+
from sofic.shifts.base import SymbolicModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def trim_transient(model: SymbolicModel) -> SymbolicModel:
|
|
16
|
+
"""Remove states not on bi-infinite paths."""
|
|
17
|
+
forward = _forward_reachable(model)
|
|
18
|
+
backward = _backward_reachable(model)
|
|
19
|
+
keep = forward & backward
|
|
20
|
+
result = model.copy()
|
|
21
|
+
for state in list(result.states()):
|
|
22
|
+
if state not in keep:
|
|
23
|
+
result.graph.nx.remove_node(state)
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def factor_language(model: SymbolicModel, length: int) -> Iterator[tuple[Any, ...]]:
|
|
28
|
+
"""Yield distinct factor words of the given length."""
|
|
29
|
+
if length <= 0:
|
|
30
|
+
yield ()
|
|
31
|
+
return
|
|
32
|
+
seen: set[tuple[Any, ...]] = set()
|
|
33
|
+
for start in model.states():
|
|
34
|
+
queue: deque[tuple[Hashable, tuple[Any, ...]]] = deque([(start, ())])
|
|
35
|
+
while queue:
|
|
36
|
+
state, prefix = queue.popleft()
|
|
37
|
+
if len(prefix) == length:
|
|
38
|
+
if prefix not in seen:
|
|
39
|
+
seen.add(prefix)
|
|
40
|
+
yield prefix
|
|
41
|
+
continue
|
|
42
|
+
for transition in model.graph.out_transitions(state):
|
|
43
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
44
|
+
if symbol is None:
|
|
45
|
+
continue
|
|
46
|
+
queue.append((transition.target, prefix + (symbol,)))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def adjacency_matrix(model: SymbolicModel) -> tuple[np.ndarray, tuple[Hashable, ...]]:
|
|
50
|
+
states = tuple(model.states())
|
|
51
|
+
index = {state: i for i, state in enumerate(states)}
|
|
52
|
+
n = len(states)
|
|
53
|
+
matrix = np.zeros((n, n), dtype=float)
|
|
54
|
+
for transition in model.transitions():
|
|
55
|
+
i = index[transition.source]
|
|
56
|
+
j = index[transition.target]
|
|
57
|
+
matrix[i, j] += 1.0
|
|
58
|
+
return matrix, states
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def topological_entropy_from_matrix(matrix: np.ndarray) -> float:
|
|
62
|
+
if matrix.size == 0:
|
|
63
|
+
return 0.0
|
|
64
|
+
eigenvalues = np.linalg.eigvals(matrix)
|
|
65
|
+
spectral_radius = float(np.max(np.abs(eigenvalues)))
|
|
66
|
+
return float(np.log(max(spectral_radius, 0.0)))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _forward_reachable(model: SymbolicModel) -> set[Hashable]:
|
|
70
|
+
return set(model.graph.forward_reachable(set(model.states())))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _backward_reachable(model: SymbolicModel) -> set[Hashable]:
|
|
74
|
+
reverse = model.graph.reverse()
|
|
75
|
+
reachable: set[Hashable] = set()
|
|
76
|
+
queue = deque(reverse.states())
|
|
77
|
+
while queue:
|
|
78
|
+
state = queue.popleft()
|
|
79
|
+
if state in reachable:
|
|
80
|
+
continue
|
|
81
|
+
reachable.add(state)
|
|
82
|
+
for transition in reverse.out_transitions(state):
|
|
83
|
+
queue.append(transition.target)
|
|
84
|
+
return reachable
|
sofic/shifts/base.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Symbolic dynamical systems base class."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable, Iterator
|
|
6
|
+
from typing import Any, Self
|
|
7
|
+
|
|
8
|
+
from sofic.base import StateMachine
|
|
9
|
+
from sofic.graph import ATTR_SYMBOL
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SymbolicModel(StateMachine):
|
|
13
|
+
"""Labeled transition system for shift presentations."""
|
|
14
|
+
|
|
15
|
+
symbol_alphabet: frozenset[Any]
|
|
16
|
+
|
|
17
|
+
def __init__(self, symbol_alphabet: frozenset[Any] | None = None, **kwargs: Any) -> None:
|
|
18
|
+
super().__init__(**kwargs)
|
|
19
|
+
self.symbol_alphabet = symbol_alphabet if symbol_alphabet is not None else frozenset()
|
|
20
|
+
|
|
21
|
+
def add_transition(self, source: Hashable, target: Hashable, symbol: Any, **attrs: Any) -> int:
|
|
22
|
+
"""Add a labeled transition in the shift presentation."""
|
|
23
|
+
return self.graph.add_transition(source, target, **{ATTR_SYMBOL: symbol, **attrs})
|
|
24
|
+
|
|
25
|
+
def validate(self) -> None:
|
|
26
|
+
for transition in self.transitions():
|
|
27
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
28
|
+
if symbol is not None:
|
|
29
|
+
self._require(symbol in self.symbol_alphabet, f"symbol {symbol!r} not in alphabet")
|
|
30
|
+
|
|
31
|
+
def factor_language(self, length: int) -> Iterator[tuple[Any, ...]]:
|
|
32
|
+
from sofic.shifts.algorithms import factor_language
|
|
33
|
+
|
|
34
|
+
yield from factor_language(self, length)
|
|
35
|
+
|
|
36
|
+
def words_of_length(self, length: int) -> Iterator[tuple[Any, ...]]:
|
|
37
|
+
"""Yield distinct factor words of exactly ``length`` symbols."""
|
|
38
|
+
yield from self.factor_language(length)
|
|
39
|
+
|
|
40
|
+
def is_unifilar(self) -> bool:
|
|
41
|
+
"""Return whether this presentation is right-resolving (unifilar)."""
|
|
42
|
+
from sofic.properties import is_unifilar_symbols
|
|
43
|
+
|
|
44
|
+
return is_unifilar_symbols(self)
|
|
45
|
+
|
|
46
|
+
def trim_transient(self) -> Self:
|
|
47
|
+
from sofic.shifts.algorithms import trim_transient
|
|
48
|
+
|
|
49
|
+
return trim_transient(self)
|