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/viz/tikz.py ADDED
@@ -0,0 +1,381 @@
1
+ """TikZ / Vaucanson rendering for sofic state-machine models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import tempfile
8
+ from collections import defaultdict
9
+ from collections.abc import Hashable, Mapping
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from sofic.base import StateMachine
14
+ from sofic.graph import Transition
15
+ from sofic.viz._context import VizContext, tikz_draw_color, viz_context
16
+ from sofic.viz._edge import (
17
+ PART_EMISSION,
18
+ PART_KIND,
19
+ PART_MATCH_TAG,
20
+ PART_MULTIPLICITY,
21
+ PART_OUTPUT,
22
+ PART_PROB,
23
+ PART_QUASIPROB,
24
+ PART_STACK,
25
+ PART_SYMBOL,
26
+ STYLE_DYCK,
27
+ STYLE_EDGE,
28
+ STYLE_PROB_ONLY,
29
+ STYLE_SYMBOL_ONLY,
30
+ STYLE_TMC,
31
+ STYLE_TRANSDUCER,
32
+ STYLE_VPA,
33
+ edge_spec,
34
+ part_value,
35
+ )
36
+ from sofic.viz._names import node_name
37
+ from sofic.viz._tikz_format import (
38
+ format_belief_tikz_node,
39
+ format_edge_latex,
40
+ format_prob_latex,
41
+ format_state_latex,
42
+ format_state_tikz_node,
43
+ format_symbol_latex,
44
+ format_symbol_only_latex,
45
+ format_transducer_edge_latex,
46
+ latex_escape,
47
+ )
48
+ from sofic.viz._tikz_layout import (
49
+ edge_style,
50
+ layout_circle,
51
+ layout_graphviz,
52
+ placement_to_xy,
53
+ plan_loop_styles,
54
+ )
55
+ from sofic.viz.graphviz import _model_for_viz
56
+
57
+
58
+ def _tikz_state_label(context: VizContext, state: Hashable) -> str:
59
+ from sofic.generators.mixed_state import MixedState, pure_state_index
60
+
61
+ if isinstance(state, MixedState):
62
+ if pure_state_index(state) is not None:
63
+ label = context.state_labels.get(state, format_state_latex(state))
64
+ return latex_escape(label)
65
+ return format_belief_tikz_node(state.belief)
66
+ return format_state_tikz_node(state)
67
+
68
+
69
+ def _format_dyck_match_tag_latex(tag: str) -> str:
70
+ if tag.startswith("m") and tag[1:].isdigit():
71
+ return rf"m_{{{tag[1:]}}}"
72
+ return latex_escape(tag)
73
+
74
+
75
+ def _tikz_edge_label(model: StateMachine, transition: Transition) -> str:
76
+ spec = edge_spec(model, transition)
77
+ style = spec.style
78
+
79
+ if style == STYLE_SYMBOL_ONLY:
80
+ symbol = part_value(spec, PART_SYMBOL)
81
+ return format_symbol_only_latex(symbol) if symbol is not None else ""
82
+
83
+ if style == STYLE_TRANSDUCER:
84
+ symbol = part_value(spec, PART_SYMBOL)
85
+ output = part_value(spec, PART_OUTPUT)
86
+ if symbol is None and output is None:
87
+ return ""
88
+ return format_transducer_edge_latex(symbol, output)
89
+
90
+ if style == STYLE_PROB_ONLY:
91
+ prob = part_value(spec, PART_PROB)
92
+ return rf"${format_prob_latex(prob)}$" if prob is not None else ""
93
+
94
+ if style == STYLE_EDGE:
95
+ label_symbol = part_value(spec, PART_EMISSION, PART_SYMBOL)
96
+ value = part_value(spec, PART_PROB, PART_QUASIPROB)
97
+ if label_symbol is not None and value is not None:
98
+ return format_edge_latex(label_symbol, value)
99
+ if label_symbol is not None:
100
+ return format_symbol_only_latex(label_symbol)
101
+ if value is not None:
102
+ return rf"${format_prob_latex(value)}$"
103
+ return ""
104
+
105
+ if style == STYLE_VPA:
106
+ parts: list[str] = []
107
+ for part in spec.parts:
108
+ if part.kind == PART_SYMBOL:
109
+ parts.append(format_symbol_latex(part.value))
110
+ elif part.kind == PART_KIND:
111
+ parts.append(latex_escape(str(part.value)))
112
+ elif part.kind == PART_STACK:
113
+ parts.append(rf"\uparrow{format_symbol_latex(part.value)}")
114
+ return "$" + r"\mid".join(parts) + "$" if parts else ""
115
+
116
+ if style == STYLE_DYCK:
117
+ parts = []
118
+ for part in spec.parts:
119
+ if part.kind == PART_SYMBOL:
120
+ parts.append(rf"\Symbol{{{format_symbol_latex(part.value)}}}")
121
+ elif part.kind == PART_KIND:
122
+ parts.append(rf"\mathrm{{{latex_escape(str(part.value))}}}")
123
+ elif part.kind == PART_MATCH_TAG:
124
+ parts.append(_format_dyck_match_tag_latex(part.value))
125
+ return "$" + r"\mid ".join(parts) + "$" if parts else ""
126
+
127
+ if style == STYLE_TMC:
128
+ parts = []
129
+ for part in spec.parts:
130
+ if part.kind == PART_SYMBOL:
131
+ parts.append(format_symbol_latex(part.value))
132
+ elif part.kind == PART_MULTIPLICITY:
133
+ parts.append(latex_escape(f"\\times {part.value}"))
134
+ return "$" + r"\mid".join(parts) + "$" if parts else ""
135
+
136
+ # STYLE_FALLBACK
137
+ label_symbol = part_value(spec, PART_EMISSION, PART_SYMBOL)
138
+ prob = part_value(spec, PART_PROB)
139
+ if label_symbol is not None and prob is not None:
140
+ return format_edge_latex(label_symbol, prob)
141
+ if label_symbol is not None:
142
+ return format_symbol_only_latex(label_symbol)
143
+ if prob is not None:
144
+ return rf"${format_prob_latex(prob)}$"
145
+ return ""
146
+
147
+
148
+ def _tikz_display_kwargs(model: StateMachine) -> dict[str, Any]:
149
+ """Kwargs for notebook / default TikZ rendering."""
150
+ from sofic.generators.bidirectional_epsilon_machine import BidirectionalEpsilonMachine
151
+
152
+ if isinstance(model, BidirectionalEpsilonMachine):
153
+ return {"style": "auto", "layout": "graphviz"}
154
+ return {"style": "auto"}
155
+
156
+
157
+ def model_to_tikz(
158
+ model: StateMachine,
159
+ *,
160
+ fragment: bool = True,
161
+ tikzset_filename: str | None = None,
162
+ style: str = "auto",
163
+ layout: str = "circle",
164
+ radius: str = "2cm",
165
+ bend_angle: int | float = 15,
166
+ scale: float = 1,
167
+ positions: Mapping[Hashable, tuple[float, float]] | None = None,
168
+ angles: Mapping[Hashable, float] | None = None,
169
+ rankdir: str | None = None,
170
+ label: str | None = None,
171
+ edge_label_pos: float = 0.5,
172
+ color_by_emission: bool = True,
173
+ ) -> str:
174
+ """Return a Vaucanson-style TikZ picture for ``model``.
175
+
176
+ Args:
177
+ edge_label_pos: Fraction along each edge (0 = source, 1 = target) at
178
+ which to place the edge label. Use ``1/3`` to keep labels clear of
179
+ mid-edge crossings.
180
+ color_by_emission: Colour edges by emission (or input/label symbol).
181
+ Defaults to ``True``. Visibly pushdown / Dyck kind colours take
182
+ precedence.
183
+ """
184
+ model = _model_for_viz(model)
185
+ context = viz_context(model, style=style, color_by_emission=color_by_emission)
186
+
187
+ if layout == "circle":
188
+ coords = layout_circle(model, radius=radius, positions=positions, angles=angles)
189
+ elif layout == "graphviz":
190
+ coords = layout_graphviz(model, style=style, rankdir=rankdir)
191
+ else:
192
+ raise ValueError(f"unknown layout {layout!r}; expected 'circle' or 'graphviz'")
193
+
194
+ lines: list[str] = []
195
+ if tikzset_filename:
196
+ lines.append(rf"\tikzsetnextfilename{{{latex_escape(tikzset_filename)}}}")
197
+
198
+ picture_options = [
199
+ "style=vaucanson",
200
+ f"bend angle={bend_angle}",
201
+ f"scale={scale}",
202
+ "every node/.style={transform shape}",
203
+ ]
204
+ lines.append(r"\begin{tikzpicture}[" + ",\n ".join(picture_options) + "]")
205
+
206
+ from sofic.generators.bidirectional_epsilon_machine import BidirectionalEpsilonMachine
207
+ from sofic.generators.mixed_state import MixedState, pure_state_index
208
+
209
+ for state in sorted(model.states(), key=repr):
210
+ node = node_name(state)
211
+ placement = coords[state]
212
+ node_label = _tikz_state_label(context, state)
213
+ state_opts = ["state"]
214
+ if (
215
+ isinstance(model, BidirectionalEpsilonMachine)
216
+ or isinstance(state, MixedState)
217
+ and pure_state_index(state) is None
218
+ ):
219
+ state_opts.append(r"font=\footnotesize")
220
+ if state in context.accepting_states:
221
+ state_opts.append("accepting")
222
+ fillcolor = context.node_fillcolor(state)
223
+ if fillcolor is not None:
224
+ state_opts.append(f"fill={fillcolor}")
225
+ if context.highlight_initial_states and state in context.initial_states:
226
+ if context.show_start_node:
227
+ state_opts.append("initial")
228
+ else:
229
+ # Match Graphviz penwidth=2.5 (vs 1.0) with Vaucanson's line width=2 default.
230
+ state_opts.append("line width=5pt")
231
+ lines.append(f" \\node [{', '.join(state_opts)}] ({node}) {placement} {{{node_label}}};")
232
+
233
+ transitions = list(model.transitions())
234
+ grouped: dict[tuple[Hashable, Hashable], list[Transition]] = defaultdict(list)
235
+ for transition in transitions:
236
+ key = (transition.source, transition.target)
237
+ grouped[key].append(transition)
238
+
239
+ position_xy = {state: placement_to_xy(coords[state]) for state in coords}
240
+ loop_styles = plan_loop_styles(position_xy, grouped)
241
+
242
+ edge_lines: list[str] = []
243
+ for key in sorted(grouped, key=lambda item: (repr(item[0]), repr(item[1]))):
244
+ source, target = key
245
+ group = grouped[key]
246
+ has_reverse = (target, source) in grouped
247
+ for index, transition in enumerate(group):
248
+ style_opts = edge_style(
249
+ source,
250
+ target,
251
+ parallel_index=index,
252
+ total_parallel=len(group),
253
+ has_reverse=has_reverse,
254
+ loop_style=loop_styles.get((source, target, index)),
255
+ )
256
+ color = context.edge_color(transition)
257
+ if color:
258
+ draw = f"draw={tikz_draw_color(color)}"
259
+ style_opts = f"{style_opts}, {draw}" if style_opts else draw
260
+ edge_label = _tikz_edge_label(model, transition)
261
+ source_name = node_name(source)
262
+ target_name = node_name(target)
263
+ opts = f"[{style_opts}]" if style_opts else ""
264
+ if edge_label:
265
+ label_opts = [
266
+ f"pos={edge_label_pos:g}",
267
+ "fill=white",
268
+ "inner sep=1pt",
269
+ "font=\\scriptsize",
270
+ ]
271
+ # On reciprocal pairs, park labels on opposite sides of the bend.
272
+ if has_reverse and source != target:
273
+ label_opts.append("auto")
274
+ if source_name > target_name:
275
+ label_opts.append("swap")
276
+ label_part = f" node[{','.join(label_opts)}] {{{edge_label}}}"
277
+ else:
278
+ label_part = ""
279
+ edge_lines.append(f"({source_name}) edge {opts}{label_part} ({target_name})")
280
+
281
+ if edge_lines:
282
+ lines.append(" \\path " + "\n ".join(edge_lines) + ";")
283
+
284
+ lines.append(r"\end{tikzpicture}")
285
+ if label:
286
+ lines.append(rf"\label{{{latex_escape(label)}}}")
287
+
288
+ body = "\n".join(lines) + "\n"
289
+ if fragment:
290
+ return body
291
+ return _standalone_document(body)
292
+
293
+
294
+ def _standalone_document(body: str) -> str:
295
+ from sofic.viz._tikz_compile import compilation_document
296
+
297
+ return compilation_document(body)
298
+
299
+
300
+ def compile_tikz(
301
+ fragment: str,
302
+ *,
303
+ format: str = "png",
304
+ ) -> bytes:
305
+ """Compile a TikZ fragment to PDF, PNG, or SVG bytes."""
306
+ from sofic.viz._tikz_compile import compile_tikz_fragment
307
+
308
+ return compile_tikz_fragment(fragment, format=format)
309
+
310
+
311
+ def model_to_tikz_image(
312
+ model: StateMachine,
313
+ *,
314
+ format: str = "png",
315
+ **kwargs: Any,
316
+ ) -> bytes:
317
+ """Render ``model`` to compiled image bytes via ``pdflatex``."""
318
+ display_kwargs = {**_tikz_display_kwargs(model), **kwargs}
319
+ fragment = model_to_tikz(model, fragment=True, **display_kwargs)
320
+ return compile_tikz(fragment, format=format)
321
+
322
+
323
+ def draw_tikz(
324
+ model: StateMachine,
325
+ filename: str | None = None,
326
+ *,
327
+ format: str | None = None,
328
+ view: bool = False,
329
+ **kwargs: Any,
330
+ ) -> str | None:
331
+ """Write a TikZ figure for ``model``.
332
+
333
+ When ``format`` is ``png``, ``svg``, or ``pdf`` (or inferred from ``filename``),
334
+ compile with ``pdflatex`` and write the rendered image. Otherwise write a
335
+ ``.tikz`` LaTeX fragment.
336
+ """
337
+ import subprocess
338
+ import sys
339
+
340
+ inferred = None
341
+ if filename is not None:
342
+ inferred = Path(filename).suffix.lstrip(".").lower() or None
343
+ resolved_format = (format or inferred or "tikz").lower()
344
+
345
+ display_kwargs = {**_tikz_display_kwargs(model), **kwargs}
346
+ fragment = model_to_tikz(model, fragment=True, **display_kwargs)
347
+ if resolved_format == "tikz":
348
+ if filename is None:
349
+ return None
350
+ path = Path(filename)
351
+ path.parent.mkdir(parents=True, exist_ok=True)
352
+ path.write_text(fragment, encoding="utf-8")
353
+ if view:
354
+ subprocess.run(["open", str(path)], check=False)
355
+ return str(path)
356
+
357
+ image_bytes = compile_tikz(fragment, format=resolved_format)
358
+ if filename is None:
359
+ suffix = f".{resolved_format}"
360
+ fd, tmp_name = tempfile.mkstemp(suffix=suffix)
361
+ os.close(fd)
362
+ tmp = Path(tmp_name)
363
+ tmp.write_bytes(image_bytes)
364
+ if view:
365
+ if sys.platform == "darwin":
366
+ subprocess.run(["open", str(tmp)], check=False)
367
+ elif shutil.which("xdg-open"):
368
+ subprocess.run(["xdg-open", str(tmp)], check=False)
369
+ return str(tmp)
370
+
371
+ path = Path(filename)
372
+ if path.suffix == "":
373
+ path = path.with_suffix(f".{resolved_format}")
374
+ path.parent.mkdir(parents=True, exist_ok=True)
375
+ path.write_bytes(image_bytes)
376
+ if view:
377
+ if sys.platform == "darwin":
378
+ subprocess.run(["open", str(path)], check=False)
379
+ elif shutil.which("xdg-open"):
380
+ subprocess.run(["xdg-open", str(path)], check=False)
381
+ return str(path)