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.
Files changed (150) hide show
  1. sofic/__init__.py +185 -0
  2. sofic/automata/__init__.py +207 -0
  3. sofic/automata/_config_simulation.py +40 -0
  4. sofic/automata/active.py +611 -0
  5. sofic/automata/alergia.py +222 -0
  6. sofic/automata/algorithms.py +376 -0
  7. sofic/automata/atomaton.py +58 -0
  8. sofic/automata/base.py +161 -0
  9. sofic/automata/buchi.py +23 -0
  10. sofic/automata/buchi_simulation.py +67 -0
  11. sofic/automata/canonical_dual.py +18 -0
  12. sofic/automata/canonical_extraction.py +122 -0
  13. sofic/automata/dfa.py +85 -0
  14. sofic/automata/dfasat.py +195 -0
  15. sofic/automata/edsm.py +219 -0
  16. sofic/automata/enumeration.py +44 -0
  17. sofic/automata/icdfa.py +421 -0
  18. sofic/automata/idfa.py +363 -0
  19. sofic/automata/languages/__init__.py +39 -0
  20. sofic/automata/languages/_quotient_utils.py +64 -0
  21. sofic/automata/languages/atoms.py +31 -0
  22. sofic/automata/languages/automaton_ops.py +243 -0
  23. sofic/automata/languages/base.py +67 -0
  24. sofic/automata/languages/operations.py +78 -0
  25. sofic/automata/languages/quotients.py +66 -0
  26. sofic/automata/languages/residuals.py +25 -0
  27. sofic/automata/learning.py +79 -0
  28. sofic/automata/nfa.py +39 -0
  29. sofic/automata/nwa.py +343 -0
  30. sofic/automata/nwa_simulation.py +56 -0
  31. sofic/automata/observation.py +40 -0
  32. sofic/automata/papni.py +301 -0
  33. sofic/automata/regex.py +128 -0
  34. sofic/automata/rfsa.py +35 -0
  35. sofic/automata/rpni.py +193 -0
  36. sofic/automata/subsequential.py +201 -0
  37. sofic/automata/transducer_operations.py +350 -0
  38. sofic/automata/transducer_simulation.py +150 -0
  39. sofic/automata/transducers.py +365 -0
  40. sofic/automata/unifilar.py +107 -0
  41. sofic/automata/vpa.py +1373 -0
  42. sofic/automata/vpa_simulation.py +53 -0
  43. sofic/base.py +153 -0
  44. sofic/core.py +47 -0
  45. sofic/examples/__init__.py +86 -0
  46. sofic/examples/epsilon_machines.py +1089 -0
  47. sofic/examples/processes.py +1491 -0
  48. sofic/examples/shifts.py +144 -0
  49. sofic/exceptions.py +33 -0
  50. sofic/generators/__init__.py +115 -0
  51. sofic/generators/_word_measures.py +94 -0
  52. sofic/generators/alternative_complexity.py +104 -0
  53. sofic/generators/base.py +327 -0
  54. sofic/generators/bidirectional_construction.py +717 -0
  55. sofic/generators/bidirectional_epsilon_machine.py +689 -0
  56. sofic/generators/block_convergence.py +668 -0
  57. sofic/generators/block_entropy.py +578 -0
  58. sofic/generators/channel_measures.py +75 -0
  59. sofic/generators/conversions.py +182 -0
  60. sofic/generators/directional_flow.py +245 -0
  61. sofic/generators/edge_emissions.py +36 -0
  62. sofic/generators/edge_machine.py +178 -0
  63. sofic/generators/epsilon_construction.py +193 -0
  64. sofic/generators/epsilon_inference.py +703 -0
  65. sofic/generators/epsilon_machine.py +557 -0
  66. sofic/generators/epsilon_transducer.py +168 -0
  67. sofic/generators/epsilon_transducer_construction.py +185 -0
  68. sofic/generators/epsilon_transducer_inference.py +499 -0
  69. sofic/generators/hmm_inference.py +719 -0
  70. sofic/generators/information_diagram.py +428 -0
  71. sofic/generators/lumping.py +447 -0
  72. sofic/generators/markov.py +100 -0
  73. sofic/generators/mealy.py +156 -0
  74. sofic/generators/measures.py +257 -0
  75. sofic/generators/minimal_generative_model.py +821 -0
  76. sofic/generators/mixed_state.py +250 -0
  77. sofic/generators/mixed_state_construction.py +163 -0
  78. sofic/generators/moore.py +75 -0
  79. sofic/generators/nmachine.py +78 -0
  80. sofic/generators/nmachine_construction.py +70 -0
  81. sofic/generators/pfa.py +100 -0
  82. sofic/generators/prob.py +291 -0
  83. sofic/generators/process_equivalence.py +207 -0
  84. sofic/generators/quasi_inference.py +74 -0
  85. sofic/generators/quasi_realization.py +97 -0
  86. sofic/generators/reversal.py +66 -0
  87. sofic/generators/stack_hmm.py +426 -0
  88. sofic/generators/stack_inference.py +509 -0
  89. sofic/generators/stationary.py +134 -0
  90. sofic/generators/stochastic.py +65 -0
  91. sofic/generators/synchronization.py +407 -0
  92. sofic/generators/topological_epsilon_enumeration.py +349 -0
  93. sofic/generators/words.py +226 -0
  94. sofic/graph.py +135 -0
  95. sofic/indexing.py +31 -0
  96. sofic/inference/__init__.py +45 -0
  97. sofic/inference/bayesian/__init__.py +68 -0
  98. sofic/inference/bayesian/comparison.py +199 -0
  99. sofic/inference/bayesian/counts.py +219 -0
  100. sofic/inference/bayesian/diversity.py +254 -0
  101. sofic/inference/bayesian/epsilon.py +270 -0
  102. sofic/inference/bayesian/hdp_hmm.py +340 -0
  103. sofic/inference/bayesian/markov.py +294 -0
  104. sofic/inference/bayesian/pymc_backend.py +71 -0
  105. sofic/inference/bayesian/stack_hmm.py +215 -0
  106. sofic/inference/model_selection.py +365 -0
  107. sofic/inference/spectral.py +564 -0
  108. sofic/operations.py +16 -0
  109. sofic/properties.py +339 -0
  110. sofic/serialization.py +450 -0
  111. sofic/shifts/__init__.py +48 -0
  112. sofic/shifts/algorithms.py +84 -0
  113. sofic/shifts/base.py +49 -0
  114. sofic/shifts/cover_construction.py +76 -0
  115. sofic/shifts/covers.py +47 -0
  116. sofic/shifts/dyck_algorithms.py +100 -0
  117. sofic/shifts/dyck_enumeration.py +275 -0
  118. sofic/shifts/markov_dyck.py +172 -0
  119. sofic/shifts/parry_construction.py +82 -0
  120. sofic/shifts/sft.py +104 -0
  121. sofic/shifts/sft_construction.py +52 -0
  122. sofic/shifts/sliding_block_code.py +156 -0
  123. sofic/shifts/sofic.py +111 -0
  124. sofic/shifts/sofic_dyck.py +110 -0
  125. sofic/shifts/sofic_relation.py +64 -0
  126. sofic/shifts/textile.py +104 -0
  127. sofic/shifts/tmc.py +46 -0
  128. sofic/shifts/tmc_construction.py +58 -0
  129. sofic/shifts/topological_anatomy.py +150 -0
  130. sofic/states.py +27 -0
  131. sofic/testing/__init__.py +8 -0
  132. sofic/testing/strategies.py +154 -0
  133. sofic/viz/__init__.py +16 -0
  134. sofic/viz/_context.py +345 -0
  135. sofic/viz/_edge.py +216 -0
  136. sofic/viz/_format.py +89 -0
  137. sofic/viz/_labels.py +34 -0
  138. sofic/viz/_names.py +17 -0
  139. sofic/viz/_rational.py +20 -0
  140. sofic/viz/_tikz_compile.py +177 -0
  141. sofic/viz/_tikz_format.py +122 -0
  142. sofic/viz/_tikz_layout.py +218 -0
  143. sofic/viz/assets/vaucanson.tikz +71 -0
  144. sofic/viz/graphviz.py +158 -0
  145. sofic/viz/idiagram.py +350 -0
  146. sofic/viz/tikz.py +381 -0
  147. sofic-0.1.0.dist-info/METADATA +444 -0
  148. sofic-0.1.0.dist-info/RECORD +150 -0
  149. sofic-0.1.0.dist-info/WHEEL +4 -0
  150. 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
+ )
@@ -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)