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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Fischer and Krieger cover constructions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from sofic.graph import ATTR_SYMBOL, TransitionGraph
|
|
9
|
+
from sofic.shifts.covers import LeftFischerCover, LeftKriegerCover, RightFischerCover, RightKriegerCover
|
|
10
|
+
from sofic.shifts.sofic import SoficShift
|
|
11
|
+
from sofic.states import sequential_labels
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _follower_language(shift: SoficShift, vertex: Any, max_len: int = 8) -> frozenset[tuple[Any, ...]]:
|
|
15
|
+
from collections import deque
|
|
16
|
+
|
|
17
|
+
seen: set[tuple[Any, ...]] = set()
|
|
18
|
+
queue: deque[tuple[Any, tuple[Any, ...]]] = deque([(vertex, ())])
|
|
19
|
+
while queue:
|
|
20
|
+
state, prefix = queue.popleft()
|
|
21
|
+
if len(prefix) > max_len:
|
|
22
|
+
continue
|
|
23
|
+
if prefix:
|
|
24
|
+
seen.add(prefix)
|
|
25
|
+
for transition in shift.graph.out_transitions(state):
|
|
26
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
27
|
+
if symbol is None:
|
|
28
|
+
continue
|
|
29
|
+
queue.append((transition.target, prefix + (symbol,)))
|
|
30
|
+
return frozenset(seen)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _build_left_fischer(shift: SoficShift) -> LeftFischerCover:
|
|
34
|
+
followers: dict[Any, frozenset[tuple[Any, ...]]] = {
|
|
35
|
+
vertex: _follower_language(shift, vertex) for vertex in shift.states()
|
|
36
|
+
}
|
|
37
|
+
classes: dict[frozenset[tuple[Any, ...]], list[Any]] = defaultdict(list)
|
|
38
|
+
for vertex, language in followers.items():
|
|
39
|
+
classes[language].append(vertex)
|
|
40
|
+
|
|
41
|
+
graph = TransitionGraph()
|
|
42
|
+
class_for_vertex = {vertex: language for vertex, language in followers.items()}
|
|
43
|
+
state_ids = {language: sequential_labels(len(classes))[index] for index, language in enumerate(classes)}
|
|
44
|
+
for _language, state_id in state_ids.items():
|
|
45
|
+
graph.add_state(state_id)
|
|
46
|
+
|
|
47
|
+
for transition in shift.transitions():
|
|
48
|
+
source_lang = class_for_vertex[transition.source]
|
|
49
|
+
target_lang = class_for_vertex[transition.target]
|
|
50
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
51
|
+
graph.add_transition(state_ids[source_lang], state_ids[target_lang], **{ATTR_SYMBOL: symbol})
|
|
52
|
+
|
|
53
|
+
return LeftFischerCover(
|
|
54
|
+
graph=graph,
|
|
55
|
+
symbol_alphabet=shift.symbol_alphabet,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def left_fischer_from_sofic(shift: SoficShift) -> LeftFischerCover:
|
|
60
|
+
return _build_left_fischer(shift.trim_transient())
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def right_fischer_from_sofic(shift: SoficShift) -> RightFischerCover:
|
|
64
|
+
left = left_fischer_from_sofic(shift.reverse())
|
|
65
|
+
return RightFischerCover(
|
|
66
|
+
graph=left.graph.copy(),
|
|
67
|
+
symbol_alphabet=left.symbol_alphabet,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def left_krieger_from_sofic(shift: SoficShift) -> LeftKriegerCover:
|
|
72
|
+
raise NotImplementedError("Left Krieger cover construction is not yet implemented; use left_fischer_from_sofic")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def right_krieger_from_sofic(shift: SoficShift) -> RightKriegerCover:
|
|
76
|
+
raise NotImplementedError("Right Krieger cover construction is not yet implemented; use right_fischer_from_sofic")
|
sofic/shifts/covers.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Fischer and Krieger covers of sofic shifts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sofic.shifts.sofic import SoficShift
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LeftFischerCover(SoficShift):
|
|
11
|
+
"""Left Fischer cover presentation."""
|
|
12
|
+
|
|
13
|
+
@classmethod
|
|
14
|
+
def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> LeftFischerCover:
|
|
15
|
+
from sofic.shifts.cover_construction import left_fischer_from_sofic
|
|
16
|
+
|
|
17
|
+
return left_fischer_from_sofic(shift)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RightFischerCover(SoficShift):
|
|
21
|
+
"""Right Fischer cover presentation."""
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> RightFischerCover:
|
|
25
|
+
from sofic.shifts.cover_construction import right_fischer_from_sofic
|
|
26
|
+
|
|
27
|
+
return right_fischer_from_sofic(shift)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class LeftKriegerCover(SoficShift):
|
|
31
|
+
"""Left Krieger cover presentation."""
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> LeftKriegerCover:
|
|
35
|
+
from sofic.shifts.cover_construction import left_krieger_from_sofic
|
|
36
|
+
|
|
37
|
+
return left_krieger_from_sofic(shift)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RightKriegerCover(SoficShift):
|
|
41
|
+
"""Right Krieger cover presentation."""
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> RightKriegerCover:
|
|
45
|
+
from sofic.shifts.cover_construction import right_krieger_from_sofic
|
|
46
|
+
|
|
47
|
+
return right_krieger_from_sofic(shift)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Algorithms for Dyck shift presentations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from collections.abc import Hashable, Iterator, Sequence
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from sofic.graph import ATTR_KIND, ATTR_SYMBOL, KIND_CALL, KIND_INTERNAL, KIND_RETURN, Transition
|
|
10
|
+
from sofic.shifts.sofic_dyck import TransitionRef, transition_ref
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from sofic.shifts.sofic_dyck import SoficDyckShift
|
|
14
|
+
|
|
15
|
+
Configuration = tuple[Hashable, tuple[TransitionRef, ...]]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_admissible_word(shift: SoficDyckShift, word: Sequence[Any]) -> bool:
|
|
19
|
+
"""Return whether ``word`` labels an admissible finite Dyck path."""
|
|
20
|
+
word = tuple(word)
|
|
21
|
+
if not word:
|
|
22
|
+
return True
|
|
23
|
+
if any(symbol not in shift.symbol_alphabet for symbol in word):
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
current: set[Configuration] = {(state, ()) for state in shift.states()}
|
|
27
|
+
for symbol in word:
|
|
28
|
+
next_configs: set[Configuration] = set()
|
|
29
|
+
for state, stack in current:
|
|
30
|
+
for transition in shift.graph.out_transitions(state):
|
|
31
|
+
if transition.data.get(ATTR_SYMBOL) != symbol:
|
|
32
|
+
continue
|
|
33
|
+
_advance_transition(shift, transition, stack, next_configs)
|
|
34
|
+
if not next_configs:
|
|
35
|
+
return False
|
|
36
|
+
current = next_configs
|
|
37
|
+
|
|
38
|
+
return bool(current)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def admissible_words(shift: SoficDyckShift, length: int) -> Iterator[tuple[Any, ...]]:
|
|
42
|
+
"""Yield distinct admissible words of exactly ``length`` symbols."""
|
|
43
|
+
if length <= 0:
|
|
44
|
+
yield ()
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
seen_words: set[tuple[Any, ...]] = set()
|
|
48
|
+
seen_configs: set[tuple[Hashable, tuple[Any, ...], tuple[TransitionRef, ...]]] = set()
|
|
49
|
+
queue: deque[tuple[Hashable, tuple[Any, ...], tuple[TransitionRef, ...]]] = deque()
|
|
50
|
+
for state in shift.states():
|
|
51
|
+
config = (state, (), ())
|
|
52
|
+
queue.append(config)
|
|
53
|
+
seen_configs.add(config)
|
|
54
|
+
|
|
55
|
+
while queue:
|
|
56
|
+
state, prefix, stack = queue.popleft()
|
|
57
|
+
if len(prefix) == length:
|
|
58
|
+
if prefix not in seen_words:
|
|
59
|
+
seen_words.add(prefix)
|
|
60
|
+
yield prefix
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
for transition in shift.graph.out_transitions(state):
|
|
64
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
65
|
+
if symbol is None:
|
|
66
|
+
continue
|
|
67
|
+
for target, next_stack in _successors(shift, transition, stack):
|
|
68
|
+
config = (target, prefix + (symbol,), next_stack)
|
|
69
|
+
if config in seen_configs:
|
|
70
|
+
continue
|
|
71
|
+
seen_configs.add(config)
|
|
72
|
+
queue.append(config)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _advance_transition(
|
|
76
|
+
shift: SoficDyckShift,
|
|
77
|
+
transition: Transition,
|
|
78
|
+
stack: tuple[TransitionRef, ...],
|
|
79
|
+
next_configs: set[Configuration],
|
|
80
|
+
) -> None:
|
|
81
|
+
for target, next_stack in _successors(shift, transition, stack):
|
|
82
|
+
next_configs.add((target, next_stack))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _successors(
|
|
86
|
+
shift: SoficDyckShift,
|
|
87
|
+
transition: Transition,
|
|
88
|
+
stack: tuple[TransitionRef, ...],
|
|
89
|
+
) -> Iterator[tuple[Hashable, tuple[TransitionRef, ...]]]:
|
|
90
|
+
kind = transition.data.get(ATTR_KIND)
|
|
91
|
+
ref = transition_ref(transition)
|
|
92
|
+
if kind == KIND_CALL:
|
|
93
|
+
yield transition.target, stack + (ref,)
|
|
94
|
+
elif kind == KIND_RETURN:
|
|
95
|
+
if not stack:
|
|
96
|
+
yield transition.target, stack
|
|
97
|
+
elif (stack[-1], ref) in shift.matched_edges:
|
|
98
|
+
yield transition.target, stack[:-1]
|
|
99
|
+
elif kind == KIND_INTERNAL:
|
|
100
|
+
yield transition.target, stack
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Canonical strings and enumeration for small Sofic-Dyck topologies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from collections.abc import Iterator, Sequence
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from itertools import product
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from sofic.exceptions import SoficValidationError
|
|
12
|
+
from sofic.graph import ATTR_KIND, ATTR_SYMBOL, KIND_CALL, KIND_INTERNAL, KIND_RETURN
|
|
13
|
+
from sofic.shifts.dyck_algorithms import is_admissible_word
|
|
14
|
+
from sofic.shifts.sofic_dyck import SoficDyckShift, TransitionRef, transition_ref
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"DyckGraphString",
|
|
18
|
+
"count_dyck_graph_strings",
|
|
19
|
+
"dyck_graph_string_to_shift",
|
|
20
|
+
"iter_dyck_graph_strings",
|
|
21
|
+
"iter_sofic_dyck_topologies",
|
|
22
|
+
"shift_to_dyck_graph_string",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
_KIND_CALL = 0
|
|
26
|
+
_KIND_RETURN = 1
|
|
27
|
+
_KIND_INTERNAL = 2
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class DyckEnumerationError(SoficValidationError):
|
|
31
|
+
"""Raised when Dyck-graph enumeration fails."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class DyckGraphString:
|
|
36
|
+
"""Canonical encoding of a small matched-edge Dyck graph.
|
|
37
|
+
|
|
38
|
+
``transitions`` lists ``(source, kind, symbol_index, target)`` in
|
|
39
|
+
lexicographic order. ``matched_pairs`` lists ``(call_transition_index,
|
|
40
|
+
return_transition_index)`` into ``transitions``.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
n: int
|
|
44
|
+
call_symbols: tuple[Any, ...]
|
|
45
|
+
return_symbols: tuple[Any, ...]
|
|
46
|
+
internal_symbols: tuple[Any, ...]
|
|
47
|
+
transitions: tuple[tuple[int, int, int, int], ...]
|
|
48
|
+
matched_pairs: tuple[tuple[int, int], ...]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def shift_to_dyck_graph_string(shift: SoficDyckShift) -> DyckGraphString:
|
|
52
|
+
"""Serialize a ``SoficDyckShift`` into canonical edge-index form."""
|
|
53
|
+
states = sorted(shift.states(), key=repr)
|
|
54
|
+
state_index = {state: index for index, state in enumerate(states)}
|
|
55
|
+
call_symbols = tuple(sorted(shift.call_alphabet, key=repr))
|
|
56
|
+
return_symbols = tuple(sorted(shift.return_alphabet, key=repr))
|
|
57
|
+
internal_symbols = tuple(sorted(shift.internal_alphabet, key=repr))
|
|
58
|
+
call_index = {symbol: index for index, symbol in enumerate(call_symbols)}
|
|
59
|
+
return_index = {symbol: index for index, symbol in enumerate(return_symbols)}
|
|
60
|
+
internal_index = {symbol: index for index, symbol in enumerate(internal_symbols)}
|
|
61
|
+
|
|
62
|
+
encoded: list[tuple[int, int, int, int]] = []
|
|
63
|
+
call_positions: dict[TransitionRef, int] = {}
|
|
64
|
+
return_positions: dict[TransitionRef, int] = {}
|
|
65
|
+
|
|
66
|
+
for transition in sorted(shift.transitions(), key=lambda item: (repr(item.source), repr(item.target), item.key)):
|
|
67
|
+
kind = transition.data.get(ATTR_KIND)
|
|
68
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
69
|
+
if kind == KIND_CALL:
|
|
70
|
+
kind_code = _KIND_CALL
|
|
71
|
+
symbol_index = call_index[symbol]
|
|
72
|
+
elif kind == KIND_RETURN:
|
|
73
|
+
kind_code = _KIND_RETURN
|
|
74
|
+
symbol_index = return_index[symbol]
|
|
75
|
+
elif kind == KIND_INTERNAL:
|
|
76
|
+
kind_code = _KIND_INTERNAL
|
|
77
|
+
symbol_index = internal_index[symbol]
|
|
78
|
+
else:
|
|
79
|
+
raise DyckEnumerationError(f"unknown edge kind {kind!r}")
|
|
80
|
+
encoded.append((state_index[transition.source], kind_code, symbol_index, state_index[transition.target]))
|
|
81
|
+
ref = transition_ref(transition)
|
|
82
|
+
position = len(encoded) - 1
|
|
83
|
+
if kind == KIND_CALL:
|
|
84
|
+
call_positions[ref] = position
|
|
85
|
+
elif kind == KIND_RETURN:
|
|
86
|
+
return_positions[ref] = position
|
|
87
|
+
|
|
88
|
+
matched = tuple(
|
|
89
|
+
sorted((call_positions[call_ref], return_positions[return_ref]) for call_ref, return_ref in shift.matched_edges)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
return DyckGraphString(
|
|
93
|
+
n=len(states),
|
|
94
|
+
call_symbols=call_symbols,
|
|
95
|
+
return_symbols=return_symbols,
|
|
96
|
+
internal_symbols=internal_symbols,
|
|
97
|
+
transitions=tuple(encoded),
|
|
98
|
+
matched_pairs=tuple(sorted(matched)),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def dyck_graph_string_to_shift(spec: DyckGraphString) -> SoficDyckShift:
|
|
103
|
+
"""Build a ``SoficDyckShift`` from a ``DyckGraphString``."""
|
|
104
|
+
shift = SoficDyckShift(
|
|
105
|
+
call_alphabet=frozenset(spec.call_symbols),
|
|
106
|
+
return_alphabet=frozenset(spec.return_symbols),
|
|
107
|
+
internal_alphabet=frozenset(spec.internal_symbols),
|
|
108
|
+
)
|
|
109
|
+
states = [f"q{index}" for index in range(spec.n)]
|
|
110
|
+
for state in states:
|
|
111
|
+
shift.graph.add_state(state)
|
|
112
|
+
|
|
113
|
+
edge_refs: list[TransitionRef] = []
|
|
114
|
+
for source, kind_code, symbol_index, target in spec.transitions:
|
|
115
|
+
source_state = states[source]
|
|
116
|
+
target_state = states[target]
|
|
117
|
+
if kind_code == _KIND_CALL:
|
|
118
|
+
symbol = spec.call_symbols[symbol_index]
|
|
119
|
+
edge_refs.append(shift.add_call_transition(source_state, target_state, symbol))
|
|
120
|
+
elif kind_code == _KIND_RETURN:
|
|
121
|
+
symbol = spec.return_symbols[symbol_index]
|
|
122
|
+
edge_refs.append(shift.add_return_transition(source_state, target_state, symbol))
|
|
123
|
+
else:
|
|
124
|
+
symbol = spec.internal_symbols[symbol_index]
|
|
125
|
+
edge_refs.append(shift.add_internal_transition(source_state, target_state, symbol))
|
|
126
|
+
|
|
127
|
+
for call_index, return_index in spec.matched_pairs:
|
|
128
|
+
shift.add_matched_pair(edge_refs[call_index], edge_refs[return_index])
|
|
129
|
+
|
|
130
|
+
shift.validate()
|
|
131
|
+
return shift
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _one_state_transition_choices(
|
|
135
|
+
symbols_per_kind: Sequence[int],
|
|
136
|
+
*,
|
|
137
|
+
include_empty: bool,
|
|
138
|
+
) -> Iterator[tuple[tuple[int, int, int, int], ...]]:
|
|
139
|
+
"""Enumerate transition subsets on a single state looping to itself."""
|
|
140
|
+
slots: list[tuple[int, int, int, int]] = []
|
|
141
|
+
for kind_code, symbol_count in enumerate(symbols_per_kind):
|
|
142
|
+
for symbol_index in range(symbol_count):
|
|
143
|
+
slots.append((0, kind_code, symbol_index, 0))
|
|
144
|
+
if not slots:
|
|
145
|
+
if include_empty:
|
|
146
|
+
yield ()
|
|
147
|
+
return
|
|
148
|
+
for mask in range(1 if not include_empty else 0, 1 << len(slots)):
|
|
149
|
+
selected = tuple(slots[index] for index in range(len(slots)) if mask & (1 << index))
|
|
150
|
+
if selected or include_empty:
|
|
151
|
+
yield selected
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def iter_dyck_graph_strings(
|
|
155
|
+
*,
|
|
156
|
+
n: int = 1,
|
|
157
|
+
call_symbols: Sequence[Any] = ("a",),
|
|
158
|
+
return_symbols: Sequence[Any] = ("A",),
|
|
159
|
+
internal_symbols: Sequence[Any] = (),
|
|
160
|
+
include_empty: bool = False,
|
|
161
|
+
) -> Iterator[DyckGraphString]:
|
|
162
|
+
"""Yield canonical Dyck-graph strings for small topologies."""
|
|
163
|
+
if n != 1:
|
|
164
|
+
raise DyckEnumerationError("iter_dyck_graph_strings currently supports n=1 only")
|
|
165
|
+
calls = tuple(call_symbols)
|
|
166
|
+
returns = tuple(return_symbols)
|
|
167
|
+
internals = tuple(internal_symbols)
|
|
168
|
+
if len(calls) != len(returns):
|
|
169
|
+
raise DyckEnumerationError("call and return symbol counts must match")
|
|
170
|
+
|
|
171
|
+
for transitions in _one_state_transition_choices(
|
|
172
|
+
(len(calls), len(returns), len(internals)),
|
|
173
|
+
include_empty=include_empty,
|
|
174
|
+
):
|
|
175
|
+
call_edge_indices = [index for index, item in enumerate(transitions) if item[1] == _KIND_CALL]
|
|
176
|
+
return_edge_indices = [index for index, item in enumerate(transitions) if item[1] == _KIND_RETURN]
|
|
177
|
+
call_by_symbol: dict[int, list[int]] = defaultdict(list)
|
|
178
|
+
return_by_symbol: dict[int, list[int]] = defaultdict(list)
|
|
179
|
+
for edge_index in call_edge_indices:
|
|
180
|
+
call_by_symbol[transitions[edge_index][2]].append(edge_index)
|
|
181
|
+
for edge_index in return_edge_indices:
|
|
182
|
+
return_by_symbol[transitions[edge_index][2]].append(edge_index)
|
|
183
|
+
|
|
184
|
+
matched_options: list[list[tuple[int, int]]] = []
|
|
185
|
+
for symbol_index in call_by_symbol:
|
|
186
|
+
if symbol_index not in return_by_symbol:
|
|
187
|
+
matched_options = []
|
|
188
|
+
break
|
|
189
|
+
pairs = [
|
|
190
|
+
(call_index, return_index)
|
|
191
|
+
for call_index, return_index in zip(
|
|
192
|
+
call_by_symbol[symbol_index],
|
|
193
|
+
return_by_symbol[symbol_index],
|
|
194
|
+
strict=False,
|
|
195
|
+
)
|
|
196
|
+
]
|
|
197
|
+
if not pairs:
|
|
198
|
+
matched_options = []
|
|
199
|
+
break
|
|
200
|
+
matched_options.append(pairs)
|
|
201
|
+
if call_by_symbol and not matched_options:
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
if not matched_options:
|
|
205
|
+
yield DyckGraphString(
|
|
206
|
+
n=1,
|
|
207
|
+
call_symbols=calls,
|
|
208
|
+
return_symbols=returns,
|
|
209
|
+
internal_symbols=internals,
|
|
210
|
+
transitions=transitions,
|
|
211
|
+
matched_pairs=(),
|
|
212
|
+
)
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
for combo in product(*matched_options):
|
|
216
|
+
pairs: list[tuple[int, int]] = []
|
|
217
|
+
for item in combo:
|
|
218
|
+
if isinstance(item, tuple) and len(item) == 2:
|
|
219
|
+
pairs.append(item)
|
|
220
|
+
matched = tuple(sorted(set(pairs)))
|
|
221
|
+
yield DyckGraphString(
|
|
222
|
+
n=1,
|
|
223
|
+
call_symbols=calls,
|
|
224
|
+
return_symbols=returns,
|
|
225
|
+
internal_symbols=internals,
|
|
226
|
+
transitions=transitions,
|
|
227
|
+
matched_pairs=matched,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def iter_sofic_dyck_topologies(
|
|
232
|
+
*,
|
|
233
|
+
n: int = 1,
|
|
234
|
+
call_symbols: Sequence[Any] = ("a",),
|
|
235
|
+
return_symbols: Sequence[Any] = ("A",),
|
|
236
|
+
internal_symbols: Sequence[Any] = (),
|
|
237
|
+
) -> Iterator[SoficDyckShift]:
|
|
238
|
+
"""Yield valid ``SoficDyckShift`` topologies from canonical Dyck-graph strings."""
|
|
239
|
+
seen: set[tuple[Any, ...]] = set()
|
|
240
|
+
for spec in iter_dyck_graph_strings(
|
|
241
|
+
n=n,
|
|
242
|
+
call_symbols=call_symbols,
|
|
243
|
+
return_symbols=return_symbols,
|
|
244
|
+
internal_symbols=internal_symbols,
|
|
245
|
+
):
|
|
246
|
+
try:
|
|
247
|
+
shift = dyck_graph_string_to_shift(spec)
|
|
248
|
+
except (SoficValidationError, ValueError):
|
|
249
|
+
continue
|
|
250
|
+
if not is_admissible_word(shift, ()):
|
|
251
|
+
continue
|
|
252
|
+
key = (spec.transitions, spec.matched_pairs)
|
|
253
|
+
if key in seen:
|
|
254
|
+
continue
|
|
255
|
+
seen.add(key)
|
|
256
|
+
yield shift
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def count_dyck_graph_strings(
|
|
260
|
+
*,
|
|
261
|
+
n: int = 1,
|
|
262
|
+
call_symbols: Sequence[Any] = ("a",),
|
|
263
|
+
return_symbols: Sequence[Any] = ("A",),
|
|
264
|
+
internal_symbols: Sequence[Any] = (),
|
|
265
|
+
) -> int:
|
|
266
|
+
"""Count canonical Dyck-graph strings for the given signature."""
|
|
267
|
+
return sum(
|
|
268
|
+
1
|
|
269
|
+
for _spec in iter_dyck_graph_strings(
|
|
270
|
+
n=n,
|
|
271
|
+
call_symbols=call_symbols,
|
|
272
|
+
return_symbols=return_symbols,
|
|
273
|
+
internal_symbols=internal_symbols,
|
|
274
|
+
)
|
|
275
|
+
)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Markov-Dyck shift constructions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable, Sequence
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from sofic.graph import ATTR_KIND, ATTR_SYMBOL, KIND_CALL, KIND_RETURN, TransitionGraph
|
|
11
|
+
from sofic.shifts.sofic_dyck import MatchedEdge, SoficDyckShift, TransitionRef, transition_ref
|
|
12
|
+
|
|
13
|
+
GraphKind = Literal["edge", "vertex"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MarkovDyckShift(SoficDyckShift):
|
|
17
|
+
"""Markov-Dyck shift associated with a finite directed graph or matrix."""
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_adjacency(
|
|
21
|
+
cls,
|
|
22
|
+
matrix: np.ndarray,
|
|
23
|
+
labels: Sequence[Hashable] | None = None,
|
|
24
|
+
**kwargs: Any,
|
|
25
|
+
) -> MarkovDyckShift:
|
|
26
|
+
"""Build the Markov-Dyck shift from a square adjacency matrix."""
|
|
27
|
+
arr = _as_square_binary_matrix(matrix)
|
|
28
|
+
resolved_labels = _resolve_labels(arr.shape[0], labels)
|
|
29
|
+
return _from_binary_matrix(cls, arr, resolved_labels, **kwargs)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_graph(
|
|
33
|
+
cls,
|
|
34
|
+
graph: Any,
|
|
35
|
+
*,
|
|
36
|
+
kind: GraphKind = "edge",
|
|
37
|
+
**kwargs: Any,
|
|
38
|
+
) -> MarkovDyckShift:
|
|
39
|
+
"""Build an edge- or vertex-type Markov-Dyck shift from ``graph``."""
|
|
40
|
+
if kind == "edge":
|
|
41
|
+
matrix, labels = _edge_adjacency(graph)
|
|
42
|
+
elif kind == "vertex":
|
|
43
|
+
matrix, labels = _vertex_adjacency(graph)
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError("kind must be 'edge' or 'vertex'")
|
|
46
|
+
return _from_binary_matrix(cls, matrix, labels, **kwargs)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _from_binary_matrix(
|
|
50
|
+
cls: type[MarkovDyckShift],
|
|
51
|
+
matrix: np.ndarray,
|
|
52
|
+
labels: tuple[Hashable, ...],
|
|
53
|
+
**kwargs: Any,
|
|
54
|
+
) -> MarkovDyckShift:
|
|
55
|
+
graph = TransitionGraph()
|
|
56
|
+
call_symbols = tuple((KIND_CALL, label) for label in labels)
|
|
57
|
+
return_symbols = tuple((KIND_RETURN, label) for label in labels)
|
|
58
|
+
|
|
59
|
+
for symbol in (*call_symbols, *return_symbols):
|
|
60
|
+
graph.add_state(symbol)
|
|
61
|
+
|
|
62
|
+
row_intersections = _row_intersections(matrix)
|
|
63
|
+
n = matrix.shape[0]
|
|
64
|
+
for previous_index in range(n):
|
|
65
|
+
for next_index in range(n):
|
|
66
|
+
previous_call = call_symbols[previous_index]
|
|
67
|
+
previous_return = return_symbols[previous_index]
|
|
68
|
+
next_call = call_symbols[next_index]
|
|
69
|
+
next_return = return_symbols[next_index]
|
|
70
|
+
|
|
71
|
+
if matrix[next_index, previous_index]:
|
|
72
|
+
graph.add_transition(previous_call, next_call, **{ATTR_KIND: KIND_CALL, ATTR_SYMBOL: next_call})
|
|
73
|
+
if previous_index == next_index:
|
|
74
|
+
graph.add_transition(previous_call, next_return, **{ATTR_KIND: KIND_RETURN, ATTR_SYMBOL: next_return})
|
|
75
|
+
if row_intersections[next_index, previous_index]:
|
|
76
|
+
graph.add_transition(previous_return, next_call, **{ATTR_KIND: KIND_CALL, ATTR_SYMBOL: next_call})
|
|
77
|
+
if matrix[previous_index, next_index]:
|
|
78
|
+
graph.add_transition(previous_return, next_return, **{ATTR_KIND: KIND_RETURN, ATTR_SYMBOL: next_return})
|
|
79
|
+
|
|
80
|
+
matched_edges = _matched_edges_by_label(graph, labels)
|
|
81
|
+
return cls(
|
|
82
|
+
graph=graph,
|
|
83
|
+
call_alphabet=frozenset(call_symbols),
|
|
84
|
+
return_alphabet=frozenset(return_symbols),
|
|
85
|
+
matched_edges=matched_edges,
|
|
86
|
+
**kwargs,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _as_square_binary_matrix(matrix: np.ndarray) -> np.ndarray:
|
|
91
|
+
arr = np.asarray(matrix)
|
|
92
|
+
if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
|
|
93
|
+
raise ValueError("adjacency matrix must be square")
|
|
94
|
+
return (arr != 0).astype(bool)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _resolve_labels(n: int, labels: Sequence[Hashable] | None) -> tuple[Hashable, ...]:
|
|
98
|
+
if labels is None:
|
|
99
|
+
return tuple(range(n))
|
|
100
|
+
resolved = tuple(labels)
|
|
101
|
+
if len(resolved) != n:
|
|
102
|
+
raise ValueError("labels length must match adjacency matrix size")
|
|
103
|
+
if len(frozenset(resolved)) != n:
|
|
104
|
+
raise ValueError("labels must be distinct")
|
|
105
|
+
return resolved
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _row_intersections(matrix: np.ndarray) -> np.ndarray:
|
|
109
|
+
n = matrix.shape[0]
|
|
110
|
+
intersections = np.zeros((n, n), dtype=bool)
|
|
111
|
+
for i in range(n):
|
|
112
|
+
for j in range(n):
|
|
113
|
+
intersections[i, j] = bool(np.any(matrix[i] & matrix[j]))
|
|
114
|
+
return intersections
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _matched_edges_by_label(graph: TransitionGraph, labels: tuple[Hashable, ...]) -> frozenset[MatchedEdge]:
|
|
118
|
+
call_edges: dict[Hashable, set[TransitionRef]] = {label: set() for label in labels}
|
|
119
|
+
return_edges: dict[Hashable, set[TransitionRef]] = {label: set() for label in labels}
|
|
120
|
+
for transition in graph.transitions():
|
|
121
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
122
|
+
if not isinstance(symbol, tuple) or len(symbol) != 2:
|
|
123
|
+
continue
|
|
124
|
+
role, label = symbol
|
|
125
|
+
if role == KIND_CALL:
|
|
126
|
+
call_edges[label].add(transition_ref(transition))
|
|
127
|
+
elif role == KIND_RETURN:
|
|
128
|
+
return_edges[label].add(transition_ref(transition))
|
|
129
|
+
|
|
130
|
+
matched_edges: set[MatchedEdge] = set()
|
|
131
|
+
for label in labels:
|
|
132
|
+
for call_ref in call_edges[label]:
|
|
133
|
+
for return_ref in return_edges[label]:
|
|
134
|
+
matched_edges.add((call_ref, return_ref))
|
|
135
|
+
return frozenset(matched_edges)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _vertex_adjacency(graph: Any) -> tuple[np.ndarray, tuple[Hashable, ...]]:
|
|
139
|
+
if isinstance(graph, TransitionGraph):
|
|
140
|
+
labels = tuple(graph.states())
|
|
141
|
+
index = {label: i for i, label in enumerate(labels)}
|
|
142
|
+
matrix = np.zeros((len(labels), len(labels)), dtype=bool)
|
|
143
|
+
for transition in graph.transitions():
|
|
144
|
+
matrix[index[transition.target], index[transition.source]] = True
|
|
145
|
+
return matrix, labels
|
|
146
|
+
|
|
147
|
+
labels = tuple(graph.nodes())
|
|
148
|
+
index = {label: i for i, label in enumerate(labels)}
|
|
149
|
+
matrix = np.zeros((len(labels), len(labels)), dtype=bool)
|
|
150
|
+
for source, target in graph.edges():
|
|
151
|
+
matrix[index[target], index[source]] = True
|
|
152
|
+
return matrix, labels
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _edge_adjacency(graph: Any) -> tuple[np.ndarray, tuple[Hashable, ...]]:
|
|
156
|
+
endpoints: tuple[tuple[Hashable, Hashable], ...]
|
|
157
|
+
if isinstance(graph, TransitionGraph):
|
|
158
|
+
transitions = tuple(graph.transitions())
|
|
159
|
+
labels = tuple(transition_ref(transition) for transition in transitions)
|
|
160
|
+
endpoints = tuple((transition.source, transition.target) for transition in transitions)
|
|
161
|
+
elif graph.is_multigraph():
|
|
162
|
+
labels = tuple((source, target, key) for source, target, key in graph.edges(keys=True))
|
|
163
|
+
endpoints = tuple((source, target) for source, target, _key in labels)
|
|
164
|
+
else:
|
|
165
|
+
labels = tuple((source, target) for source, target in graph.edges())
|
|
166
|
+
endpoints = labels
|
|
167
|
+
|
|
168
|
+
matrix = np.zeros((len(labels), len(labels)), dtype=bool)
|
|
169
|
+
for previous, (_source_previous, target_previous) in enumerate(endpoints):
|
|
170
|
+
for next_, (source_next, _target_next) in enumerate(endpoints):
|
|
171
|
+
matrix[next_, previous] = target_previous == source_next
|
|
172
|
+
return matrix, labels
|