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,82 @@
|
|
|
1
|
+
"""Parry measure construction for topological Markov chains."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from sofic.generators.mealy import MealyHMM
|
|
8
|
+
from sofic.graph import ATTR_EMISSION, ATTR_MULTIPLICITY, ATTR_PROB
|
|
9
|
+
from sofic.shifts.algorithms import adjacency_matrix
|
|
10
|
+
from sofic.shifts.tmc import TopologicalMarkovChain
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _perron_pair(matrix: np.ndarray) -> tuple[float, np.ndarray, np.ndarray]:
|
|
14
|
+
"""Return (λ, left, right) Perron eigenvectors with left^T right = 1."""
|
|
15
|
+
eigenvalues, right_vecs = np.linalg.eig(matrix)
|
|
16
|
+
index = int(np.argmax(np.real(eigenvalues)))
|
|
17
|
+
lam = float(np.real(eigenvalues[index]))
|
|
18
|
+
right = np.real(right_vecs[:, index])
|
|
19
|
+
if right[np.argmax(np.abs(right))] < 0.0:
|
|
20
|
+
right = -right
|
|
21
|
+
right = np.maximum(right, 1e-300)
|
|
22
|
+
|
|
23
|
+
left_eigenvalues, left_vecs = np.linalg.eig(matrix.T)
|
|
24
|
+
left_index = int(np.argmax(np.real(left_eigenvalues)))
|
|
25
|
+
left = np.real(left_vecs[:, left_index])
|
|
26
|
+
if left[np.argmax(np.abs(left))] < 0.0:
|
|
27
|
+
left = -left
|
|
28
|
+
left = np.maximum(left, 1e-300)
|
|
29
|
+
|
|
30
|
+
scale = float(left @ right)
|
|
31
|
+
if scale < 0.0:
|
|
32
|
+
left = -left
|
|
33
|
+
scale = -scale
|
|
34
|
+
if scale <= 0.0:
|
|
35
|
+
raise ValueError("failed to normalize Perron eigenvectors")
|
|
36
|
+
left /= scale
|
|
37
|
+
return lam, left, right
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def parry_measure(tmc: TopologicalMarkovChain) -> MealyHMM:
|
|
41
|
+
matrix, states = adjacency_matrix(tmc)
|
|
42
|
+
if matrix.size == 0:
|
|
43
|
+
return MealyHMM(initial_distribution={}, observation_alphabet=frozenset())
|
|
44
|
+
|
|
45
|
+
lam, left, right = _perron_pair(matrix)
|
|
46
|
+
n = len(states)
|
|
47
|
+
transition = np.zeros((n, n), dtype=float)
|
|
48
|
+
for i in range(n):
|
|
49
|
+
for j in range(n):
|
|
50
|
+
if matrix[i, j] > 0.0:
|
|
51
|
+
transition[i, j] = matrix[i, j] * right[j] / (lam * right[i])
|
|
52
|
+
|
|
53
|
+
stationary = left * right
|
|
54
|
+
stationary /= stationary.sum()
|
|
55
|
+
|
|
56
|
+
hmm = MealyHMM(
|
|
57
|
+
initial_distribution={states[i]: float(stationary[i]) for i in range(n)},
|
|
58
|
+
observation_alphabet=tmc.symbol_alphabet,
|
|
59
|
+
)
|
|
60
|
+
for state in states:
|
|
61
|
+
hmm.graph.add_state(state)
|
|
62
|
+
|
|
63
|
+
for transition_edge in tmc.transitions():
|
|
64
|
+
i = states.index(transition_edge.source)
|
|
65
|
+
j = states.index(transition_edge.target)
|
|
66
|
+
multiplicity = float(transition_edge.data.get(ATTR_MULTIPLICITY, 1))
|
|
67
|
+
total = matrix[i, j]
|
|
68
|
+
if total <= 0.0:
|
|
69
|
+
continue
|
|
70
|
+
symbol = transition_edge.data.get(ATTR_EMISSION)
|
|
71
|
+
if symbol is None:
|
|
72
|
+
from sofic.graph import ATTR_SYMBOL
|
|
73
|
+
|
|
74
|
+
symbol = transition_edge.data.get(ATTR_SYMBOL)
|
|
75
|
+
weight = transition[i, j] * (multiplicity / total)
|
|
76
|
+
hmm.graph.add_transition(
|
|
77
|
+
transition_edge.source,
|
|
78
|
+
transition_edge.target,
|
|
79
|
+
**{ATTR_PROB: float(weight), ATTR_EMISSION: symbol},
|
|
80
|
+
)
|
|
81
|
+
hmm.validate()
|
|
82
|
+
return hmm
|
sofic/shifts/sft.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Shifts of finite type."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from itertools import product
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from sofic.graph import TransitionGraph
|
|
9
|
+
from sofic.shifts.base import SymbolicModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ShiftOfFiniteType(SymbolicModel):
|
|
13
|
+
"""SFT specified by forbidden words or an explicit presentation."""
|
|
14
|
+
|
|
15
|
+
_forbidden_words: frozenset[tuple[Any, ...]]
|
|
16
|
+
_has_forbidden_word_spec: bool
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
forbidden_words: set[tuple[Any, ...]] | frozenset[tuple[Any, ...]] | None = None,
|
|
21
|
+
**kwargs: Any,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(**kwargs)
|
|
24
|
+
self._has_forbidden_word_spec = forbidden_words is not None
|
|
25
|
+
self._forbidden_words = frozenset(tuple(word) for word in (forbidden_words or set()))
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_forbidden_words(
|
|
29
|
+
cls,
|
|
30
|
+
forbidden: set[tuple[Any, ...]],
|
|
31
|
+
symbol_alphabet: frozenset[Any],
|
|
32
|
+
**kwargs: Any,
|
|
33
|
+
) -> ShiftOfFiniteType:
|
|
34
|
+
from sofic.shifts.sft_construction import from_forbidden_words
|
|
35
|
+
|
|
36
|
+
return from_forbidden_words(forbidden, symbol_alphabet, **kwargs)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_presentation(
|
|
40
|
+
cls,
|
|
41
|
+
graph: TransitionGraph,
|
|
42
|
+
symbol_alphabet: frozenset[Any],
|
|
43
|
+
**kwargs: Any,
|
|
44
|
+
) -> ShiftOfFiniteType:
|
|
45
|
+
return cls(graph=graph, symbol_alphabet=symbol_alphabet, **kwargs)
|
|
46
|
+
|
|
47
|
+
def forbidden_words(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
length: int | None = None,
|
|
51
|
+
max_length: int | None = None,
|
|
52
|
+
minimal: bool = False,
|
|
53
|
+
) -> frozenset[tuple[Any, ...]]:
|
|
54
|
+
"""Return forbidden words stored on or inferred from this presentation.
|
|
55
|
+
|
|
56
|
+
With no bounds, this returns the finite defining set supplied to
|
|
57
|
+
:meth:`from_forbidden_words`. For presentation-only SFTs, pass
|
|
58
|
+
``length`` or ``max_length`` to enumerate forbidden blocks up to that
|
|
59
|
+
bound.
|
|
60
|
+
"""
|
|
61
|
+
if length is None and max_length is None:
|
|
62
|
+
if minimal and self._has_forbidden_word_spec:
|
|
63
|
+
return _minimal_forbidden(self._forbidden_words)
|
|
64
|
+
if self._has_forbidden_word_spec:
|
|
65
|
+
return self._forbidden_words
|
|
66
|
+
raise ValueError("pass length or max_length to infer forbidden words from a presentation")
|
|
67
|
+
if length is not None and max_length is not None:
|
|
68
|
+
raise ValueError("pass either length or max_length, not both")
|
|
69
|
+
if length is not None and length < 0:
|
|
70
|
+
raise ValueError("length must be nonnegative")
|
|
71
|
+
if max_length is not None and max_length < 0:
|
|
72
|
+
raise ValueError("max_length must be nonnegative")
|
|
73
|
+
if length is not None:
|
|
74
|
+
lengths = range(length, length + 1)
|
|
75
|
+
else:
|
|
76
|
+
assert max_length is not None
|
|
77
|
+
lengths = range(1, max_length + 1)
|
|
78
|
+
forbidden = set().union(*(self._forbidden_words_by_length(n) for n in lengths))
|
|
79
|
+
return _minimal_forbidden(frozenset(forbidden)) if minimal else frozenset(forbidden)
|
|
80
|
+
|
|
81
|
+
def _forbidden_words_by_length(self, length: int) -> frozenset[tuple[Any, ...]]:
|
|
82
|
+
if length < 0:
|
|
83
|
+
raise ValueError("length must be nonnegative")
|
|
84
|
+
alphabet = sorted(self.symbol_alphabet, key=repr)
|
|
85
|
+
allowed = set(self.factor_language(length))
|
|
86
|
+
candidates = set(product(alphabet, repeat=length))
|
|
87
|
+
return frozenset(candidates - allowed)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _minimal_forbidden(words: frozenset[tuple[Any, ...]]) -> frozenset[tuple[Any, ...]]:
|
|
91
|
+
minimal: set[tuple[Any, ...]] = set()
|
|
92
|
+
for word in words:
|
|
93
|
+
has_forbidden_subword = False
|
|
94
|
+
for start in range(len(word)):
|
|
95
|
+
for stop in range(start + 1, len(word) + 1):
|
|
96
|
+
subword = word[start:stop]
|
|
97
|
+
if subword != word and subword in words:
|
|
98
|
+
has_forbidden_subword = True
|
|
99
|
+
break
|
|
100
|
+
if has_forbidden_subword:
|
|
101
|
+
break
|
|
102
|
+
if not has_forbidden_subword:
|
|
103
|
+
minimal.add(word)
|
|
104
|
+
return frozenset(minimal)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""SFT construction from forbidden words."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sofic.graph import ATTR_SYMBOL, TransitionGraph
|
|
8
|
+
from sofic.shifts.sft import ShiftOfFiniteType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def from_forbidden_words(
|
|
12
|
+
forbidden: set[tuple[Any, ...]],
|
|
13
|
+
symbol_alphabet: frozenset[Any],
|
|
14
|
+
*,
|
|
15
|
+
max_states: int = 256,
|
|
16
|
+
) -> ShiftOfFiniteType:
|
|
17
|
+
"""Build an SFT presentation via a follower automaton on allowed prefixes."""
|
|
18
|
+
alphabet = tuple(symbol_alphabet)
|
|
19
|
+
forbidden_set = set(forbidden)
|
|
20
|
+
max_len = max((len(word) for word in forbidden_set), default=0)
|
|
21
|
+
|
|
22
|
+
def is_allowed(prefix: tuple[Any, ...]) -> bool:
|
|
23
|
+
return all(not (len(word) <= len(prefix) and prefix[-len(word) :] == word) for word in forbidden_set)
|
|
24
|
+
|
|
25
|
+
graph = TransitionGraph()
|
|
26
|
+
start: tuple[Any, ...] = ()
|
|
27
|
+
graph.add_state(start)
|
|
28
|
+
queue = [start]
|
|
29
|
+
seen = {start}
|
|
30
|
+
|
|
31
|
+
while queue:
|
|
32
|
+
prefix = queue.pop(0)
|
|
33
|
+
if len(seen) >= max_states:
|
|
34
|
+
break
|
|
35
|
+
for symbol in alphabet:
|
|
36
|
+
extended = prefix + (symbol,)
|
|
37
|
+
if not is_allowed(extended):
|
|
38
|
+
continue
|
|
39
|
+
trimmed = extended
|
|
40
|
+
if max_len > 0:
|
|
41
|
+
trimmed = extended[-max_len:]
|
|
42
|
+
if trimmed not in seen:
|
|
43
|
+
seen.add(trimmed)
|
|
44
|
+
graph.add_state(trimmed)
|
|
45
|
+
queue.append(trimmed)
|
|
46
|
+
graph.add_transition(prefix, trimmed, **{ATTR_SYMBOL: symbol})
|
|
47
|
+
|
|
48
|
+
return ShiftOfFiniteType.from_presentation(
|
|
49
|
+
graph,
|
|
50
|
+
symbol_alphabet=symbol_alphabet,
|
|
51
|
+
forbidden_words=forbidden_set,
|
|
52
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Sliding block codes (factor maps) between subshifts.
|
|
2
|
+
|
|
3
|
+
A sliding block code is the symbolic-dynamics form of a transducer: a map
|
|
4
|
+
``Phi`` between shift spaces induced by a local rule on a window of ``memory + 1
|
|
5
|
+
+ anticipation`` consecutive symbols (the Curtis-Hedlund-Lyndon theorem
|
|
6
|
+
characterizes exactly the shift-commuting continuous maps this way). See
|
|
7
|
+
Lind & Marcus, *An Introduction to Symbolic Dynamics and Coding* (1995), ch. 1
|
|
8
|
+
and 6.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Iterable, Mapping, Sequence
|
|
14
|
+
from itertools import product
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from sofic.graph import ATTR_OUTPUT, ATTR_SYMBOL, TransitionGraph
|
|
18
|
+
from sofic.shifts.sofic import SoficShift
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SlidingBlockCode:
|
|
22
|
+
"""A block map ``Phi`` with given memory and anticipation.
|
|
23
|
+
|
|
24
|
+
``block_map`` sends each length-``(memory + 1 + anticipation)`` input window to
|
|
25
|
+
a single output symbol. Applying ``Phi`` to a point slides this window,
|
|
26
|
+
emitting one output symbol per position.
|
|
27
|
+
|
|
28
|
+
Examples
|
|
29
|
+
--------
|
|
30
|
+
>>> from sofic.shifts.sliding_block_code import SlidingBlockCode
|
|
31
|
+
>>> nor = SlidingBlockCode(
|
|
32
|
+
... {("0", "0"): "1", ("0", "1"): "0", ("1", "0"): "0", ("1", "1"): "0"},
|
|
33
|
+
... memory=1,
|
|
34
|
+
... anticipation=0,
|
|
35
|
+
... )
|
|
36
|
+
>>> nor.apply_word(["0", "0", "1", "0"])
|
|
37
|
+
('1', '0', '0')
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
block_map: Mapping[Sequence[Any], Any],
|
|
43
|
+
*,
|
|
44
|
+
memory: int = 0,
|
|
45
|
+
anticipation: int = 0,
|
|
46
|
+
input_alphabet: Iterable[Any] | None = None,
|
|
47
|
+
output_alphabet: Iterable[Any] | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
if memory < 0 or anticipation < 0:
|
|
50
|
+
raise ValueError("memory and anticipation must be non-negative")
|
|
51
|
+
self.memory = memory
|
|
52
|
+
self.anticipation = anticipation
|
|
53
|
+
self.window = memory + anticipation + 1
|
|
54
|
+
self.block_map: dict[tuple[Any, ...], Any] = {tuple(key): value for key, value in block_map.items()}
|
|
55
|
+
for key in self.block_map:
|
|
56
|
+
if len(key) != self.window:
|
|
57
|
+
raise ValueError(f"block_map key {key!r} has length {len(key)}, expected window {self.window}")
|
|
58
|
+
self.input_alphabet = frozenset(
|
|
59
|
+
input_alphabet if input_alphabet is not None else {symbol for key in self.block_map for symbol in key}
|
|
60
|
+
)
|
|
61
|
+
self.output_alphabet = frozenset(output_alphabet if output_alphabet is not None else self.block_map.values())
|
|
62
|
+
|
|
63
|
+
def __repr__(self) -> str:
|
|
64
|
+
return f"SlidingBlockCode(window={self.window}, memory={self.memory}, anticipation={self.anticipation})"
|
|
65
|
+
|
|
66
|
+
def apply_word(self, word: Sequence[Any]) -> tuple[Any, ...]:
|
|
67
|
+
"""Return ``Phi`` applied to a finite word (shrinks by ``window - 1``)."""
|
|
68
|
+
word = tuple(word)
|
|
69
|
+
if len(word) < self.window:
|
|
70
|
+
return ()
|
|
71
|
+
return tuple(self.block_map[word[i : i + self.window]] for i in range(len(word) - self.window + 1))
|
|
72
|
+
|
|
73
|
+
def apply(self, shift: Any) -> SoficShift:
|
|
74
|
+
"""Return the image subshift ``Phi(shift)`` as a sofic presentation.
|
|
75
|
+
|
|
76
|
+
Uses the higher-block construction: vertices are allowed
|
|
77
|
+
``(window - 1)``-blocks of ``shift`` and each allowed ``window``-block
|
|
78
|
+
contributes an edge labeled by its image symbol.
|
|
79
|
+
"""
|
|
80
|
+
image = SoficShift(symbol_alphabet=frozenset(self.output_alphabet))
|
|
81
|
+
blocks = list(shift.factor_language(self.window))
|
|
82
|
+
vertices = {block[:-1] for block in blocks} | {block[1:] for block in blocks}
|
|
83
|
+
for vertex in vertices:
|
|
84
|
+
image.graph.add_state(vertex)
|
|
85
|
+
used_outputs: set[Any] = set()
|
|
86
|
+
for block in blocks:
|
|
87
|
+
output = self.block_map.get(block)
|
|
88
|
+
if output is None:
|
|
89
|
+
continue
|
|
90
|
+
image.add_transition(block[:-1], block[1:], output)
|
|
91
|
+
used_outputs.add(output)
|
|
92
|
+
image.symbol_alphabet = frozenset(used_outputs)
|
|
93
|
+
return image.trim_transient()
|
|
94
|
+
|
|
95
|
+
def is_right_resolving(self, shift: Any | None = None) -> bool:
|
|
96
|
+
"""Return whether the induced image presentation is right-resolving."""
|
|
97
|
+
source = shift if shift is not None else full_shift(self.input_alphabet)
|
|
98
|
+
return self.apply(source).is_unifilar()
|
|
99
|
+
|
|
100
|
+
def compose(self, other: SlidingBlockCode) -> SlidingBlockCode:
|
|
101
|
+
"""Return ``other`` applied after ``self`` (function composition ``other ∘ self``)."""
|
|
102
|
+
window = self.window + other.window - 1
|
|
103
|
+
composed: dict[tuple[Any, ...], Any] = {}
|
|
104
|
+
for word in product(sorted(self.input_alphabet, key=repr), repeat=window):
|
|
105
|
+
middle = self.apply_word(word)
|
|
106
|
+
if len(middle) != other.window:
|
|
107
|
+
continue
|
|
108
|
+
output = other.block_map.get(tuple(middle))
|
|
109
|
+
if output is None:
|
|
110
|
+
continue
|
|
111
|
+
composed[word] = output
|
|
112
|
+
return SlidingBlockCode(
|
|
113
|
+
composed,
|
|
114
|
+
memory=self.memory + other.memory,
|
|
115
|
+
anticipation=self.anticipation + other.anticipation,
|
|
116
|
+
input_alphabet=self.input_alphabet,
|
|
117
|
+
output_alphabet=other.output_alphabet,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def to_transducer(self) -> Any:
|
|
121
|
+
"""Return a Mealy machine realizing this code (output delayed by anticipation)."""
|
|
122
|
+
from sofic.automata.transducers import MealyMachine
|
|
123
|
+
|
|
124
|
+
graph = TransitionGraph()
|
|
125
|
+
context_length = self.window - 1
|
|
126
|
+
contexts = list(product(sorted(self.input_alphabet, key=repr), repeat=context_length))
|
|
127
|
+
for context in contexts:
|
|
128
|
+
graph.add_state(context)
|
|
129
|
+
used_outputs: set[Any] = set()
|
|
130
|
+
for context in contexts:
|
|
131
|
+
for symbol in sorted(self.input_alphabet, key=repr):
|
|
132
|
+
window = (*context, symbol)
|
|
133
|
+
output = self.block_map.get(window)
|
|
134
|
+
if output is None:
|
|
135
|
+
continue
|
|
136
|
+
target = window[1:]
|
|
137
|
+
graph.add_transition(context, target, **{ATTR_SYMBOL: symbol, ATTR_OUTPUT: output})
|
|
138
|
+
used_outputs.add(output)
|
|
139
|
+
machine = MealyMachine(
|
|
140
|
+
input_alphabet=frozenset(self.input_alphabet),
|
|
141
|
+
output_alphabet=frozenset(used_outputs),
|
|
142
|
+
initial_states=frozenset(contexts),
|
|
143
|
+
graph=graph,
|
|
144
|
+
)
|
|
145
|
+
machine.validate()
|
|
146
|
+
return machine
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def full_shift(alphabet: Iterable[Any]) -> SoficShift:
|
|
150
|
+
"""Return the full shift over ``alphabet`` as a one-state sofic presentation."""
|
|
151
|
+
symbols = frozenset(alphabet)
|
|
152
|
+
shift = SoficShift(symbol_alphabet=symbols)
|
|
153
|
+
shift.graph.add_state("*")
|
|
154
|
+
for symbol in symbols:
|
|
155
|
+
shift.add_transition("*", "*", symbol)
|
|
156
|
+
return shift
|
sofic/shifts/sofic.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Sofic shift presentations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from sofic.shifts.base import SymbolicModel
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from sofic.generators.mealy import MealyHMM
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SoficShift(SymbolicModel):
|
|
14
|
+
"""Sofic subshift given by a labeled directed graph presentation."""
|
|
15
|
+
|
|
16
|
+
def topological_entropy(self) -> float:
|
|
17
|
+
from sofic.shifts.tmc_construction import topological_entropy
|
|
18
|
+
|
|
19
|
+
return topological_entropy(self)
|
|
20
|
+
|
|
21
|
+
def parry_measure(self) -> MealyHMM:
|
|
22
|
+
"""Measure of maximal entropy (Parry measure) as a labeled Mealy HMM.
|
|
23
|
+
|
|
24
|
+
The unique measure attaining ``h_top`` (Parry 1964): built from the Perron
|
|
25
|
+
data of a right-resolving presentation of this shift, with each edge's
|
|
26
|
+
emitted symbol preserved. See
|
|
27
|
+
:func:`~sofic.shifts.topological_anatomy.parry_measure_sofic`.
|
|
28
|
+
"""
|
|
29
|
+
from sofic.shifts.topological_anatomy import parry_measure_sofic
|
|
30
|
+
|
|
31
|
+
return parry_measure_sofic(self)
|
|
32
|
+
|
|
33
|
+
def topological_anatomy(self) -> dict[str, float]:
|
|
34
|
+
"""Topological information anatomy: the MME split ``h_top = b_top + r_top``.
|
|
35
|
+
|
|
36
|
+
Evaluates the metric information anatomy (James et al. 2013) of the
|
|
37
|
+
observed symbol process at the measure of maximal entropy, returning
|
|
38
|
+
``{h_top, b_top, r_top, excess_entropy}``. Here ``r_top`` is the MME
|
|
39
|
+
erasure entropy rate (Verdu & Weissman 2008). See
|
|
40
|
+
:func:`~sofic.shifts.topological_anatomy.topological_anatomy`.
|
|
41
|
+
"""
|
|
42
|
+
from sofic.shifts.topological_anatomy import topological_anatomy
|
|
43
|
+
|
|
44
|
+
return topological_anatomy(self)
|
|
45
|
+
|
|
46
|
+
def markov_order(self) -> int | float:
|
|
47
|
+
"""Markov order ``R`` when the presentation is right-resolving (unifilar)."""
|
|
48
|
+
from sofic.generators.synchronization import graph_from_sofic_shift, markov_order_from_graph
|
|
49
|
+
|
|
50
|
+
return markov_order_from_graph(graph_from_sofic_shift(self))
|
|
51
|
+
|
|
52
|
+
def cryptic_order(self) -> int | float:
|
|
53
|
+
"""Cryptic order ``k_chi`` when the presentation is right-resolving."""
|
|
54
|
+
from sofic.generators.synchronization import cryptic_order_from_graph, graph_from_sofic_shift
|
|
55
|
+
|
|
56
|
+
return cryptic_order_from_graph(graph_from_sofic_shift(self))
|
|
57
|
+
|
|
58
|
+
def reset_threshold(self) -> int | float:
|
|
59
|
+
"""Reset threshold: shortest synchronizing (reset) word length.
|
|
60
|
+
|
|
61
|
+
The complement of :meth:`markov_order`; ``math.inf`` when not exactly
|
|
62
|
+
synchronizable. For the Cerny bound (complete automata only) see Volkov
|
|
63
|
+
(2008).
|
|
64
|
+
"""
|
|
65
|
+
from sofic.generators.synchronization import graph_from_sofic_shift, reset_threshold_from_graph
|
|
66
|
+
|
|
67
|
+
return reset_threshold_from_graph(graph_from_sofic_shift(self))
|
|
68
|
+
|
|
69
|
+
def synchronizing_word(self) -> list[Any] | None:
|
|
70
|
+
"""Return a shortest synchronizing word, or ``None`` if not exactly synchronizable.
|
|
71
|
+
|
|
72
|
+
The list of symbols has length :meth:`reset_threshold` (Travers &
|
|
73
|
+
Crutchfield, arXiv:1008.4182).
|
|
74
|
+
"""
|
|
75
|
+
from sofic.generators.synchronization import graph_from_sofic_shift, shortest_synchronizing_word_from_graph
|
|
76
|
+
|
|
77
|
+
return shortest_synchronizing_word_from_graph(graph_from_sofic_shift(self))
|
|
78
|
+
|
|
79
|
+
def is_exactly_synchronizable(self) -> bool:
|
|
80
|
+
"""Return whether this presentation is exactly synchronizable.
|
|
81
|
+
|
|
82
|
+
True iff a finite synchronizing word exists (the reset threshold is
|
|
83
|
+
finite; Travers & Crutchfield, arXiv:1008.4182). Strictly weaker than
|
|
84
|
+
finite Markov order: see :meth:`is_definite`.
|
|
85
|
+
"""
|
|
86
|
+
from sofic.generators.synchronization import graph_from_sofic_shift, is_exactly_synchronizable
|
|
87
|
+
|
|
88
|
+
return is_exactly_synchronizable(graph_from_sofic_shift(self))
|
|
89
|
+
|
|
90
|
+
def is_asymptotically_synchronizable(self) -> bool:
|
|
91
|
+
"""Return whether this presentation is asymptotically synchronizable.
|
|
92
|
+
|
|
93
|
+
True for every finite-state ε-machine (Travers & Crutchfield,
|
|
94
|
+
arXiv:1008.4182); theorem-backed rather than computed.
|
|
95
|
+
"""
|
|
96
|
+
from sofic.generators.synchronization import (
|
|
97
|
+
graph_from_sofic_shift,
|
|
98
|
+
is_asymptotically_synchronizable_from_graph,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return is_asymptotically_synchronizable_from_graph(graph_from_sofic_shift(self))
|
|
102
|
+
|
|
103
|
+
def is_definite(self) -> bool:
|
|
104
|
+
"""Return whether this presentation is a definite automaton (finite Markov order).
|
|
105
|
+
|
|
106
|
+
Definiteness (Perles-Rabin-Shamir) means the state is fixed by the last
|
|
107
|
+
``R`` symbols. Implies :meth:`is_exactly_synchronizable`.
|
|
108
|
+
"""
|
|
109
|
+
from sofic.generators.synchronization import graph_from_sofic_shift, is_definite_from_graph
|
|
110
|
+
|
|
111
|
+
return is_definite_from_graph(graph_from_sofic_shift(self))
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Sofic-Dyck shift presentations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable, Iterator, Sequence
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from sofic.graph import ATTR_KIND, ATTR_SYMBOL, KIND_CALL, KIND_INTERNAL, KIND_RETURN, Transition
|
|
9
|
+
from sofic.shifts.base import SymbolicModel
|
|
10
|
+
|
|
11
|
+
TransitionRef = tuple[Hashable, Hashable, int]
|
|
12
|
+
MatchedEdge = tuple[TransitionRef, TransitionRef]
|
|
13
|
+
|
|
14
|
+
_KINDS = frozenset({KIND_CALL, KIND_RETURN, KIND_INTERNAL})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def transition_ref(transition: Transition) -> TransitionRef:
|
|
18
|
+
"""Return the stable graph reference for ``transition``."""
|
|
19
|
+
return transition.source, transition.target, transition.key
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SoficDyckShift(SymbolicModel):
|
|
23
|
+
"""Sofic-Dyck shift presented by a finite Dyck automaton."""
|
|
24
|
+
|
|
25
|
+
call_alphabet: frozenset[Any]
|
|
26
|
+
return_alphabet: frozenset[Any]
|
|
27
|
+
internal_alphabet: frozenset[Any]
|
|
28
|
+
matched_edges: frozenset[MatchedEdge]
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
call_alphabet: frozenset[Any] | None = None,
|
|
33
|
+
return_alphabet: frozenset[Any] | None = None,
|
|
34
|
+
internal_alphabet: frozenset[Any] | None = None,
|
|
35
|
+
matched_edges: set[MatchedEdge] | frozenset[MatchedEdge] | None = None,
|
|
36
|
+
symbol_alphabet: frozenset[Any] | None = None,
|
|
37
|
+
**kwargs: Any,
|
|
38
|
+
) -> None:
|
|
39
|
+
self.call_alphabet = call_alphabet if call_alphabet is not None else frozenset()
|
|
40
|
+
self.return_alphabet = return_alphabet if return_alphabet is not None else frozenset()
|
|
41
|
+
self.internal_alphabet = internal_alphabet if internal_alphabet is not None else frozenset()
|
|
42
|
+
inferred_alphabet = self.call_alphabet | self.return_alphabet | self.internal_alphabet
|
|
43
|
+
super().__init__(
|
|
44
|
+
symbol_alphabet=symbol_alphabet if symbol_alphabet is not None else inferred_alphabet, **kwargs
|
|
45
|
+
)
|
|
46
|
+
self.matched_edges = frozenset(matched_edges or frozenset())
|
|
47
|
+
|
|
48
|
+
def validate(self) -> None:
|
|
49
|
+
super().validate()
|
|
50
|
+
role_alphabet = self.call_alphabet | self.return_alphabet | self.internal_alphabet
|
|
51
|
+
self._require(
|
|
52
|
+
len(self.call_alphabet) + len(self.return_alphabet) + len(self.internal_alphabet) == len(role_alphabet),
|
|
53
|
+
"call, return, and internal alphabets must be disjoint",
|
|
54
|
+
)
|
|
55
|
+
self._require(self.symbol_alphabet == role_alphabet, "symbol_alphabet must equal the visible role alphabets")
|
|
56
|
+
|
|
57
|
+
call_edges: set[TransitionRef] = set()
|
|
58
|
+
return_edges: set[TransitionRef] = set()
|
|
59
|
+
all_edges: set[TransitionRef] = set()
|
|
60
|
+
for transition in self.transitions():
|
|
61
|
+
ref = transition_ref(transition)
|
|
62
|
+
all_edges.add(ref)
|
|
63
|
+
kind = transition.data.get(ATTR_KIND)
|
|
64
|
+
symbol = transition.data.get(ATTR_SYMBOL)
|
|
65
|
+
self._require(kind in _KINDS, f"invalid Dyck edge kind {kind!r}")
|
|
66
|
+
self._require(symbol is not None, "Dyck shift transitions require a symbol")
|
|
67
|
+
if kind == KIND_CALL:
|
|
68
|
+
self._require(symbol in self.call_alphabet, f"{symbol!r} not in call alphabet")
|
|
69
|
+
call_edges.add(ref)
|
|
70
|
+
elif kind == KIND_RETURN:
|
|
71
|
+
self._require(symbol in self.return_alphabet, f"{symbol!r} not in return alphabet")
|
|
72
|
+
return_edges.add(ref)
|
|
73
|
+
else:
|
|
74
|
+
self._require(symbol in self.internal_alphabet, f"{symbol!r} not in internal alphabet")
|
|
75
|
+
|
|
76
|
+
for call_ref, return_ref in self.matched_edges:
|
|
77
|
+
self._require(call_ref in all_edges, f"matched call edge {call_ref!r} is missing")
|
|
78
|
+
self._require(return_ref in all_edges, f"matched return edge {return_ref!r} is missing")
|
|
79
|
+
self._require(call_ref in call_edges, f"matched edge {call_ref!r} is not a call transition")
|
|
80
|
+
self._require(return_ref in return_edges, f"matched edge {return_ref!r} is not a return transition")
|
|
81
|
+
|
|
82
|
+
def add_call_transition(self, source: Hashable, target: Hashable, symbol: Any, **attrs: Any) -> TransitionRef:
|
|
83
|
+
"""Add a call transition and return its stable edge reference."""
|
|
84
|
+
key = self.graph.add_transition(source, target, **{**attrs, ATTR_KIND: KIND_CALL, ATTR_SYMBOL: symbol})
|
|
85
|
+
return source, target, key
|
|
86
|
+
|
|
87
|
+
def add_return_transition(self, source: Hashable, target: Hashable, symbol: Any, **attrs: Any) -> TransitionRef:
|
|
88
|
+
"""Add a return transition and return its stable edge reference."""
|
|
89
|
+
key = self.graph.add_transition(source, target, **{**attrs, ATTR_KIND: KIND_RETURN, ATTR_SYMBOL: symbol})
|
|
90
|
+
return source, target, key
|
|
91
|
+
|
|
92
|
+
def add_internal_transition(self, source: Hashable, target: Hashable, symbol: Any, **attrs: Any) -> TransitionRef:
|
|
93
|
+
"""Add an internal transition and return its stable edge reference."""
|
|
94
|
+
key = self.graph.add_transition(source, target, **{**attrs, ATTR_KIND: KIND_INTERNAL, ATTR_SYMBOL: symbol})
|
|
95
|
+
return source, target, key
|
|
96
|
+
|
|
97
|
+
def add_matched_pair(self, call_ref: TransitionRef, return_ref: TransitionRef) -> None:
|
|
98
|
+
"""Mark ``call_ref`` and ``return_ref`` as a legal call-return pair."""
|
|
99
|
+
self.matched_edges = frozenset({*self.matched_edges, (call_ref, return_ref)})
|
|
100
|
+
|
|
101
|
+
def is_admissible_word(self, word: Sequence[Any]) -> bool:
|
|
102
|
+
"""Return whether ``word`` is a finite factor of this Dyck presentation."""
|
|
103
|
+
from sofic.shifts.dyck_algorithms import is_admissible_word
|
|
104
|
+
|
|
105
|
+
return is_admissible_word(self, word)
|
|
106
|
+
|
|
107
|
+
def factor_language(self, length: int) -> Iterator[tuple[Any, ...]]:
|
|
108
|
+
from sofic.shifts.dyck_algorithms import admissible_words
|
|
109
|
+
|
|
110
|
+
yield from admissible_words(self, length)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Sofic relations: subshifts over a product alphabet ``X x Y``.
|
|
2
|
+
|
|
3
|
+
The topological support of a transducer is a subshift of the product shift on
|
|
4
|
+
``X x Y`` -- a "sofic relation" whose input and output projections are the
|
|
5
|
+
transducer's domain and range subshifts. This is the symbolic-dynamics reading
|
|
6
|
+
of a transducer, complementary to the sliding block code (Lind & Marcus, *An
|
|
7
|
+
Introduction to Symbolic Dynamics and Coding* (1995), ch. 6).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from sofic.graph import ATTR_OUTPUT, ATTR_SYMBOL, EPSILON
|
|
15
|
+
from sofic.shifts.sofic import SoficShift
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SoficRelation(SoficShift):
|
|
19
|
+
"""A sofic subshift whose symbols are ``(input, output)`` pairs."""
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def from_transducer(cls, transducer: Any) -> SoficRelation:
|
|
23
|
+
"""Build the topological support of a transducer (probabilities dropped)."""
|
|
24
|
+
relation = cls()
|
|
25
|
+
pairs: set[tuple[Any, Any]] = set()
|
|
26
|
+
for state in transducer.states():
|
|
27
|
+
relation.graph.add_state(state)
|
|
28
|
+
for transition in transducer.transitions():
|
|
29
|
+
input_symbol = transition.data.get(ATTR_SYMBOL, EPSILON)
|
|
30
|
+
output_symbol = transition.data.get(ATTR_OUTPUT, EPSILON)
|
|
31
|
+
pair = (input_symbol, output_symbol)
|
|
32
|
+
relation.add_transition(transition.source, transition.target, pair)
|
|
33
|
+
pairs.add(pair)
|
|
34
|
+
relation.symbol_alphabet = frozenset(pairs)
|
|
35
|
+
return relation
|
|
36
|
+
|
|
37
|
+
def input_alphabet(self) -> frozenset[Any]:
|
|
38
|
+
"""Return the set of input symbols appearing in the relation."""
|
|
39
|
+
return frozenset(pair[0] for pair in self.symbol_alphabet)
|
|
40
|
+
|
|
41
|
+
def output_alphabet(self) -> frozenset[Any]:
|
|
42
|
+
"""Return the set of output symbols appearing in the relation."""
|
|
43
|
+
return frozenset(pair[1] for pair in self.symbol_alphabet)
|
|
44
|
+
|
|
45
|
+
def input_shift(self) -> SoficShift:
|
|
46
|
+
"""Return the projection onto the input coordinate as a sofic shift."""
|
|
47
|
+
return self._project(0)
|
|
48
|
+
|
|
49
|
+
def output_shift(self) -> SoficShift:
|
|
50
|
+
"""Return the projection onto the output coordinate as a sofic shift."""
|
|
51
|
+
return self._project(1)
|
|
52
|
+
|
|
53
|
+
def _project(self, index: int) -> SoficShift:
|
|
54
|
+
projected = SoficShift()
|
|
55
|
+
symbols: set[Any] = set()
|
|
56
|
+
for state in self.states():
|
|
57
|
+
projected.graph.add_state(state)
|
|
58
|
+
for transition in self.transitions():
|
|
59
|
+
pair = transition.data.get(ATTR_SYMBOL)
|
|
60
|
+
symbol = pair[index] if isinstance(pair, tuple) else pair
|
|
61
|
+
projected.add_transition(transition.source, transition.target, symbol)
|
|
62
|
+
symbols.add(symbol)
|
|
63
|
+
projected.symbol_alphabet = frozenset(symbols)
|
|
64
|
+
return projected
|