diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bc74670 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,115 @@ +# Changelog + +## 0.3.0 (unreleased) + +A correctness review and breaking refactor. Old names are **not** kept as +aliases; use the tables below to migrate. + +### Behavior changes + +- **All information quantities are in bits**, including log-likelihoods + (`log_likelihood`, the Baum-Welch trace, cross-validated log-likelihood, + HDP-HMM traces), topological entropy (was nats), and collision entropy (was + nats). AIC, AICc, BIC, and WAIC keep their standard deviance scale (computed + from the natural log-likelihood); the MDL code length is in bits. `score` and + `observed_information` remain derivatives of the natural log-likelihood, so + standard errors are unchanged. +- **Covers follow Lind & Marcus**: *right* means right-resolving. + `RightFischerCover` is now the exact minimal right-resolving presentation + (it was built from follower sets truncated at length 8 and labeled *left*); + reducible shifts raise `SoficValidationError`. `LeftFischerCover` is its + mirror image. +- `MaximizedPrimeAtomaton` subclasses `NFA`, not `AtomicAutomaton`: it is the + reverse of the canonical RFSA of the reversed language and need not be atomic. +- `CanonicalVisiblyPushdownAutomaton.from_vpa` raises + `NonWellMatchedLanguageError` for languages with pending calls or returns. +- `DeterministicVisiblyPushdownAutomaton.from_vpa` determinizes + nondeterministic input instead of raising. +- VPA boolean and structural operations return concrete automata. +- Wildcard VPA returns fire on every stack symbol and, with a bottom symbol, on + the empty stack (the simulator's behavior; the docstring said otherwise). +- `ShiftOfFiniteType.from_forbidden_words` raises instead of silently + truncating at `max_states`; an empty forbidden set builds the full shift. +- `equivalent()` compares automata over the union of both alphabets. +- `MarkovChain.words_of_length(0)` and `sample_path` respect the initial + distribution; sampling from an initial law with no mass raises `ValueError`. +- `block_entropy_estimates(use_exact=True)` reports crypticity as `C_mu - E`. +- `SlidingBlockCode.apply` keeps constraints longer than the window and raises + when the block map misses an allowed block. +- Baum-Welch raises when every sequence is impossible and warns when some are. +- `BuchiAutomaton.accepts_lasso` rejects an empty loop. +- Stack CSSR no longer drops histories during determinization (which produced + zero-mass states and validation errors). +- TikZ labels: fixed uncompilable `\midcall` / `\uparrowA` and escaped + `\times` / sympy LaTeX. +- cmpy example factories validate parameters like their curated counterparts + (e.g. a coin bias of 1 raises). Processes previously built by `Even`, `Nemo`, + and `ABC` now come from the curated functions and emit integer symbols `0`/`1`; + `alternating_biased_coins(p, p)` keeps its two-state presentation. +- `joint_block_distribution(block_length=n)` counts blocks of `n` symbols + (`history_length=h` meant `h + 1` symbols); the default `2` is unchanged. + +### New + +- Exact canonical RFSA (`CanonicalRFSA.from_language`), maximized prime + átomaton, `CanonicalRFSA.dual` / `MaximizedPrimeAtomaton.dual`, exact + `prime_residuals`, `atoms`, `prime_atoms`, and `ResidualTable`. +- NL\*: `learn_rfsa_nlstar`, `learn_prime_atomaton_nlstar`, + `learn_rfsa_from_language`, and `AutomatonEquivalenceOracle`. +- VPA: `determinize`, `is_empty`, `accepted_word`, `is_universal`, `includes`, + `equivalent`, `has_unmatched_word`; `sofic.automata.vpa.to_single_entry` / + `to_multiple_entry`; modular `minimize` converts automatically when no + modules are given. `NestedWordAutomaton` gains the same operations. +- Exact left/right Krieger covers. +- `sofic.generators.matrices` (joint matrices and start-vector policies) and + `sofic.generators.sampling`. + +### Module moves + +| Old module | New module | +|---|---| +| `sofic.generators.hmm_inference` | `sofic.inference.hmm` (`filtering`, `em`, `information`); `sample` → `sofic.generators.sampling` | +| `sofic.generators.epsilon_inference` | `sofic.inference.cssr` (`process`, `subtree`, `counts`, `significance`); `spectral` → `sofic.inference.spectral` | +| `sofic.generators.epsilon_transducer_inference` | `sofic.inference.cssr.transducer` | +| `sofic.generators.stack_inference` | `sofic.inference.cssr.stack` | +| `sofic.automata.{active,rpni,edsm,dfasat,alergia,papni,observation}` | `sofic.automata.learning.*` | +| `sofic.automata.learning` (NL\*) | `sofic.automata.learning.nlstar` | +| `sofic.automata.vpa`, `vpa_simulation` | `sofic.automata.vpa` package (`base`, `operations`, `deterministic`, `modular`, `canonical`, `simulation`) | +| `sofic.automata.{icdfa,idfa,enumeration}` | `sofic.automata.enumeration.{icdfa,idfa,words}` | +| `sofic.automata.{canonical_extraction,rfsa,atomaton,canonical_dual}` | `sofic.automata.canonical.{residual,rfsa,atomaton,dual}` | +| `sofic.shifts.sofic_relation` | `sofic.shifts.product_alphabet_shift` | + +### Renames and removals + +| Old | New | +|---|---| +| `cssr` | `learn_epsilon_machine_cssr` | +| `subtree_merge` | `learn_epsilon_machine_subtree` | +| `spectral` (wrapper) | `learn_epsilon_machine_spectral` | +| `transcssr` | `learn_epsilon_transducer_cssr` | +| `stack_cssr` / `stack_subtree_merge` / `fit_stack_hmm_mle` | `learn_stack_hmm_cssr` / `learn_stack_hmm_subtree` / `learn_stack_hmm_mle` | +| `suggest_lmax` | `suggest_max_history` | +| `Lmax=`, `L=` (CSSR family and subtree learners) | `max_history=` | +| `reconstruction_sweep(lmaxes=)` | `reconstruction_sweep(max_histories=)` | +| `GoodnessOfFit.L`, `goodness_of_fit(L=)` | `block_length` | +| `forward(scaled=)`, `backward(scaled=)` | `normalize=` | +| `joint_block_distribution(history_length=h)` | `joint_block_distribution(block_length=h + 1)` | +| `QuasiStochasticModel.transition_matrices`, `quasi_inference.transition_matrices` | `symbol_matrices` | +| `sofic.generators.words.hmm_*`, `pfa_*`, `quasi_*`, `markov_*` functions | private; use the model methods | +| `cartesian_product_gg` / `cartesian_product_tt` | `generator_product` / `transducer_product` | +| `compose_tt` / `compose_tg` | `compose_transducers` / `compose_transducer_generator` | +| `wnfa_to_wdfa` / `minimum_wdfa` | `determinize_wheeler` / `minimize_wheeler` | +| `papni_encode` / `papni_encode_samples` | `encode_dyck_word` / `encode_dyck_samples` | +| ICDFA helpers `next_flags`, `string_from_flags`, `flags_from_string`, `count_flag_sequences` | `icdfa_next_flags`, `icdfa_string_from_flags`, `icdfa_flags_from_string`, `icdfa_count_flag_sequences` | +| IDFA helpers `string_from_flags`, `extended_flags`, `transition_count` | `idfa_string_from_flags`, `idfa_extended_flags`, `idfa_transition_count` | +| `CallDrivenAutomaton` | `ModularVisiblyPushdownAutomaton` | +| `CompositeVisiblyPushdownAutomaton`, `union_vpa`, `intersection_vpa`, `complement_vpa`, `difference_vpa`, `concat_vpa`, `kleene_star_vpa` | removed; use the VPA methods | +| `LabeledAutomaton.intersect` / `concatenate` / `star` | `intersection` / `concat` / `kleene_star` | +| `learn_maximized_prime_atomaton` | `learn_prime_atomaton_nlstar` (or `learn_rfsa_nlstar`) | +| `SoficRelation`, `to_sofic_relation` | `ProductAlphabetShift`, `to_product_alphabet_shift` | +| cover `from_sofic` | `from_presentation` | +| `{left,right}_{fischer,krieger}_from_sofic` | `{left,right}_{fischer,krieger}_cover` | +| `sofic.from_yaml`, `serialization.from_yaml` | `model_from_yaml` | +| `examples.processes`: `BiasedCoin`, `FairCoin`, `Even`, `Nemo`, `NRPS`, `ABC` | removed: `bernoulli`, `fair_coin`, `even_process`, `nemo_process`, `noisy_random_phase_slip`, `alternating_biased_coins(1 - p, 1 - q)` | +| `GoldenMean` / `Butterfly` / `PSB` | `golden_mean_forbid_00` / `butterfly_two_branch` / `phase_slip_backtrack_cmpy` | +| other PascalCase `examples.processes` factories (e.g. `BitFlip`, `Delay`, `RestrictedGM`, `IrreversibleTwoState`) | snake_case (`bit_flip`, `delay`, `restricted_gm`, `irreversible_two_state`) | diff --git a/README.rst b/README.rst index 9a26a98..8433bbf 100644 --- a/README.rst +++ b/README.rst @@ -171,7 +171,7 @@ read off computational-mechanics quantities: from sofic import EpsilonMachine eps = EpsilonMachine.from_hmm(gm) # minimize an HMM presentation - # eps = EpsilonMachine.from_sequence(data, method="cssr", Lmax=4) # infer + # eps = EpsilonMachine.from_sequence(data, method="cssr", max_history=4) # infer # eps = EpsilonMachine.from_sequence(data, method="spectral", prefix_length=3, rank=2) eps.statistical_complexity() # 0.9183 bits (C_mu) diff --git a/docs/automata/learning.rst b/docs/automata/learning.rst index e8ab557..fcb7717 100644 --- a/docs/automata/learning.rst +++ b/docs/automata/learning.rst @@ -188,6 +188,6 @@ For fitting probabilities on the learned topology, see .. autofunction:: sofic.automata.learning.papni.learn_sofic_dyck_shift_papni .. autofunction:: sofic.automata.learning.papni.is_well_matched -.. autofunction:: sofic.automata.learning.papni.papni_encode -.. autofunction:: sofic.automata.learning.papni.papni_encode_samples +.. autofunction:: sofic.automata.learning.papni.encode_dyck_word +.. autofunction:: sofic.automata.learning.papni.encode_dyck_samples .. autofunction:: sofic.automata.learning.papni.sofic_dyck_shift_from_papni_dfa diff --git a/docs/automata/subsequential.rst b/docs/automata/subsequential.rst index 870ebf7..cdb0da7 100644 --- a/docs/automata/subsequential.rst +++ b/docs/automata/subsequential.rst @@ -31,9 +31,9 @@ hierarchy :cite:`Mohri2009`. In [5]: from sofic.automata.subsequential import WeightedFiniteStateTransducer - In [6]: from sofic.examples.processes import BinaryChannel + In [6]: from sofic.examples.processes import binary_channel - In [7]: w = WeightedFiniteStateTransducer.from_transducer(BinaryChannel(0.1, 0.2)) + In [7]: w = WeightedFiniteStateTransducer.from_transducer(binary_channel(0.1, 0.2)) @doctest float In [8]: w.weight(['0'], ['0']) diff --git a/docs/automata/transducers.rst b/docs/automata/transducers.rst index c301b78..262dfa2 100644 --- a/docs/automata/transducers.rst +++ b/docs/automata/transducers.rst @@ -29,15 +29,15 @@ Composition The composition helpers mirror the common ``cmpy`` transducer operations: -* :func:`sofic.automata.transducer_operations.compose_tt` serially composes +* :func:`sofic.automata.transducer_operations.compose_transducers` serially composes transducers. -* :func:`sofic.automata.transducer_operations.compose_tg` composes a +* :func:`sofic.automata.transducer_operations.compose_transducer_generator` composes a transducer with a stochastic generator and returns a joint input/output generator. * :func:`sofic.automata.transducer_operations.transduce_generator` returns the output-only generator induced by driving a transducer with a generator. -* :func:`sofic.automata.transducer_operations.cartesian_product_tt` and - :func:`sofic.automata.transducer_operations.cartesian_product_gg` build +* :func:`sofic.automata.transducer_operations.transducer_product` and + :func:`sofic.automata.transducer_operations.generator_product` build tuple-symbol Cartesian products. For convenience, :class:`MealyMachine` also exposes ``compose``, @@ -55,8 +55,8 @@ API .. autofunction:: sofic.automata.transducer_simulation.transduce_mealy .. autofunction:: sofic.automata.transducer_simulation.transduce_moore -.. autofunction:: sofic.automata.transducer_operations.compose_tt -.. autofunction:: sofic.automata.transducer_operations.compose_tg +.. autofunction:: sofic.automata.transducer_operations.compose_transducers +.. autofunction:: sofic.automata.transducer_operations.compose_transducer_generator .. autofunction:: sofic.automata.transducer_operations.transduce_generator -.. autofunction:: sofic.automata.transducer_operations.cartesian_product_tt -.. autofunction:: sofic.automata.transducer_operations.cartesian_product_gg +.. autofunction:: sofic.automata.transducer_operations.transducer_product +.. autofunction:: sofic.automata.transducer_operations.generator_product diff --git a/docs/automata/vpa.rst b/docs/automata/vpa.rst index 22fd902..33884e6 100644 --- a/docs/automata/vpa.rst +++ b/docs/automata/vpa.rst @@ -70,7 +70,7 @@ forms exist for well-matched languages, or once calls are assigned to modules A k-module MEVPA. A module may have several entries, and the pushed symbol depends only on the caller state. -``CallDrivenAutomaton`` +``ModularVisiblyPushdownAutomaton`` The shared modular generalization: a call's target depends only on the call symbol. @@ -108,7 +108,7 @@ API .. autoclass:: MultipleEntryVisiblyPushdownAutomaton :members: minimize -.. autoclass:: CallDrivenAutomaton +.. autoclass:: ModularVisiblyPushdownAutomaton :members: minimize .. autoclass:: CanonicalVisiblyPushdownAutomaton diff --git a/docs/automata/wheeler.rst b/docs/automata/wheeler.rst index 5274d3f..c347bc9 100644 --- a/docs/automata/wheeler.rst +++ b/docs/automata/wheeler.rst @@ -71,7 +71,7 @@ What the order buys uses this to replace ``2^n`` subsets with ``n(n+1)/2`` intervals, which makes the Markov order, cryptic order, and reset threshold polynomial. * Determinization to at most ``2n - 1 - |Sigma|`` states - (:func:`wnfa_to_wdfa`) and a unique minimal WDFA (:func:`minimum_wdfa`), + (:func:`determinize_wheeler`) and a unique minimal WDFA (:func:`minimize_wheeler`), both of which fail for general automata. * A succinct index — see below. @@ -100,8 +100,8 @@ Width Canonical forms and minimization ================================ -.. autofunction:: minimum_wdfa -.. autofunction:: wnfa_to_wdfa +.. autofunction:: minimize_wheeler +.. autofunction:: determinize_wheeler .. autofunction:: wheeler_canonical_form .. autofunction:: wheeler_isomorphic .. autofunction:: wheeler_state_index diff --git a/docs/core/serialization.rst b/docs/core/serialization.rst index bcfc090..6cf794a 100644 --- a/docs/core/serialization.rst +++ b/docs/core/serialization.rst @@ -26,7 +26,7 @@ The instance methods :meth:`~sofic.core.StateMachine.to_yaml`, :meth:`~sofic.core.StateMachine.write_yaml`, :meth:`~sofic.core.StateMachine.from_yaml`, and :meth:`~sofic.core.StateMachine.read_yaml` delegate to the module-level -functions below. The polymorphic :func:`model_from_yaml` / :func:`from_yaml` +functions below. The polymorphic :func:`model_from_yaml` / :func:`read_yaml` readers reconstruct the correct subclass from the serialized class tag, so they are convenient when the concrete type is not known in advance. @@ -35,7 +35,6 @@ API .. autofunction:: model_to_yaml .. autofunction:: model_from_yaml -.. autofunction:: from_yaml .. autofunction:: read_yaml .. autofunction:: model_to_dict .. autofunction:: model_from_dict diff --git a/docs/examples.rst b/docs/examples.rst index 4c749b1..071a7bd 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -165,33 +165,41 @@ Process library --------------- In addition to the curated ε-machines above, :mod:`sofic.examples.processes` -ports a large library of parametrized process factories (``GoldenMean``, -``Even``, ``Nemo``, ``IID``, ``Ising``, ``Ehrenfest``, the periodic and -``Misiurewicz`` families, and many more). Each is a function that returns a -generator, defaulting to an :class:`~sofic.generators.epsilon_machine.EpsilonMachine` -but accepting a ``machine_type`` argument: +ports a large library of parametrized process factories from cmpy +(``golden_mean_forbid_00``, ``iid``, ``ising``, ``ehrenfest``, the periodic and +``misiurewicz`` families, and many more). Every factory name is snake_case. Each +is a function that returns a generator, defaulting to an +:class:`~sofic.generators.epsilon_machine.EpsilonMachine` but accepting a +``machine_type`` argument: .. ipython:: - In [1]: from sofic.examples import GoldenMean, Even, Nemo + In [1]: from sofic.examples import golden_mean_forbid_00 - In [2]: gm = GoldenMean(bias=0.5) + In [2]: gm = golden_mean_forbid_00(bias=0.5) @doctest float In [3]: gm.entropy_rate() Out[3]: 0.6666666666666665 -Several factories share one implementation with a curated example and differ only -in string symbols (``"0"``/``"1"``), state names, or parametrization: -``BiasedCoin(b)`` is :func:`bernoulli` ``(b)``, ``Even`` is :func:`even_process`, -``Nemo`` is :func:`nemo_process`, ``GoldenMean(b)`` is -:func:`golden_mean_forward` ``(1 - b)`` (and :func:`golden_mean_markov` ``(b)`` -with ``A``/``B`` swapped), ``ABC(p, q)`` is :func:`alternating_biased_coins` -``(1 - p, 1 - q)`` for ``p != q``, ``RestrictedGM`` is -:func:`restricted_golden_mean`, and ``IrreversibleTwoState()`` is +cmpy factories that merely duplicated a curated example are not ported; use the +curated function instead: cmpy's ``BiasedCoin(b)`` is :func:`bernoulli` ``(b)``, +``FairCoin`` is :func:`fair_coin`, ``Even(bias=b)`` is :func:`even_process` +``(b)``, ``Nemo(p=p, q=q)`` is :func:`nemo_process` ``(p, q)``, ``NRPS`` is +:func:`noisy_random_phase_slip`, and ``ABC(p, q)`` is +:func:`alternating_biased_coins` ``(1 - p, 1 - q)``. The curated versions emit +integer symbols ``0``/``1`` (except :func:`bernoulli`), whereas the ported +factories emit strings ``"0"``/``"1"``. + +Several remaining factories share an implementation with a curated example and +differ only in string symbols, state names, or parametrization: +``golden_mean_forbid_00(b)`` is :func:`golden_mean_forward` ``(1 - b)`` (and +:func:`golden_mean_markov` ``(b)`` with ``A``/``B`` swapped), ``restricted_gm`` +is :func:`restricted_golden_mean`, and ``irreversible_two_state()`` is :func:`ellison_fig9_forward`. Similar names do not always mean the same process: -:func:`golden_mean` is the ``0 <-> 1`` mirror of ``GoldenMean``, and ``Butterfly`` -and ``PSB`` differ from :func:`butterfly_process` and +:func:`golden_mean` (forbids ``11``) is the ``0 <-> 1`` mirror of +``golden_mean_forbid_00``, and ``butterfly_two_branch`` and +``phase_slip_backtrack_cmpy`` differ from :func:`butterfly_process` and :func:`~sofic.examples.epsilon_machines.phase_slip_backtrack`. The module also exposes registries — ``processes.process_list`` and @@ -205,8 +213,8 @@ parametrized tests and sweeps: In [5]: len(processes.process_list) > 0 Out[5]: True -A parallel set of transducer factories (``BitFlip``, ``Parity``, ``Delay``, -``BinaryChannel``, …) lives alongside the processes and produces +A parallel set of transducer factories (``bit_flip``, ``parity``, ``delay``, +``binary_channel``, …) lives alongside the processes and produces :class:`~sofic.automata.transducers.MealyMachine` instances. Symbolic-shift examples (:mod:`sofic.examples.shifts`) provide the diff --git a/docs/generators/epsilon_transducer.rst b/docs/generators/epsilon_transducer.rst index 38b29b6..d53ab53 100644 --- a/docs/generators/epsilon_transducer.rst +++ b/docs/generators/epsilon_transducer.rst @@ -26,16 +26,16 @@ Minimize a joint-unifilar stochastic transducer to its causal states: In [1]: from sofic import EpsilonTransducer - In [2]: from sofic.examples.processes import BinaryChannel, GMtoEven + In [2]: from sofic.examples.processes import binary_channel, gm_to_even - In [3]: eps = EpsilonTransducer.from_channel(BinaryChannel(0.1, 0.2)) + In [3]: eps = EpsilonTransducer.from_channel(binary_channel(0.1, 0.2)) @doctest In [4]: len(list(eps.states())) Out[4]: 1 @doctest - In [5]: EpsilonTransducer.from_channel(GMtoEven()).is_unifilar() + In [5]: EpsilonTransducer.from_channel(gm_to_even()).is_unifilar() Out[5]: True The channel can also be read off a joint ``(input, output)`` generator with diff --git a/docs/generators/information_anatomy.rst b/docs/generators/information_anatomy.rst index 2732bd8..c04382e 100644 --- a/docs/generators/information_anatomy.rst +++ b/docs/generators/information_anatomy.rst @@ -120,7 +120,7 @@ forward and reverse structural ephemeral branches match. .. ipython:: - In [1]: from sofic.examples import nemo_process, NRPS + In [1]: from sofic.examples import nemo_process, noisy_random_phase_slip In [2]: nemo = nemo_process().to_bidirectional() @@ -128,7 +128,7 @@ forward and reverse structural ephemeral branches match. In [3]: round(nemo.internal_markov_entropy_rate(), 6), round(nemo.reverse_internal_markov_entropy_rate(), 6) Out[3]: (0.5, 0.5) - In [4]: nrps = NRPS().to_bidirectional() + In [4]: nrps = noisy_random_phase_slip().to_bidirectional() # An arrow of time (r_fwd != r_rev): forward and reverse chain rates differ. In [5]: round(nrps.internal_markov_entropy_rate(), 6), round(nrps.reverse_internal_markov_entropy_rate(), 6) diff --git a/docs/generators/quasi_realization.rst b/docs/generators/quasi_realization.rst index 376c9ec..fb6928a 100644 --- a/docs/generators/quasi_realization.rst +++ b/docs/generators/quasi_realization.rst @@ -16,6 +16,6 @@ API .. autoclass:: QuasiRealization .. autoclass:: QuasiStochasticModel -.. autofunction:: sofic.generators.quasi_inference.transition_matrices +.. autofunction:: sofic.generators.quasi_inference.symbol_matrices .. autofunction:: sofic.generators.quasi_inference.stationary_quasidistribution .. autofunction:: sofic.generators.quasi_inference.word_probability diff --git a/docs/inference/cssr.rst b/docs/inference/cssr.rst index a493bc0..77cb6b7 100644 --- a/docs/inference/cssr.rst +++ b/docs/inference/cssr.rst @@ -29,7 +29,7 @@ Quick start observations, _ = oracle.sample(5000, rng) inferred = EpsilonMachine.from_sequence( - observations, method="cssr", Lmax=4, alpha=0.001 + observations, method="cssr", max_history=4, alpha=0.001 ) len(list(inferred.states())) # 2 for the even process @@ -39,7 +39,7 @@ CSSR CSSR :cite:`Shalizi2002` starts from an IID model and grows causal states in three phases: 1. **Initialize** — one state for the empty history. -2. **Homogenize** — extend each suffix one symbol into the past, up to ``Lmax``; a +2. **Homogenize** — extend each suffix one symbol into the past, up to ``max_history``; a child suffix whose next-symbol distribution differs significantly from its state's (G-test, :math:`\chi^2`, Monte Carlo exact G-test, or total-variation threshold) moves to the best @@ -47,23 +47,23 @@ CSSR :cite:`Shalizi2002` starts from an IID model and grows causal states in thr 3. **Determinize** — drop transient states, then split states until each state and symbol lead to a single successor, then keep the most-visited recurrent class. -A length-``Lmax`` suffix has no one-symbol extension in the suffix tree, so its +A length-``max_history`` suffix has no one-symbol extension in the suffix tree, so its successor drops the oldest symbol. For a non-Markovian process that can forget the -phase: in the even process with ``Lmax = 3``, the successor of ``011`` on ``1`` would -be the ambiguous ``111``. So the length-``Lmax + 1`` suffix (here ``0111``) is tested +phase: in the even process with ``max_history = 3``, the successor of ``011`` on ``1`` would +be the ambiguous ``111``. So the length-``max_history + 1`` suffix (here ``0111``) is tested against the truncated suffix's state, and is sent to the best matching state when the two differ. -Choose ``Lmax`` at least the synchronization length of the source (its order, for +Choose ``max_history`` at least the synchronization length of the source (its order, for a Markov source). Much larger values run many more significance tests, and some split states by chance; lowering ``alpha`` counters this. A process that is not -exactly synchronizable has no finite-``Lmax`` reconstruction, and CSSR returns +exactly synchronizable has no finite-``max_history`` reconstruction, and CSSR returns extra states. -Choosing ``Lmax`` and calibrating the tests -------------------------------------------- +Choosing ``max_history`` and calibrating the tests +-------------------------------------------------- -``Lmax="auto"`` sets ``Lmax`` to :func:`suggest_lmax`, the Markov order estimated +``max_history="auto"`` sets ``max_history`` to :func:`suggest_max_history`, the Markov order estimated by :func:`dit.inference.select_markov_order`. Its default method tests order :math:`n` against :math:`n + 1` with surrogates that preserve the observed :math:`(n + 1)`-gram counts exactly, so the test holds its nominal size at any @@ -89,12 +89,12 @@ apply directly. .. code-block:: python inferred = EpsilonMachine.from_sequence( - observations, method="cssr", Lmax="auto", test="exact", correction="bonferroni" + observations, method="cssr", max_history="auto", test="exact", correction="bonferroni" ) -.. autofunction:: cssr +.. autofunction:: learn_epsilon_machine_cssr -.. autofunction:: suggest_lmax +.. autofunction:: suggest_max_history After reconstruction, check the result with :func:`~sofic.inference.diagnostics.goodness_of_fit` and @@ -112,11 +112,11 @@ to a unifilar presentation. With ``delta=0``, two morphs are equivalent unless G-test at significance 0.01 tells them apart, a tolerance that scales with the sample. Transitions follow the same successor rule as CSSR. -``subtree_merge`` accepts ``alpha``, ``test`` (including ``"exact"``) and +``learn_epsilon_machine_subtree`` accepts ``alpha``, ``test`` (including ``"exact"``) and ``correction="bonferroni"``, which divides ``alpha`` over the history pairs -compared, as well as ``L="auto"``. +compared, as well as ``max_history="auto"``. -.. autofunction:: subtree_merge +.. autofunction:: learn_epsilon_machine_subtree Spectral reconstruction ======================= @@ -139,7 +139,7 @@ process). Otherwise the Hankel singular-value gap selects the rank. observations, method="spectral", prefix_length=3, rank=2 ) -.. autofunction:: sofic.inference.spectral.spectral +.. autofunction:: sofic.inference.spectral.learn_epsilon_machine_spectral Unified entry point =================== diff --git a/docs/inference/diagnostics.rst b/docs/inference/diagnostics.rst index a6ec334..7fa76f5 100644 --- a/docs/inference/diagnostics.rst +++ b/docs/inference/diagnostics.rst @@ -15,7 +15,7 @@ Goodness of fit :func:`goodness_of_fit` is a parametric bootstrap :cite:`Efron1993`. It simulates sequences as long as the data from the fitted machine and compares a -length-``L`` word statistic of the data with its distribution over the +length-``block_length`` word statistic of the data with its distribution over the simulations. Two statistics are available: * ``"g"`` — the G statistic of the observed word counts against the machine's @@ -25,17 +25,17 @@ simulations. Two statistics are available: Because the null distribution is simulated, overlapping windows need no correction. A small p-value means the machine misses structure. For CSSR that -usually means ``Lmax`` is shorter than the source's synchronization length. +usually means ``max_history`` is shorter than the source's synchronization length. .. code-block:: python - from sofic.inference.cssr import cssr + from sofic.inference.cssr import learn_epsilon_machine_cssr from sofic.inference.diagnostics import goodness_of_fit - machine = cssr(data, Lmax=1) - goodness_of_fit(machine, data, L=6).pvalue # small for the even process - machine = cssr(data, Lmax=4) - goodness_of_fit(machine, data, L=6).pvalue # large + machine = learn_epsilon_machine_cssr(data, max_history=1) + goodness_of_fit(machine, data, block_length=6).pvalue # small for the even process + machine = learn_epsilon_machine_cssr(data, max_history=4) + goodness_of_fit(machine, data, block_length=6).pvalue # large Observed words the machine forbids are listed in ``forbidden_words``. @@ -51,7 +51,7 @@ words the source never emits and can add spurious states. For example, on even-process data it returns 6–12-state machines where subsampling returns the true 2 states. -:func:`reconstruction_sweep` reconstructs over a grid of ``alpha`` and ``Lmax``. +:func:`reconstruction_sweep` reconstructs over a grid of ``alpha`` and ``max_history``. A structure that persists over a range of settings is better supported than one that appears at a single setting. diff --git a/docs/inference/spectral.rst b/docs/inference/spectral.rst index 8f84b0b..d24f959 100644 --- a/docs/inference/spectral.rst +++ b/docs/inference/spectral.rst @@ -68,16 +68,16 @@ Projection to an ε-machine non-negative Mealy projection when one exists in the learned basis, otherwise mixed-state enumeration of the observable operators :cite:`Ellison2009`. The same path is -:func:`~sofic.inference.spectral.spectral` / +:func:`~sofic.inference.spectral.learn_epsilon_machine_spectral` / ``EpsilonMachine.from_sequence(..., method="spectral")``. .. code-block:: python - from sofic.inference.spectral import spectral + from sofic.inference.spectral import learn_epsilon_machine_spectral from sofic.examples import golden_mean process = golden_mean(0.5) - eps = spectral(word_probability=process.word_probability, alphabet=(0, 1), prefix_length=3, rank=2) + eps = learn_epsilon_machine_spectral(word_probability=process.word_probability, alphabet=(0, 1), prefix_length=3, rank=2) len(list(eps.states())) # 2 API diff --git a/docs/inference/stack_cssr.rst b/docs/inference/stack_cssr.rst index 0b2ab14..ef1fa1c 100644 --- a/docs/inference/stack_cssr.rst +++ b/docs/inference/stack_cssr.rst @@ -15,9 +15,9 @@ Two families are provided: * **Topology known.** Given a :class:`~sofic.shifts.sofic_dyck.SoficDyckShift` presentation, - :func:`fit_stack_hmm_mle` estimates smoothed maximum-likelihood transition + :func:`learn_stack_hmm_mle` estimates smoothed maximum-likelihood transition weights from a sample. -* **Topology unknown.** :func:`stack_cssr` and :func:`stack_subtree_merge` +* **Topology unknown.** :func:`learn_stack_hmm_cssr` and :func:`learn_stack_hmm_subtree` reconstruct both the control graph and its probabilities from a single long sequence by splitting stack-aware histories, in the spirit of Causal-State Splitting Reconstruction :cite:`Shalizi2004`. @@ -28,14 +28,14 @@ Two families are provided: .. code-block:: python from sofic.automata import DyckAlphabet - from sofic.inference.cssr import stack_cssr + from sofic.inference.cssr import learn_stack_hmm_cssr alphabet = DyckAlphabet( call_alphabet=frozenset({"("}), return_alphabet=frozenset({")"}), internal_alphabet=frozenset({"a"}), ) - model = stack_cssr(sequence, alphabet=alphabet, Lmax=4, max_stack_depth=8) + model = learn_stack_hmm_cssr(sequence, alphabet=alphabet, max_history=4, max_stack_depth=8) model.validate() Histories are counted with a bounded stack depth, so ``max_stack_depth`` caps @@ -48,18 +48,18 @@ can follow is decided by the stack top through matched call-return pairs, not by the finite control. Return edges are matched only to calls observed to close them. -``stack_cssr`` accepts the same calibration options as -:func:`~sofic.inference.cssr.cssr`: ``test="exact"``, -``correction="bonferroni"`` (over eligible configurations), and ``Lmax="auto"``. +``learn_stack_hmm_cssr`` accepts the same calibration options as +:func:`~sofic.inference.cssr.learn_epsilon_machine_cssr`: ``test="exact"``, +``correction="bonferroni"`` (over eligible configurations), and ``max_history="auto"``. Stack processes generally have infinite Markov order, so the automatic depth is a lower bound on the suffix length the data support. API === -.. autofunction:: stack_cssr -.. autofunction:: stack_subtree_merge -.. autofunction:: fit_stack_hmm_mle +.. autofunction:: learn_stack_hmm_cssr +.. autofunction:: learn_stack_hmm_subtree +.. autofunction:: learn_stack_hmm_mle .. autofunction:: learn_stack_hmm_papni .. autoclass:: StackSuffixCounts diff --git a/docs/inference/transcssr.rst b/docs/inference/transcssr.rst index dc09af1..6de557e 100644 --- a/docs/inference/transcssr.rst +++ b/docs/inference/transcssr.rst @@ -16,10 +16,10 @@ symbol ``x``. Rare histories inherit their parent's state (controlled by ``min_count``); the split decision uses a G-test at significance ``alpha``, the same test as :func:`~sofic.inference.cssr.morphs_differ` (with Yates' continuity correction at one degree of freedom). -As in :func:`~sofic.inference.cssr.cssr`, ``test="exact"`` uses a +As in :func:`~sofic.inference.cssr.learn_epsilon_machine_cssr`, ``test="exact"`` uses a Monte Carlo exact G-test for tables with small expected counts, and ``correction="bonferroni"`` divides ``alpha`` by the number of -(history, input symbol) tests. ``Lmax="auto"`` sets the depth from the Markov +(history, input symbol) tests. ``max_history="auto"`` sets the depth from the Markov order of the joint ``(input, output)`` sequence :cite:`Pethel2014`. .. ipython:: @@ -28,15 +28,15 @@ order of the joint ``(input, output)`` sequence :cite:`Pethel2014`. In [2]: from sofic import EpsilonTransducer, MealyHMM - In [3]: from sofic.examples.processes import Delay + In [3]: from sofic.examples.processes import delay - In [4]: from sofic.automata.transducer_operations import compose_tg + In [4]: from sofic.automata.transducer_operations import compose_transducer_generator In [5]: inp = MealyHMM(observation_alphabet=frozenset({'0', '1'}), initial_distribution={'S': 1.0}) In [6]: inp.graph.add_state('S'); _ = inp.add_transition('S', 'S', '0', 0.5); _ = inp.add_transition('S', 'S', '1', 0.5); inp.validate() - In [7]: joint = compose_tg(Delay(1), inp, joint=True) + In [7]: joint = compose_transducer_generator(delay(1), inp, joint=True) In [8]: obs, _ = joint.sample(8000, np.random.default_rng(0)) @@ -49,7 +49,7 @@ order of the joint ``(input, output)`` sequence :cite:`Pethel2014`. API === -.. autofunction:: transcssr +.. autofunction:: learn_epsilon_transducer_cssr .. autoclass:: JointSuffixCounts :members: from_sequences, output_counts, state_morph diff --git a/docs/shifts/covers.rst b/docs/shifts/covers.rst index a935dc8..d1b1683 100644 --- a/docs/shifts/covers.rst +++ b/docs/shifts/covers.rst @@ -34,21 +34,21 @@ images of path relations lying on cycles of the finite relation monoid. In [4]: for s, t, a in [("A", "A", "0"), ("A", "B", "1"), ("B", "A", "1")]: ...: even.graph.add_transition(s, t, **{ATTR_SYMBOL: a}) - In [5]: len(list(RightKriegerCover.from_sofic(even).states())) + In [5]: len(list(RightKriegerCover.from_presentation(even).states())) API === .. autoclass:: LeftFischerCover - :members: from_sofic + :members: from_presentation .. autoclass:: RightFischerCover - :members: from_sofic + :members: from_presentation .. autoclass:: LeftKriegerCover - :members: from_sofic + :members: from_presentation .. autoclass:: RightKriegerCover - :members: from_sofic + :members: from_presentation -.. autofunction:: sofic.shifts.cover_construction.left_fischer_from_sofic -.. autofunction:: sofic.shifts.cover_construction.right_fischer_from_sofic -.. autofunction:: sofic.shifts.cover_construction.left_krieger_from_sofic -.. autofunction:: sofic.shifts.cover_construction.right_krieger_from_sofic +.. autofunction:: sofic.shifts.cover_construction.left_fischer_cover +.. autofunction:: sofic.shifts.cover_construction.right_fischer_cover +.. autofunction:: sofic.shifts.cover_construction.left_krieger_cover +.. autofunction:: sofic.shifts.cover_construction.right_krieger_cover diff --git a/docs/shifts/product_alphabet_shift.rst b/docs/shifts/product_alphabet_shift.rst new file mode 100644 index 0000000..890f8c1 --- /dev/null +++ b/docs/shifts/product_alphabet_shift.rst @@ -0,0 +1,36 @@ +.. product_alphabet_shift.rst +.. py:module:: sofic.shifts.product_alphabet_shift + +********************** +Product-Alphabet Shift +********************** + +A :class:`ProductAlphabetShift` is the topological support of a transducer: a +sofic subshift of the product shift on ``X x Y`` whose input and output projections are the +transducer's domain and range subshifts :cite:`LindMarcus1995`. It is the +symbolic-dynamics reading of a transducer, complementary to the +:doc:`sliding block code `. + +.. ipython:: + + In [1]: from sofic import ProductAlphabetShift + + In [2]: from sofic.examples.processes import gm_to_even + + In [3]: rel = ProductAlphabetShift.from_transducer(gm_to_even()) + + @doctest + In [4]: sorted(rel.output_alphabet()) + Out[4]: ['0', '1'] + + In [5]: input_shift = rel.input_shift() + +Build one with :meth:`~ProductAlphabetShift.from_transducer` and recover the coordinate +shifts with :meth:`~ProductAlphabetShift.input_shift` and +:meth:`~ProductAlphabetShift.output_shift`. + +API +=== + +.. autoclass:: ProductAlphabetShift + :members: from_transducer, input_shift, output_shift, input_alphabet, output_alphabet diff --git a/docs/shifts/shifts.rst b/docs/shifts/shifts.rst index 2f07371..03a0425 100644 --- a/docs/shifts/shifts.rst +++ b/docs/shifts/shifts.rst @@ -18,7 +18,7 @@ type, Sofic shifts, Dyck shifts, topological Markov chains, and covers markov_dyck_shift topological_markov_chain sliding_block_code - sofic_relation + product_alphabet_shift textile dyck_enumeration covers diff --git a/docs/shifts/sofic_relation.rst b/docs/shifts/sofic_relation.rst deleted file mode 100644 index 852c389..0000000 --- a/docs/shifts/sofic_relation.rst +++ /dev/null @@ -1,36 +0,0 @@ -.. sofic_relation.rst -.. py:module:: sofic.shifts.sofic_relation - -************** -Sofic Relation -************** - -A :class:`SoficRelation` is the topological support of a transducer: a subshift -of the product shift on ``X x Y`` whose input and output projections are the -transducer's domain and range subshifts :cite:`LindMarcus1995`. It is the -symbolic-dynamics reading of a transducer, complementary to the -:doc:`sliding block code `. - -.. ipython:: - - In [1]: from sofic import SoficRelation - - In [2]: from sofic.examples.processes import GMtoEven - - In [3]: rel = SoficRelation.from_transducer(GMtoEven()) - - @doctest - In [4]: sorted(rel.output_alphabet()) - Out[4]: ['0', '1'] - - In [5]: input_shift = rel.input_shift() - -Build one with :meth:`~SoficRelation.from_transducer` and recover the coordinate -shifts with :meth:`~SoficRelation.input_shift` and -:meth:`~SoficRelation.output_shift`. - -API -=== - -.. autoclass:: SoficRelation - :members: from_transducer, input_shift, output_shift, input_alphabet, output_alphabet diff --git a/docs/shifts/textile.rst b/docs/shifts/textile.rst index 67e6285..a197383 100644 --- a/docs/shifts/textile.rst +++ b/docs/shifts/textile.rst @@ -17,9 +17,9 @@ shift. In [1]: from sofic import TextileSystem - In [2]: from sofic.examples.processes import SlidingNOR + In [2]: from sofic.examples.processes import sliding_nor - In [3]: textile = TextileSystem.from_transducer(SlidingNOR()) + In [3]: textile = TextileSystem.from_transducer(sliding_nor()) @doctest In [4]: textile.induced_code().memory @@ -29,4 +29,4 @@ API === .. autoclass:: TextileSystem - :members: from_transducer, to_transducer, to_sofic_relation, input_shift, output_shift, induced_code + :members: from_transducer, to_transducer, to_product_alphabet_shift, input_shift, output_shift, induced_code diff --git a/sofic/__init__.py b/sofic/__init__.py index 9d50338..2443353 100644 --- a/sofic/__init__.py +++ b/sofic/__init__.py @@ -37,17 +37,17 @@ WheelerIndex, WheelerOrder, automaton_to_regex, - cartesian_product_gg, - cartesian_product_tt, colex_width, complete, - compose_tg, - compose_tt, + compose_transducer_generator, + compose_transducers, determinize, equivalent, + generator_product, is_wheeler, minimize, transduce_generator, + transducer_product, trim, wheeler_order, ) @@ -86,17 +86,17 @@ wyner_generative_model, ) from sofic.operations import reverse -from sofic.serialization import from_yaml, model_from_yaml, model_to_yaml, read_yaml +from sofic.serialization import model_from_yaml, model_to_yaml, read_yaml from sofic.shifts import ( LeftFischerCover, LeftKriegerCover, MarkovDyckShift, + ProductAlphabetShift, RightFischerCover, RightKriegerCover, ShiftOfFiniteType, SlidingBlockCode, SoficDyckShift, - SoficRelation, SoficShift, SymbolicModel, TextileSystem, @@ -153,7 +153,7 @@ "ShiftOfFiniteType", "SlidingBlockCode", "SoficDyckShift", - "SoficRelation", + "ProductAlphabetShift", "SoficShift", "StateIndex", "StateMachine", @@ -174,17 +174,16 @@ "WheelerOrder", "WynerGenerativeModel", "automaton_to_regex", - "cartesian_product_gg", - "cartesian_product_tt", + "generator_product", + "transducer_product", "colex_width", "complete", - "compose_tg", - "compose_tt", + "compose_transducer_generator", + "compose_transducers", "determinize", "equivalent", "is_wheeler", "minimize", - "from_yaml", "functional_generative_model", "gacs_korner_generative_model", "is_lumpable", diff --git a/sofic/automata/__init__.py b/sofic/automata/__init__.py index 7587d5a..1b7ba55 100644 --- a/sofic/automata/__init__.py +++ b/sofic/automata/__init__.py @@ -16,19 +16,19 @@ from sofic.automata.dfa import DFA from sofic.automata.enumeration.icdfa import ( ICDFAString, - count_flag_sequences, count_icdfa, count_icdfa_empty, dfa_to_icdfa_string, first_icdfa_empty_string, - flags_from_string, + icdfa_count_flag_sequences, + icdfa_flags_from_string, + icdfa_next_flags, + icdfa_string_from_flags, icdfa_string_to_dfa, iter_icdfa, iter_icdfa_empty_strings, last_icdfa_empty_string, - next_flags, next_icdfa_empty_string, - string_from_flags, validate_icdfa_empty_string, ) from sofic.automata.enumeration.idfa import ( @@ -68,10 +68,10 @@ from sofic.automata.learning.observation import ObservationTable from sofic.automata.learning.papni import ( DyckAlphabet, + encode_dyck_samples, + encode_dyck_word, is_well_matched, learn_sofic_dyck_shift_papni, - papni_encode, - papni_encode_samples, sofic_dyck_shift_from_papni_dfa, ) from sofic.automata.learning.rpni import learn_dfa_rpni @@ -85,18 +85,18 @@ from sofic.automata.transducer_operations import ( ERROR_STATE, ERROR_SYMBOL, - cartesian_product_gg, - cartesian_product_tt, - compose_tg, - compose_tt, + compose_transducer_generator, + compose_transducers, + generator_product, transduce_generator, + transducer_product, ) from sofic.automata.transducers import MealyMachine, MooreMachine, Transducer from sofic.automata.unifilar import UnifilarAutomaton from sofic.automata.vpa import ( - CallDrivenAutomaton, CanonicalVisiblyPushdownAutomaton, DeterministicVisiblyPushdownAutomaton, + ModularVisiblyPushdownAutomaton, MultipleEntryVisiblyPushdownAutomaton, SingleEntryVisiblyPushdownAutomaton, VisiblyPushdownAutomaton, @@ -105,15 +105,15 @@ WheelerError, WheelerOrder, colex_width, + determinize_wheeler, is_input_consistent, is_wheeler, maximum_colex_relation, - minimum_wdfa, + minimize_wheeler, wheeler_canonical_form, wheeler_isomorphic, wheeler_order, wheeler_state_index, - wnfa_to_wdfa, ) from sofic.automata.wheeler_index import WheelerIndex, wheeler_index @@ -134,7 +134,7 @@ "MembershipOracle", "RandomWalkEquivalenceOracle", "TransducerOutputOracle", - "CallDrivenAutomaton", + "ModularVisiblyPushdownAutomaton", "CanonicalVisiblyPushdownAutomaton", "CanonicalRFSA", "DFA", @@ -166,13 +166,13 @@ "VisiblyPushdownAutomaton", "automaton_to_regex", "colex_width", - "cartesian_product_gg", - "cartesian_product_tt", - "compose_tg", - "compose_tt", + "generator_product", + "transducer_product", + "compose_transducer_generator", + "compose_transducers", "complete", "count_accessible_idfa", - "count_flag_sequences", + "icdfa_count_flag_sequences", "count_icdfa", "count_icdfa_empty", "determinize", @@ -180,7 +180,7 @@ "equivalent", "first_icdfa_empty_string", "first_idfa_string", - "flags_from_string", + "icdfa_flags_from_string", "icdfa_string_to_dfa", "is_input_consistent", "is_well_matched", @@ -205,15 +205,15 @@ "learn_sofic_dyck_shift_papni", "maximum_colex_relation", "minimize", - "minimum_wdfa", - "next_flags", + "minimize_wheeler", + "icdfa_next_flags", "next_icdfa_empty_string", - "papni_encode", - "papni_encode_samples", + "encode_dyck_word", + "encode_dyck_samples", "rank_idfa_string", "reroot_idfa_string", "sofic_dyck_shift_from_papni_dfa", - "string_from_flags", + "icdfa_string_from_flags", "trim", "transduce_generator", "unrank_idfa_string", @@ -224,5 +224,5 @@ "wheeler_isomorphic", "wheeler_order", "wheeler_state_index", - "wnfa_to_wdfa", + "determinize_wheeler", ] diff --git a/sofic/automata/enumeration/__init__.py b/sofic/automata/enumeration/__init__.py index e09ea83..26372de 100644 --- a/sofic/automata/enumeration/__init__.py +++ b/sofic/automata/enumeration/__init__.py @@ -2,19 +2,19 @@ from sofic.automata.enumeration.icdfa import ( ICDFAString, - count_flag_sequences, count_icdfa, count_icdfa_empty, dfa_to_icdfa_string, first_icdfa_empty_string, - flags_from_string, + icdfa_count_flag_sequences, + icdfa_flags_from_string, + icdfa_next_flags, + icdfa_string_from_flags, icdfa_string_to_dfa, iter_icdfa, iter_icdfa_empty_strings, last_icdfa_empty_string, - next_flags, next_icdfa_empty_string, - string_from_flags, validate_icdfa_empty_string, ) from sofic.automata.enumeration.idfa import ( @@ -33,24 +33,24 @@ "ICDFAString", "MISSING_TRANSITION", "count_accessible_idfa", - "count_flag_sequences", + "icdfa_count_flag_sequences", "count_icdfa", "count_icdfa_empty", "dfa_to_icdfa_string", "first_icdfa_empty_string", "first_idfa_string", - "flags_from_string", + "icdfa_flags_from_string", "icdfa_string_to_dfa", "iter_icdfa", "iter_icdfa_empty_strings", "iter_idfa_strings", "iter_language", "last_icdfa_empty_string", - "next_flags", + "icdfa_next_flags", "next_icdfa_empty_string", "rank_idfa_string", "reroot_idfa_string", - "string_from_flags", + "icdfa_string_from_flags", "unrank_idfa_string", "validate_icdfa_empty_string", "validate_idfa_string", diff --git a/sofic/automata/enumeration/icdfa.py b/sofic/automata/enumeration/icdfa.py index 8723930..68e525c 100644 --- a/sofic/automata/enumeration/icdfa.py +++ b/sofic/automata/enumeration/icdfa.py @@ -19,19 +19,19 @@ __all__ = [ "ICDFAString", - "count_flag_sequences", + "icdfa_count_flag_sequences", "count_icdfa", "count_icdfa_empty", "dfa_to_icdfa_string", "first_icdfa_empty_string", - "flags_from_string", + "icdfa_flags_from_string", "icdfa_string_to_dfa", "iter_icdfa", "iter_icdfa_empty_strings", "last_icdfa_empty_string", - "next_flags", + "icdfa_next_flags", "next_icdfa_empty_string", - "string_from_flags", + "icdfa_string_from_flags", "validate_icdfa_empty_string", ] @@ -79,7 +79,7 @@ def validate_icdfa_empty_string( raise ICDFAEnumerationError(f"state {state} does not appear in the first {k * state} symbols") -def flags_from_string(transitions: Sequence[int], *, n: int) -> tuple[int, ...]: +def icdfa_flags_from_string(transitions: Sequence[int], *, n: int) -> tuple[int, ...]: """Return first-occurrence indices ``(f_1, …, f_{n-1})`` for ``transitions``.""" if n <= 1: return () @@ -104,7 +104,7 @@ def _validate_flags(flags: Sequence[int], *, n: int, k: int) -> None: raise ICDFAEnumerationError(f"flag f_{index + 1}={flags[index]} not in ({lower}, {upper}]") -def string_from_flags( +def icdfa_string_from_flags( flags: Sequence[int], *, n: int, @@ -124,7 +124,7 @@ def first_icdfa_empty_string(*, n: int, k: int) -> tuple[int, ...]: if n == 1: return (0,) * k flags = tuple(k * state - 1 for state in range(1, n)) - return string_from_flags(flags, n=n, k=k) + return icdfa_string_from_flags(flags, n=n, k=k) def last_icdfa_empty_string(*, n: int, k: int) -> tuple[int, ...]: @@ -132,7 +132,7 @@ def last_icdfa_empty_string(*, n: int, k: int) -> tuple[int, ...]: if n == 1: return (0,) * k flags = list(range(n - 1)) - transitions = list(string_from_flags(flags, n=n, k=k)) + transitions = list(icdfa_string_from_flags(flags, n=n, k=k)) flag_set = set(flags) for index in range(k * n): if index in flag_set: @@ -141,7 +141,7 @@ def last_icdfa_empty_string(*, n: int, k: int) -> tuple[int, ...]: return tuple(transitions) -def next_flags(flags: list[int], *, k: int) -> None: +def icdfa_next_flags(flags: list[int], *, k: int) -> None: """Advance ``flags`` in-place to the next valid flag sequence, or raise ``StopIteration``.""" def nextflags(index: int) -> None: @@ -233,7 +233,7 @@ def nexticdfa(state: int, symbol: int) -> None: nexticdfa(n - 1, k - 1) -def count_flag_sequences(k: int, n: int) -> int: +def icdfa_count_flag_sequences(k: int, n: int) -> int: """Return ``F_{k,n}``, the number of valid flag sequences (Fuss--Catalan).""" if n <= 1: return 1 @@ -284,17 +284,17 @@ def iter_icdfa_empty_strings(k: int, n: int) -> Iterator[tuple[int, ...]]: return flags = [k * state - 1 for state in range(1, n)] - transitions = list(string_from_flags(flags, n=n, k=k)) + transitions = list(icdfa_string_from_flags(flags, n=n, k=k)) while True: yield tuple(transitions) try: next_icdfa_empty_string(transitions, flags, n=n, k=k) except StopIteration: try: - next_flags(flags, k=k) + icdfa_next_flags(flags, k=k) except StopIteration: break - transitions[:] = list(string_from_flags(flags, n=n, k=k)) + transitions[:] = list(icdfa_string_from_flags(flags, n=n, k=k)) def iter_icdfa(k: int, n: int) -> Iterator[tuple[tuple[int, ...], frozenset[int]]]: diff --git a/sofic/automata/enumeration/idfa.py b/sofic/automata/enumeration/idfa.py index 56adfd7..72f9357 100644 --- a/sofic/automata/enumeration/idfa.py +++ b/sofic/automata/enumeration/idfa.py @@ -14,7 +14,7 @@ from sofic.automata.enumeration.icdfa import ( _upper_bound_at, _validate_flags, - next_flags, + icdfa_next_flags, ) from sofic.exceptions import SoficValidationError @@ -22,14 +22,14 @@ "MISSING_TRANSITION", "count_accessible_idfa", "count_idfa_strings_for_flags", - "extended_flags", + "idfa_extended_flags", "first_idfa_string", "idfa_string_to_topological_graph", "iter_idfa_strings", "last_idfa_string", "next_idfa_string", "rank_idfa_string", - "transition_count", + "idfa_transition_count", "unrank_idfa_string", "validate_idfa_string", ] @@ -41,7 +41,7 @@ class IDFAEnumerationError(SoficValidationError): """Raised when incomplete accessible DFA enumeration fails.""" -def extended_flags(flags: Sequence[int], *, n: int, k: int) -> tuple[int, ...]: +def idfa_extended_flags(flags: Sequence[int], *, n: int, k: int) -> tuple[int, ...]: """Return ``(f_0, …, f_n)`` with ``f_0 = -1`` and ``f_n = nk``.""" return (MISSING_TRANSITION,) + tuple(flags) + (n * k,) @@ -71,12 +71,12 @@ def validate_idfa_string( raise IDFAEnumerationError(f"state {state} does not appear in the first {k * state} symbols") -def transition_count(transitions: Sequence[int]) -> int: +def idfa_transition_count(transitions: Sequence[int]) -> int: """Return the number of defined transitions in ``transitions``.""" return sum(1 for value in transitions if value != MISSING_TRANSITION) -def string_from_flags( +def idfa_string_from_flags( flags: Sequence[int], *, n: int, @@ -98,7 +98,7 @@ def first_idfa_string(*, n: int, k: int) -> tuple[int, ...]: if n == 1: return (MISSING_TRANSITION,) * k flags = tuple(k * state - 1 for state in range(1, n)) - return string_from_flags(flags, n=n, k=k) + return idfa_string_from_flags(flags, n=n, k=k) def last_idfa_string(*, n: int, k: int) -> tuple[int, ...]: @@ -106,7 +106,7 @@ def last_idfa_string(*, n: int, k: int) -> tuple[int, ...]: if n == 1: return (n - 1,) * k flags = list(range(n - 1)) - transitions = list(string_from_flags(flags, n=n, k=k)) + transitions = list(idfa_string_from_flags(flags, n=n, k=k)) flag_set = set(flags) for index in range(k * n): if index in flag_set: @@ -185,7 +185,7 @@ def nextidfa(state: int, symbol: int) -> None: def count_idfa_strings_for_flags(flags: Sequence[int], *, n: int, k: int) -> int: """Return the number of IDFA∅ strings with the given flag sequence.""" - ext = extended_flags(flags, n=n, k=k) + ext = idfa_extended_flags(flags, n=n, k=k) product = 1 for segment_index in range(n): segment = ext[segment_index + 1] - ext[segment_index] - 1 @@ -261,17 +261,17 @@ def _iter_idfa_strings_impl(k: int, n: int) -> Iterator[tuple[int, ...]]: return flags = [k * state - 1 for state in range(1, n)] - transitions = list(string_from_flags(flags, n=n, k=k)) + transitions = list(idfa_string_from_flags(flags, n=n, k=k)) while True: yield tuple(transitions) try: next_idfa_string(transitions, flags, n=n, k=k) except StopIteration: try: - next_flags(flags, k=k) + icdfa_next_flags(flags, k=k) except StopIteration: break - transitions[:] = list(string_from_flags(flags, n=n, k=k)) + transitions[:] = list(idfa_string_from_flags(flags, n=n, k=k)) def iter_idfa_strings(k: int, n: int) -> Iterator[tuple[int, ...]]: diff --git a/sofic/automata/learning/__init__.py b/sofic/automata/learning/__init__.py index 8a1c4cc..88d9c5d 100644 --- a/sofic/automata/learning/__init__.py +++ b/sofic/automata/learning/__init__.py @@ -26,10 +26,10 @@ from sofic.automata.learning.observation import ObservationTable from sofic.automata.learning.papni import ( DyckAlphabet, + encode_dyck_samples, + encode_dyck_word, is_well_matched, learn_sofic_dyck_shift_papni, - papni_encode, - papni_encode_samples, sofic_dyck_shift_from_papni_dfa, ) from sofic.automata.learning.rpni import learn_dfa_rpni @@ -63,7 +63,7 @@ "learn_rfsa_from_language", "learn_rfsa_nlstar", "learn_sofic_dyck_shift_papni", - "papni_encode", - "papni_encode_samples", + "encode_dyck_word", + "encode_dyck_samples", "sofic_dyck_shift_from_papni_dfa", ] diff --git a/sofic/automata/learning/papni.py b/sofic/automata/learning/papni.py index 163a8af..c2251fc 100644 --- a/sofic/automata/learning/papni.py +++ b/sofic/automata/learning/papni.py @@ -15,8 +15,8 @@ "DyckAlphabet", "is_well_matched", "learn_sofic_dyck_shift_papni", - "papni_encode", - "papni_encode_samples", + "encode_dyck_word", + "encode_dyck_samples", "sofic_dyck_shift_from_papni_dfa", ] @@ -63,7 +63,7 @@ def is_well_matched(word: Sequence[Any], alphabet: DyckAlphabet) -> bool: return counter == 0 -def papni_encode(word: Sequence[Any], alphabet: DyckAlphabet) -> tuple[Any, ...]: +def encode_dyck_word(word: Sequence[Any], alphabet: DyckAlphabet) -> tuple[Any, ...]: """Convert a well-matched word to its stack-aware representation (PAPNI Alg. 2).""" if not is_well_matched(word, alphabet): raise ValueError("word is not well-matched") @@ -84,7 +84,7 @@ def papni_encode(word: Sequence[Any], alphabet: DyckAlphabet) -> tuple[Any, ...] return tuple(encoded) -def papni_encode_samples( +def encode_dyck_samples( samples: Sequence[Sequence[Any]], alphabet: DyckAlphabet, *, @@ -98,7 +98,7 @@ def papni_encode_samples( if drop_non_well_matched: continue raise ValueError(f"sample {seq!r} is not well-matched") - encoded.append(papni_encode(seq, alphabet)) + encoded.append(encode_dyck_word(seq, alphabet)) return encoded @@ -218,7 +218,7 @@ def _infer_matched_edges_from_traces( for word in traces: if not is_well_matched(word, alphabet): continue - encoded = papni_encode(word, alphabet) + encoded = encode_dyck_word(word, alphabet) dfa_state = initial config_state = initial stack: list[TransitionRef] = [] @@ -280,7 +280,7 @@ def learn_sofic_dyck_shift_papni( alphabet: DyckAlphabet, ) -> SoficDyckShift: """Learn a ``SoficDyckShift`` topology from labeled samples via PAPNI + RPNI.""" - encoded_positive = papni_encode_samples(positive, alphabet) + encoded_positive = encode_dyck_samples(positive, alphabet) if not encoded_positive: raise ValueError("no well-matched positive samples remain after PAPNI filtering") @@ -290,7 +290,7 @@ def learn_sofic_dyck_shift_papni( seq = tuple(word) if not is_well_matched(seq, alphabet): continue - encoded_negative.append(papni_encode(seq, alphabet)) + encoded_negative.append(encode_dyck_word(seq, alphabet)) dfa = learn_dfa_rpni(encoded_positive, encoded_negative) well_matched_positive = [tuple(word) for word in positive if is_well_matched(word, alphabet)] diff --git a/sofic/automata/transducer_operations.py b/sofic/automata/transducer_operations.py index 0bb528c..bc56090 100644 --- a/sofic/automata/transducer_operations.py +++ b/sofic/automata/transducer_operations.py @@ -15,7 +15,7 @@ from sofic.graph import ATTR_EMISSION, ATTR_OUTPUT, ATTR_PROB, ATTR_SYMBOL, EPSILON -def cartesian_product_gg( +def generator_product( generators: Sequence[HiddenMarkovModel], *, create_using: type[MealyHMM] | None = None, @@ -59,7 +59,7 @@ def cartesian_product_gg( return result -def cartesian_product_tt( +def transducer_product( transducers: Sequence[MealyMachine], *, create_using: type[MealyMachine] | None = None, @@ -100,7 +100,7 @@ def cartesian_product_tt( return result -def compose_tt( +def compose_transducers( transducers: Sequence[MealyMachine], *, complete: bool = True, @@ -109,18 +109,20 @@ def compose_tt( ) -> MealyMachine: """Serially compose transducers. - ``compose_tt((t0, t1))`` returns the transducer that feeds ``t0``'s output + ``compose_transducers((t0, t1))`` returns the transducer that feeds ``t0``'s output into ``t1``. State labels are tuples ordered like the input transducers. """ if not transducers: raise ValueError("at least one transducer is required") result = transducers[0].copy() for transducer in transducers[1:]: - result = _compose_pair_tt(result, transducer, complete=complete, create_using=create_using, normalize=normalize) + result = _compose_transducer_pair( + result, transducer, complete=complete, create_using=create_using, normalize=normalize + ) return result -def compose_tg( +def compose_transducer_generator( transducer: MealyMachine, generator: HiddenMarkovModel, *, @@ -191,7 +193,7 @@ def transduce_generator( create_using: type[MealyHMM] | None = None, ) -> MealyHMM: """Return the output-only generator induced by driving ``transducer`` with ``generator``.""" - return compose_tg( + return compose_transducer_generator( transducer, generator, complete=complete, @@ -201,7 +203,7 @@ def transduce_generator( ) -def _compose_pair_tt( +def _compose_transducer_pair( left: MealyMachine, right: MealyMachine, *, @@ -342,9 +344,9 @@ def _prob(data: dict[str, Any]) -> float: __all__ = [ "ERROR_STATE", "ERROR_SYMBOL", - "cartesian_product_gg", - "cartesian_product_tt", - "compose_tg", - "compose_tt", + "generator_product", + "transducer_product", + "compose_transducer_generator", + "compose_transducers", "transduce_generator", ] diff --git a/sofic/automata/transducers.py b/sofic/automata/transducers.py index ffaa28c..817a788 100644 --- a/sofic/automata/transducers.py +++ b/sofic/automata/transducers.py @@ -175,15 +175,15 @@ def output_machine(self, *, build: bool = False) -> Any: def compose(self, other: MealyMachine, **kwargs: Any) -> MealyMachine: """Return the serial composition ``other`` after this transducer.""" - from sofic.automata.transducer_operations import compose_tt + from sofic.automata.transducer_operations import compose_transducers - return compose_tt((self, other), **kwargs) + return compose_transducers((self, other), **kwargs) def joint_machine(self, generator: Any, **kwargs: Any) -> Any: """Return the joint input/output generator induced by ``generator``.""" - from sofic.automata.transducer_operations import compose_tg + from sofic.automata.transducer_operations import compose_transducer_generator - return compose_tg(self, generator, joint=True, **kwargs) + return compose_transducer_generator(self, generator, joint=True, **kwargs) def transduce_generator(self, generator: Any, **kwargs: Any) -> Any: """Return the output generator induced by driving this transducer.""" @@ -191,11 +191,11 @@ def transduce_generator(self, generator: Any, **kwargs: Any) -> Any: return transduce_generator(self, generator, **kwargs) - def to_sofic_relation(self) -> Any: - """Return the topological support as a product-alphabet sofic relation.""" - from sofic.shifts.sofic_relation import SoficRelation + def to_product_alphabet_shift(self) -> Any: + """Return the topological support as a :class:`~sofic.shifts.ProductAlphabetShift`.""" + from sofic.shifts.product_alphabet_shift import ProductAlphabetShift - return SoficRelation.from_transducer(self) + return ProductAlphabetShift.from_transducer(self) def to_textile_system(self) -> Any: """Return this transducer as a textile system (Nasu 1995).""" diff --git a/sofic/automata/vpa/__init__.py b/sofic/automata/vpa/__init__.py index 3e7112b..c58d1b7 100644 --- a/sofic/automata/vpa/__init__.py +++ b/sofic/automata/vpa/__init__.py @@ -4,7 +4,7 @@ from sofic.automata.vpa.canonical import CanonicalVisiblyPushdownAutomaton from sofic.automata.vpa.deterministic import DeterministicVisiblyPushdownAutomaton from sofic.automata.vpa.modular import ( - CallDrivenAutomaton, + ModularVisiblyPushdownAutomaton, MultipleEntryVisiblyPushdownAutomaton, SingleEntryVisiblyPushdownAutomaton, ) @@ -13,7 +13,7 @@ __all__ = [ "BOTTOM", - "CallDrivenAutomaton", + "ModularVisiblyPushdownAutomaton", "CanonicalVisiblyPushdownAutomaton", "DeterministicVisiblyPushdownAutomaton", "MultipleEntryVisiblyPushdownAutomaton", diff --git a/sofic/automata/vpa/modular.py b/sofic/automata/vpa/modular.py index ec6759e..2b41d21 100644 --- a/sofic/automata/vpa/modular.py +++ b/sofic/automata/vpa/modular.py @@ -19,7 +19,7 @@ ) -class CallDrivenAutomaton(DeterministicVisiblyPushdownAutomaton): +class ModularVisiblyPushdownAutomaton(DeterministicVisiblyPushdownAutomaton): """Deterministic modular VPA whose call target depends only on the call symbol.""" modules: dict[Hashable, frozenset[Hashable]] @@ -74,7 +74,7 @@ def minimize( call_partition: Mapping[Any, Hashable] | None = None, base_module: Hashable | None = None, call_entries: Mapping[Any, Hashable] | None = None, - ) -> CallDrivenAutomaton: + ) -> ModularVisiblyPushdownAutomaton: """Return the module-aware deterministic quotient as a CDA.""" return _minimize_modular_vpa( cls, @@ -139,7 +139,7 @@ def _validate_call_driven_transitions(self) -> None: ) -class MultipleEntryVisiblyPushdownAutomaton(CallDrivenAutomaton): +class MultipleEntryVisiblyPushdownAutomaton(ModularVisiblyPushdownAutomaton): """Modular VPA with multiple module entries and source-determined call pushes.""" entry_states: dict[Hashable, frozenset[Hashable]] @@ -212,7 +212,7 @@ def _validate_source_determined_pushes(self) -> None: self._require(existing == pushed, "MEVPA call push must depend only on the source state") -class SingleEntryVisiblyPushdownAutomaton(CallDrivenAutomaton): +class SingleEntryVisiblyPushdownAutomaton(ModularVisiblyPushdownAutomaton): """Modular VPA with one distinguished entry per non-base module.""" entry_states: dict[Hashable, Hashable] @@ -315,7 +315,7 @@ def _deterministic_view(vpa: VisiblyPushdownAutomaton) -> DeterministicVisiblyPu def _minimize_modular_vpa( - target_cls: type[CallDrivenAutomaton], + target_cls: type[ModularVisiblyPushdownAutomaton], vpa: VisiblyPushdownAutomaton, *, modules: Mapping[Hashable, Iterable[Hashable]] | None, @@ -379,7 +379,7 @@ def _minimize_modular_vpa( def _construct_modular_view( - target_cls: type[CallDrivenAutomaton], + target_cls: type[ModularVisiblyPushdownAutomaton], det: DeterministicVisiblyPushdownAutomaton, *, modules: Mapping[Hashable, frozenset[Hashable]], @@ -387,7 +387,7 @@ def _construct_modular_view( call_partition: Mapping[Any, Hashable], call_entries: Mapping[Any, Hashable], entry_states: Mapping[Hashable, Any], -) -> CallDrivenAutomaton: +) -> ModularVisiblyPushdownAutomaton: kwargs = { "input_alphabet": det.input_alphabet, "call_alphabet": det.call_alphabet, @@ -463,7 +463,7 @@ def _infer_entry_states( def _refine_modular_partition( - vpa: CallDrivenAutomaton, + vpa: ModularVisiblyPushdownAutomaton, modules: Mapping[Hashable, frozenset[Hashable]], ) -> list[frozenset[Hashable]]: partition: list[frozenset[Hashable]] = [] @@ -512,7 +512,7 @@ def _stack_context_groups( def _modular_state_signature( - vpa: CallDrivenAutomaton, + vpa: ModularVisiblyPushdownAutomaton, state: Hashable, block_of: Mapping[Hashable, Hashable], context_groups: Sequence[tuple[Any, tuple[Any, ...]]], @@ -557,8 +557,8 @@ def _modular_state_signature( def _quotient_modular_vpa( - target_cls: type[CallDrivenAutomaton], - source: CallDrivenAutomaton, + target_cls: type[ModularVisiblyPushdownAutomaton], + source: ModularVisiblyPushdownAutomaton, partition: Sequence[frozenset[Hashable]], *, modules: Mapping[Hashable, frozenset[Hashable]], diff --git a/sofic/automata/wheeler.py b/sofic/automata/wheeler.py index 45b28ee..e99717e 100644 --- a/sofic/automata/wheeler.py +++ b/sofic/automata/wheeler.py @@ -493,7 +493,7 @@ def _order_of(model: Any, symbol_key: Callable[[Any], Any] | None) -> tuple[Labe return graph, order -def minimum_wdfa( +def minimize_wheeler( dfa: Any, *, symbol_key: Callable[[Any], Any] | None = None, @@ -576,7 +576,7 @@ def step(automaton: Any, state: Hashable, symbol: Any) -> Hashable | None: return classes -def wnfa_to_wdfa( +def determinize_wheeler( nfa: Any, *, symbol_key: Callable[[Any], Any] | None = None, diff --git a/sofic/examples/epsilon_machines.py b/sofic/examples/epsilon_machines.py index 239039d..99d40d7 100644 --- a/sofic/examples/epsilon_machines.py +++ b/sofic/examples/epsilon_machines.py @@ -165,7 +165,7 @@ def golden_mean(p: float = 0.5) -> EpsilonMachine: al., arXiv:0905.3587, Fig.~4, see :func:`golden_mean_forward` and :func:`golden_mean_reverse`; for the Parry max-entropy measure on the same shift, see :func:`golden_mean_shift_parry`. cmpy's - :func:`~sofic.examples.processes.GoldenMean` is the ``0 <-> 1`` mirror + :func:`~sofic.examples.processes.golden_mean_forbid_00` is the ``0 <-> 1`` mirror (forbids ``00``) and equals :func:`golden_mean_forward`. """ if not 0.0 < p < 1.0: @@ -273,10 +273,10 @@ def restricted_golden_mean(k: int = 1) -> EpsilonMachine: """ if k < 1: raise ValueError("k must be >= 1") - from sofic.examples.processes import RestrictedGM + from sofic.examples.processes import restricted_gm states = {str(i): label for i, label in enumerate(sequential_labels(k + 1))} - return _relabel(RestrictedGM(k), symbols={"0": 0, "1": 1}, states=states) + return _relabel(restricted_gm(k), symbols={"0": 0, "1": 1}, states=states) def nemo_process(p: float = 0.5, q: float = 0.5) -> EpsilonMachine: @@ -313,7 +313,7 @@ def phase_slip_backtrack(p: float = 0.5, q: float = 0.5) -> EpsilonMachine: """Phase-Slip Backtrack (PSB) Process (``R=3``, ``k_chi=2``). James, Mahoney, Ellison & Crutchfield, arXiv:1010.5545, Fig.~2. A different - process from cmpy's :func:`~sofic.examples.processes.PSB`. + process from cmpy's :func:`~sofic.examples.processes.phase_slip_backtrack_cmpy`. """ if not 0.0 < p < 1.0 or not 0.0 < q < 1.0: raise ValueError("p and q must be in (0, 1)") @@ -348,7 +348,7 @@ def butterfly_process() -> EpsilonMachine: Mahoney et al., arXiv:0906.5099, Fig.~1. Each causal state emits every symbol with probability ``1/8``; synchronizing symbols ``2``--``7`` always reach the same causal state regardless of the source. Not cmpy's - :func:`~sofic.examples.processes.Butterfly` (two branches per state, ``h_mu = 1``). + :func:`~sofic.examples.processes.butterfly_two_branch` (two branches per state, ``h_mu = 1``). """ states = ("A", "B", "C", "D", "E") prob = 1.0 / 8.0 @@ -377,9 +377,9 @@ def butterfly_process() -> EpsilonMachine: def ellison_fig9_forward() -> EpsilonMachine: """Forward ε-machine from Ellison et al., arXiv:1107.2168, Fig.~9.""" - from sofic.examples.processes import IrreversibleTwoState + from sofic.examples.processes import irreversible_two_state - return _relabel(IrreversibleTwoState(0.5, 0.5), symbols={"0": 0, "1": 1, "2": 2}) + return _relabel(irreversible_two_state(0.5, 0.5), symbols={"0": 0, "1": 1, "2": 2}) def tent_map_misiurewicz_a(symbolic: bool = False): diff --git a/sofic/examples/processes.py b/sofic/examples/processes.py index ee4aa43..6610917 100644 --- a/sofic/examples/processes.py +++ b/sofic/examples/processes.py @@ -17,20 +17,16 @@ from sofic.automata.transducers import MealyMachine from sofic.examples._construction import _edge_machine, _relabel from sofic.examples.epsilon_machines import ( - alternating_biased_coins, bernoulli, even_process, + fair_coin, golden_mean_forward, - nemo_process, - noisy_random_phase_slip, ) from sofic.generators.base import QuasiStochasticModel from sofic.generators.epsilon_machine import EpsilonMachine from sofic.generators.mealy import MealyHMM from sofic.graph import ATTR_EMISSION, ATTR_OUTPUT, ATTR_PROB, ATTR_QUASIPROB, ATTR_SYMBOL -RecurrentEpsilonMachine = EpsilonMachine - _STR_BITS = {0: "0", 1: "1"} @@ -97,7 +93,7 @@ def _dirichlet(n: int, rng: Any = None) -> np.ndarray: def _compatible_machine_type(machine_type: Any, default: type[MealyHMM] = EpsilonMachine) -> type[MealyHMM]: if machine_type is None: return default - if machine_type in (EpsilonMachine, RecurrentEpsilonMachine, MealyHMM): + if machine_type in (EpsilonMachine, MealyHMM): return machine_type if isinstance(machine_type, str): lowered = machine_type.lower() @@ -108,13 +104,7 @@ def _compatible_machine_type(machine_type: Any, default: type[MealyHMM] = Epsilo return default -def ABC(p: float = 0.75, q: float = 0.25) -> EpsilonMachine: - if math.isclose(p, q): - return _from_string(f"A A 0 {p}; A A 1 {1 - p}", name="ABC Process") - return _relabel(alternating_biased_coins(1 - p, 1 - q), symbols=_STR_BITS, name="ABC Process") - - -def AFC(n: int) -> EpsilonMachine: +def afc(n: int) -> EpsilonMachine: if n < 1: raise ValueError("n >= 1 required") spec = "B B 0; " @@ -124,7 +114,7 @@ def AFC(n: int) -> EpsilonMachine: return _from_string(spec, name=f"Almost Fair Coin Process, order {n}") -def AFC2(n: int) -> EpsilonMachine: +def afc2(n: int) -> EpsilonMachine: if n < 1: raise ValueError("n >= 1 required") spec = "A1 B1 1; B1 A1 0; " @@ -133,7 +123,7 @@ def AFC2(n: int) -> EpsilonMachine: return _from_string(spec, name=f"Almost Fair Coin 2 Process, order {n}") -def BandMerging(machine_type: Any = MealyHMM) -> MealyHMM: +def band_merging(machine_type: Any = MealyHMM) -> MealyHMM: cls = _compatible_machine_type(machine_type, MealyHMM) if cls is EpsilonMachine: edges = [ @@ -156,7 +146,7 @@ def BandMerging(machine_type: Any = MealyHMM) -> MealyHMM: ) -def BeadsOnNecklace( +def beads_on_necklace( machine_type: Any = MealyHMM, beads: Sequence[Any] | Sequence[Sequence[Any]] = ("a", "b"), necklace: Sequence[Any] = ("0", "1", "3"), @@ -201,7 +191,7 @@ def BeadsOnNecklace( return _edge_machine(edges, machine_type=MealyHMM, name="Beads On Necklace", normalize=False) -def BeforeAfter(machine_type: Any = MealyHMM, style: str = "simple") -> MealyHMM: +def before_after(machine_type: Any = MealyHMM, style: str = "simple") -> MealyHMM: _require_machine_type(machine_type, MealyHMM) if style == "simple": edges = [("A", "A", "0", 0.5), ("A", "B", "1", 0.5), ("B", "B", "0", 0.5), ("B", "A", "2", 0.5)] @@ -222,33 +212,26 @@ def BeforeAfter(machine_type: Any = MealyHMM, style: str = "simple") -> MealyHMM ] else: raise ValueError("style must be 'simple' or 'cayley'") - return _edge_machine(edges, machine_type=MealyHMM, name="BeforeAfter", normalize=False) - - -def BiasedCoin(bias: float, machine_type: Any = EpsilonMachine) -> EpsilonMachine: - return _relabel(bernoulli(bias), machine_type=_compatible_machine_type(machine_type), name=f"Coin, p = {bias}") - - -def FairCoin(machine_type: Any = EpsilonMachine) -> EpsilonMachine: - return BiasedCoin(0.5, machine_type=machine_type) + return _edge_machine(edges, machine_type=MealyHMM, name="before_after", normalize=False) -def RandomBiasedCoin(machine_type: Any = EpsilonMachine, rng: np.random.Generator | None = None) -> EpsilonMachine: +def random_biased_coin(machine_type: Any = EpsilonMachine, rng: np.random.Generator | None = None) -> EpsilonMachine: generator = rng if rng is not None else np.random.default_rng() - return BiasedCoin(float(generator.random()), machine_type=machine_type) + bias = float(generator.random()) + return _relabel(bernoulli(bias), machine_type=_compatible_machine_type(machine_type), name=f"Coin, p = {bias}") -def IID(k: int | Sequence[Any], machine_type: Any = EpsilonMachine) -> EpsilonMachine: +def iid(k: int | Sequence[Any], machine_type: Any = EpsilonMachine) -> EpsilonMachine: alphabet = _as_alphabet(k) return _edge_machine( [("A", "A", symbol, 1.0) for symbol in alphabet], machine_type=_compatible_machine_type(machine_type), - name=f"IID({len(alphabet)})", + name=f"iid({len(alphabet)})", normalize=True, ) -def BinaryMarkovChain( +def binary_markov_chain( p: float = 0.4, q: float = 0.3, a: float | None = None, @@ -256,24 +239,24 @@ def BinaryMarkovChain( machine_type: Any = EpsilonMachine, branch: Any = None, ) -> MealyHMM: - if machine_type in (EpsilonMachine, RecurrentEpsilonMachine, None): - return BMC_eM(p, q) + if machine_type in (EpsilonMachine, None): + return bmc_em(p, q) if isinstance(machine_type, str): lowered = machine_type.lower() if lowered == "generative": - return BMC_gen(p, q, branch=branch) + return bmc_gen(p, q, branch=branch) if lowered == "parametrized": if a is None or b is None: - raise ValueError("a and b are required for parametrized BinaryMarkovChain") - return BMC_param(p, q, a, b) + raise ValueError("a and b are required for parametrized binary_markov_chain") + return bmc_param(p, q, a, b) if lowered == "lohr": - return BMC_lohr(p) + return bmc_lohr(p) raise NotImplementedError -def BMC_eM(p: float, q: float) -> EpsilonMachine: +def bmc_em(p: float, q: float) -> EpsilonMachine: if math.isclose(p, 1 - q): - return BiasedCoin(bias=p) + return _relabel(bernoulli(p), name=f"Coin, p = {p}") return _edge_machine( [("A", "A", "0", 1 - p), ("A", "B", "1", p), ("B", "A", "0", q), ("B", "B", "1", 1 - q)], machine_type=EpsilonMachine, @@ -282,7 +265,7 @@ def BMC_eM(p: float, q: float) -> EpsilonMachine: ) -def BMC_gen(p: float, q: float, branch: Any = None) -> MealyHMM: +def bmc_gen(p: float, q: float, branch: Any = None) -> MealyHMM: del branch if p == 1 and q == 1: spec = "A B 1 1.; B A 0 1." @@ -303,26 +286,26 @@ def BMC_gen(p: float, q: float, branch: Any = None) -> MealyHMM: return _from_string(spec, machine_type=MealyHMM, name="BinaryMarkovChain gen", normalize=False) -def _BMC_param_get_a_range(p: float, q: float) -> list[float]: +def _bmc_param_get_a_range(p: float, q: float) -> list[float]: return [0, min(q, 1 - p)] -def _BMC_param_get_b_range(p: float, q: float) -> list[float]: +def _bmc_param_get_b_range(p: float, q: float) -> list[float]: return [max(q, 1 - p), 1] -def _BMC_param_check_a_range(p: float, q: float, a: float) -> bool: - low, high = _BMC_param_get_a_range(p, q) +def _bmc_param_check_a_range(p: float, q: float, a: float) -> bool: + low, high = _bmc_param_get_a_range(p, q) return low <= a <= high -def _BMC_param_check_b_range(p: float, q: float, b: float) -> bool: - low, high = _BMC_param_get_b_range(p, q) +def _bmc_param_check_b_range(p: float, q: float, b: float) -> bool: + low, high = _bmc_param_get_b_range(p, q) return low <= b <= high -def BMC_param(p: float, q: float, a: float, b: float) -> MealyHMM: - if not _BMC_param_check_a_range(p, q, a) or not _BMC_param_check_b_range(p, q, b): +def bmc_param(p: float, q: float, a: float, b: float) -> MealyHMM: + if not _bmc_param_check_a_range(p, q, a) or not _bmc_param_check_b_range(p, q, b): raise ValueError("a or b is outside the allowed range") probs = [ a * (b + p - 1) / (b - a), @@ -347,7 +330,7 @@ def BMC_param(p: float, q: float, a: float, b: float) -> MealyHMM: return _edge_machine(edges, machine_type=MealyHMM, name="BinaryMarkovChain parametrized", normalize=False) -def BMC_lohr(p: float) -> MealyHMM: +def bmc_lohr(p: float) -> MealyHMM: if p > 0.5: raise ValueError("currently requires p <= 0.5") return _edge_machine( @@ -367,7 +350,7 @@ def BMC_lohr(p: float) -> MealyHMM: ) -def Butterfly() -> EpsilonMachine: +def butterfly_two_branch() -> EpsilonMachine: """cmpy's two-branch Butterfly (``h_mu = 1``); not :func:`~sofic.examples.butterfly_process` (8 symbols, ``h_mu = 3``).""" return _from_string( """ @@ -378,7 +361,7 @@ def Butterfly() -> EpsilonMachine: ) -def Cantor(machine_type: Any = MealyHMM) -> MealyHMM: +def cantor(machine_type: Any = MealyHMM) -> MealyHMM: _require_machine_type(machine_type, MealyHMM) edges = [ ("A", "A", "0", 0.55), @@ -391,7 +374,7 @@ def Cantor(machine_type: Any = MealyHMM) -> MealyHMM: return _edge_machine(edges, machine_type=MealyHMM, name="Cantor Process", normalize=False) -def CoupledGMPs( +def coupled_gmps( epsilon: float = 0.01, p: float = 0.5, alt: bool = True, machine_type: Any = EpsilonMachine ) -> EpsilonMachine: if epsilon < 0 or p < 0: @@ -436,7 +419,7 @@ def CoupledGMPs( ) -def UncoupledGMPs(p: float = 0.5, machine_type: Any = EpsilonMachine) -> EpsilonMachine: +def uncoupled_gmps(p: float = 0.5, machine_type: Any = EpsilonMachine) -> EpsilonMachine: if not 0 <= p <= 1: raise ValueError("p must be in [0, 1]") return _edge_machine( @@ -455,7 +438,7 @@ def UncoupledGMPs(p: float = 0.5, machine_type: Any = EpsilonMachine) -> Epsilon ) -def CyclicBranching(num_states: int, num_branchings: int, num_symbols: int = 2) -> MealyHMM: +def cyclic_branching(num_states: int, num_branchings: int, num_symbols: int = 2) -> MealyHMM: if num_branchings >= num_states: raise ValueError("number of branchings must be less than number of states") edges = [] @@ -475,8 +458,8 @@ def CyclicBranching(num_states: int, num_branchings: int, num_symbols: int = 2) ) -def Ehrenfest(p: float = 0.5, N: int = 5, machine_type: Any = EpsilonMachine) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) +def ehrenfest(p: float = 0.5, N: int = 5, machine_type: Any = EpsilonMachine) -> EpsilonMachine: + _require_machine_type(machine_type, EpsilonMachine) edges = [] for state in range(N + 1): edges.append((state, state, state, 1 - p)) @@ -484,42 +467,39 @@ def Ehrenfest(p: float = 0.5, N: int = 5, machine_type: Any = EpsilonMachine) -> edges.append((state, state + 1, state + 1, p * (N - state) / N)) for state in range(1, N + 1): edges.append((state, state - 1, state - 1, p * state / N)) - return _edge_machine(edges, machine_type=EpsilonMachine, name="Ehrenfest", normalize=False) - - -def Even(machine_type: Any = EpsilonMachine, bias: float = 0.5) -> EpsilonMachine: - if machine_type in (EpsilonMachine, RecurrentEpsilonMachine, None): - return _relabel(even_process(bias), symbols=_STR_BITS, name="Even Process") - raise NotImplementedError + return _edge_machine(edges, machine_type=EpsilonMachine, name="ehrenfest", normalize=False) -def RandomEven(machine_type: Any = EpsilonMachine, rng: np.random.Generator | None = None) -> EpsilonMachine: +def random_even(machine_type: Any = EpsilonMachine, rng: np.random.Generator | None = None) -> EpsilonMachine: return uniform_mealyhmm( - Even(), name="Random Even Process", create_using=_compatible_machine_type(machine_type), prng=rng + _relabel(even_process(), symbols=_STR_BITS), + name="Random Even Process", + create_using=_compatible_machine_type(machine_type), + prng=rng, ) -def EvenRedundant(machine_type: Any = EpsilonMachine, bias: float = 0.5) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) +def even_redundant(machine_type: Any = EpsilonMachine, bias: float = 0.5) -> EpsilonMachine: + _require_machine_type(machine_type, EpsilonMachine) return _from_string( f"A A 0 {bias}; A B 1 {1 - bias}; B C 1 1.; C C 0 {bias}; C D 1 {1 - bias}; D A 1 1.", name="Even Process (4-state)", ) -def ThreEven(machine_type: Any = EpsilonMachine) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) +def three_even(machine_type: Any = EpsilonMachine) -> EpsilonMachine: + _require_machine_type(machine_type, EpsilonMachine) return _from_string("A A 0 1; A B 1 1; A C 2 1; B A 1 1; C A 2 1;", name="ThreEven Process") -def Flower( +def flower( N: int = 4, M: int = 3, forward_dist: Sequence[float] | None = None, reverse_dists: np.ndarray | None = None, machine_type: Any = EpsilonMachine, ) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) + _require_machine_type(machine_type, EpsilonMachine) if N < 2 or M < 2: raise ValueError("N and M must be at least 2") if forward_dist is None: @@ -535,23 +515,23 @@ def Flower( return _edge_machine(edges, machine_type=EpsilonMachine, name="Flower Process", normalize=False) -def FourStateAlmostIID(delta: float = 0.2) -> EpsilonMachine: +def four_state_almost_iid(delta: float = 0.2) -> EpsilonMachine: delta = max(0.0, min(float(delta), 0.24)) p, q, r, s = 0.50 - 2 * delta, 0.50 - delta, 0.50 + delta, 0.50 + 2 * delta spec = f"A A 0 {p}; A B 1 {1 - p}; B C 0 {q}; B C 1 {1 - q}; C A 0 {r}; C D 1 {1 - r}; D A 0 {s}; D D 1 {1 - s};" return _from_string(spec, name="FourStateAlmostIID Process") -def Girvan_fig6b(machine_type: Any = EpsilonMachine, alpha: float = 0.5, pi: float = 0.4) -> EpsilonMachine: +def girvan_fig6b(machine_type: Any = EpsilonMachine, alpha: float = 0.5, pi: float = 0.4) -> EpsilonMachine: return _edge_machine( [("A", "A", "1", alpha), ("A", "P", "0", 1 - alpha), ("P", "A", "1", 1 - pi), ("P", "P", "0", pi)], machine_type=_compatible_machine_type(machine_type), - name="Girvan_fig6b", + name="girvan_fig6b", normalize=False, ) -def Girvan_fig6c( +def girvan_fig6c( machine_type: Any = EpsilonMachine, alpha: float = 0.5, pi: float = 0.4, rho: float = 0.3 ) -> EpsilonMachine: return _edge_machine( @@ -564,12 +544,12 @@ def Girvan_fig6c( ("R", "A", "1", 1 - rho), ], machine_type=_compatible_machine_type(machine_type), - name="Girvan_fig6c", + name="girvan_fig6c", normalize=False, ) -def Girvan_fig6d( +def girvan_fig6d( machine_type: Any = EpsilonMachine, alpha: float = 0.5, pi: float = 0.4, @@ -588,18 +568,18 @@ def Girvan_fig6d( ("I", "P", "0", 1 - iota), ], machine_type=_compatible_machine_type(machine_type), - name="Girvan_fig6d", + name="girvan_fig6d", normalize=False, ) -def GoldenMean(bias: float = 0.5, machine_type: Any = EpsilonMachine) -> EpsilonMachine: +def golden_mean_forbid_00(bias: float = 0.5, machine_type: Any = EpsilonMachine) -> EpsilonMachine: """Golden mean forbidding ``00``; :func:`~sofic.examples.golden_mean` is its ``0 <-> 1`` mirror (forbids ``11``).""" - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) + _require_machine_type(machine_type, EpsilonMachine) return _relabel(golden_mean_forward(1 - bias), symbols=_STR_BITS, name="Golden Mean Process") -def RestrictedGM(k: int) -> EpsilonMachine: +def restricted_gm(k: int) -> EpsilonMachine: if k <= 0: raise ValueError("minimum k is 1") spec = "0 0 1 0.5; 0 1 0 0.5;" @@ -609,7 +589,7 @@ def RestrictedGM(k: int) -> EpsilonMachine: return _from_string(spec, name=f"Restricted Golden Mean Process, k={k}") -def StretchedGM(k: int) -> EpsilonMachine: +def stretched_gm(k: int) -> EpsilonMachine: if k <= 0: raise ValueError("minimum k is 1") spec = "0 0 1 0.5; 0 1 0 0.5;" @@ -619,7 +599,7 @@ def StretchedGM(k: int) -> EpsilonMachine: return _from_string(spec, name=f"Stretched Golden Mean Process, k={k}") -def RNGM(R: int, N: int, p: float = 0.5) -> EpsilonMachine: +def rn_gm(R: int, N: int, p: float = 0.5) -> EpsilonMachine: if R <= 0 or N <= 0 or R < N: raise ValueError("requires 1 <= N <= R") spec = f"0 0 1 {p}; 0 1 0 {1 - p};" @@ -631,7 +611,7 @@ def RNGM(R: int, N: int, p: float = 0.5) -> EpsilonMachine: return _from_string(spec, name=f"R-N Golden Mean Process, R={R} N={N}") -def RkGM(R: int, k: int, p: float = 0.5) -> EpsilonMachine: +def rk_gm(R: int, k: int, p: float = 0.5) -> EpsilonMachine: if R <= 0 or k <= 0: raise ValueError("R and k must be positive") spec = f"0 0 1 {p}; 0 1 0 {1 - p};" @@ -643,13 +623,16 @@ def RkGM(R: int, k: int, p: float = 0.5) -> EpsilonMachine: return _from_string(spec, name=f"R-k Golden Mean Process, R={R} k={k}") -def RandomGoldenMean(machine_type: Any = EpsilonMachine, rng: np.random.Generator | None = None) -> EpsilonMachine: +def random_golden_mean(machine_type: Any = EpsilonMachine, rng: np.random.Generator | None = None) -> EpsilonMachine: return uniform_mealyhmm( - GoldenMean(), name="Random Golden Mean Process", create_using=_compatible_machine_type(machine_type), prng=rng + golden_mean_forbid_00(), + name="Random Golden Mean Process", + create_using=_compatible_machine_type(machine_type), + prng=rng, ) -def GoldenMeanGHMM() -> QuasiStochasticModel: +def golden_mean_ghmm() -> QuasiStochasticModel: q = QuasiStochasticModel(initial_quasidistribution={"A": 2 / 3, "B": 1 / 3}) q.observation_alphabet = frozenset({"0", "1"}) q.name = "Golden Mean Process" @@ -667,7 +650,7 @@ def GoldenMeanGHMM() -> QuasiStochasticModel: return q -def NonunifilarGoldenMean(bias: float = 0.5, free: float = 2 / 3) -> MealyHMM: +def nonunifilar_golden_mean(bias: float = 0.5, free: float = 2 / 3) -> MealyHMM: pGM = bias pA = 1 / (1 + pGM) pB = pGM / (1 + pGM) @@ -695,7 +678,7 @@ def NonunifilarGoldenMean(bias: float = 0.5, free: float = 2 / 3) -> MealyHMM: return _edge_machine(edges, machine_type=MealyHMM, name=f"Nonunifilar Golden Mean, p = {bias:.2f}", normalize=False) -def IrreversibleTwoState(p: float = 0.5, q: float = 0.5, machine_type: Any = EpsilonMachine) -> EpsilonMachine: +def irreversible_two_state(p: float = 0.5, q: float = 0.5, machine_type: Any = EpsilonMachine) -> EpsilonMachine: return _edge_machine( [("A", "A", "0", p), ("A", "B", "1", 1 - p), ("B", "B", "1", q), ("B", "A", "2", 1 - q)], machine_type=_compatible_machine_type(machine_type), @@ -704,8 +687,8 @@ def IrreversibleTwoState(p: float = 0.5, q: float = 0.5, machine_type: Any = Eps ) -def Ising(machine_type: Any = EpsilonMachine, J: float = 1.0, B: float = 0.3, T: float = 1.0) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) +def ising(machine_type: Any = EpsilonMachine, J: float = 1.0, B: float = 0.3, T: float = 1.0) -> EpsilonMachine: + _require_machine_type(machine_type, EpsilonMachine) beta = 1.0 / T rad = (np.sinh(beta * B) ** 2 + np.exp(-4 * beta * J)) ** 0.5 two_p = 1.0 - (2 * np.exp(-4 * beta * J)) / (rad * (np.cosh(beta * B) + rad)) @@ -722,10 +705,10 @@ def Ising(machine_type: Any = EpsilonMachine, J: float = 1.0, B: float = 0.3, T: ) -def Lollipop( +def lollipop( N: int, M: int, p: float = 0.5, q: float = 0.5, r: float = 0.1, machine_type: Any = EpsilonMachine ) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) + _require_machine_type(machine_type, EpsilonMachine) hns = [str(ind) for ind in range(N)] sns = [str(ind) for ind in range(N, N + 2 * (M - 1) + 1)] edges = [] @@ -742,18 +725,18 @@ def Lollipop( edges.append((sns[2 * (M - 1) - 1], sns[last], "1", 1 - r)) edges.append((sns[2 * (M - 1) - 1], sns[last], "0", r)) edges.append((sns[last], hns[0], "2", 1)) - return _edge_machine(edges, machine_type=EpsilonMachine, name="Lollipop", normalize=False) + return _edge_machine(edges, machine_type=EpsilonMachine, name="lollipop", normalize=False) -def LogicMachine( +def logic_machine( logic: str, bias: float | Sequence[float] = 0.5, noise: float | Sequence[float] = 0.5, minimize: bool = True ) -> MealyHMM: del minimize - if logic == "RRX": - return RRX(machine_type=MealyHMM) - if logic == "Rn1C": - return Rn1C(noise=float(np.atleast_1d(noise)[0]), bias=float(np.atleast_1d(bias)[0])) - raise NotImplementedError("LogicMachine currently supports the common RRX and Rn1C constructors") + if logic == "rrx": + return rrx(machine_type=MealyHMM) + if logic == "rn1c": + return rn1c(noise=float(np.atleast_1d(noise)[0]), bias=float(np.atleast_1d(bias)[0])) + raise NotImplementedError("logic_machine currently supports the common 'RRX' and 'Rn1C' logics") def markov_skeleton(R: int, k: int | Sequence[Any], join: bool | None = None) -> MealyHMM: @@ -777,7 +760,7 @@ def markov_skeleton(R: int, k: int | Sequence[Any], join: bool | None = None) -> return _edge_machine(edges, machine_type=MealyHMM, name=f"Order-{R} Markov skeleton") -def Misiurewicz(machine_type: Any = MealyHMM) -> MealyHMM: +def misiurewicz(machine_type: Any = MealyHMM) -> MealyHMM: _require_machine_type(machine_type, MealyHMM) return _edge_machine( [ @@ -795,7 +778,7 @@ def Misiurewicz(machine_type: Any = MealyHMM) -> MealyHMM: ) -def MisiurewiczSimplified(machine_type: Any = MealyHMM) -> MealyHMM: +def misiurewicz_simplified(machine_type: Any = MealyHMM) -> MealyHMM: _require_machine_type(machine_type, MealyHMM) return _edge_machine( [ @@ -813,7 +796,7 @@ def MisiurewiczSimplified(machine_type: Any = MealyHMM) -> MealyHMM: ) -def MisiurewiczUniform(machine_type: Any = MealyHMM) -> MealyHMM: +def misiurewicz_uniform(machine_type: Any = MealyHMM) -> MealyHMM: _require_machine_type(machine_type, MealyHMM) return _edge_machine( [ @@ -831,29 +814,24 @@ def MisiurewiczUniform(machine_type: Any = MealyHMM) -> MealyHMM: ) -def Multiple3(machine_type: Any = MealyHMM, bias: float = 0.5) -> MealyHMM: - return MultipleN(3, machine_type=machine_type, bias=bias) +def multiple3(machine_type: Any = MealyHMM, bias: float = 0.5) -> MealyHMM: + return multiple_n(3, machine_type=machine_type, bias=bias) -def Multiple4(machine_type: Any = MealyHMM, bias: float = 0.5) -> MealyHMM: - return MultipleN(4, machine_type=machine_type, bias=bias) +def multiple4(machine_type: Any = MealyHMM, bias: float = 0.5) -> MealyHMM: + return multiple_n(4, machine_type=machine_type, bias=bias) -def MultipleN(n: int, machine_type: Any = MealyHMM, bias: float = 0.5) -> MealyHMM: - _require_machine_type(machine_type, MealyHMM, EpsilonMachine, RecurrentEpsilonMachine) +def multiple_n(n: int, machine_type: Any = MealyHMM, bias: float = 0.5) -> MealyHMM: + _require_machine_type(machine_type, MealyHMM, EpsilonMachine) edges = [(0, 0, "0", bias), (0, 1, "1", 1 - bias)] for x in range(1, int(n)): edges.append((x, 0 if x == n - 1 else x + 1, "1", 1)) - cls = EpsilonMachine if machine_type in (EpsilonMachine, RecurrentEpsilonMachine) else MealyHMM + cls = EpsilonMachine if machine_type is EpsilonMachine else MealyHMM return _edge_machine(edges, machine_type=cls, name=f"'Multiples of {n}' Process", normalize=False) -def Nemo(machine_type: Any = EpsilonMachine, p: float = 0.5, q: float = 0.5) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) - return _relabel(nemo_process(p, q), symbols=_STR_BITS, name="Nemo Process") - - -def NemoRedundant(machine_type: Any = MealyHMM, p: float = 0.5, q: float = 0.5) -> MealyHMM: +def nemo_redundant(machine_type: Any = MealyHMM, p: float = 0.5, q: float = 0.5) -> MealyHMM: _require_machine_type(machine_type, MealyHMM) return _from_string( f""" @@ -865,7 +843,7 @@ def NemoRedundant(machine_type: Any = MealyHMM, p: float = 0.5, q: float = 0.5) ) -def NoisyPeriod2(noise: float = 0.5) -> EpsilonMachine: +def noisy_period2(noise: float = 0.5) -> EpsilonMachine: return _edge_machine( [("A", "B", "0", 1), ("B", "A", "0", noise), ("B", "A", "1", 1 - noise)], machine_type=EpsilonMachine, @@ -874,12 +852,8 @@ def NoisyPeriod2(noise: float = 0.5) -> EpsilonMachine: ) -def NRPS() -> EpsilonMachine: - return noisy_random_phase_slip() - - -def Odd(machine_type: Any = EpsilonMachine, bias1: float = 0.5, bias2: float = 0.5) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) +def odd(machine_type: Any = EpsilonMachine, bias1: float = 0.5, bias2: float = 0.5) -> EpsilonMachine: + _require_machine_type(machine_type, EpsilonMachine) edges = [ ("A", "A", "0", bias1), ("A", "B", "1", 1 - bias1), @@ -890,7 +864,7 @@ def Odd(machine_type: Any = EpsilonMachine, bias1: float = 0.5, bias2: float = 0 return _edge_machine(edges, machine_type=EpsilonMachine, name="Odd Process", normalize=False) -def OddGHMM(variant: int = 1) -> QuasiStochasticModel: +def odd_ghmm(variant: int = 1) -> QuasiStochasticModel: matrices = ( {"0": [[0.5, 0, 0], [1.0, 0, 0], [1.0, 0, 0]], "1": [[0.5, -0.5, 0.5], [0, 0, 0], [0.5, 0, -0.5]]} if variant == 1 @@ -913,23 +887,23 @@ def OddGHMM(variant: int = 1) -> QuasiStochasticModel: return q -def EvenOdd(machine_type: Any = EpsilonMachine) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) +def even_odd(machine_type: Any = EpsilonMachine) -> EpsilonMachine: + _require_machine_type(machine_type, EpsilonMachine) return _from_string("A B 0 0.5; B A 0 1; A C 1 0.5; C B 0 0.5; C D 1 0.5; D C 1 1;", name="EvenOdd Process") -def ThreEvenOdd(machine_type: Any = EpsilonMachine) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) +def three_even_odd(machine_type: Any = EpsilonMachine) -> EpsilonMachine: + _require_machine_type(machine_type, EpsilonMachine) return _from_string( "A A 0 1; A B 1 1; B A 1 1; A C 2 1; C A 0 1; C B 1 1; C D 2 1; D C 2 1;", name="ThreEvenOdd Process" ) -def Period(P: int) -> MealyHMM: - return Periodic("0" * (P - 1) + "1") +def period(P: int) -> MealyHMM: + return periodic("0" * (P - 1) + "1") -def Periodic(word: Sequence[Any], reduce: bool = True) -> MealyHMM: +def periodic(word: Sequence[Any], reduce: bool = True) -> MealyHMM: base = _word_period(word) if reduce else word edges = [] for i, symbol in enumerate(base): @@ -939,52 +913,52 @@ def Periodic(word: Sequence[Any], reduce: bool = True) -> MealyHMM: ) -def Period1(machine_type: Any = MealyHMM) -> MealyHMM: - return Periodic("1") if machine_type is MealyHMM else _from_string("A A 1 1", name="Period-1 Process") +def period1(machine_type: Any = MealyHMM) -> MealyHMM: + return periodic("1") if machine_type is MealyHMM else _from_string("A A 1 1", name="Period-1 Process") -def Period2(machine_type: Any = MealyHMM) -> MealyHMM: - return Periodic("01") if machine_type is MealyHMM else _from_string("A B 0 1; B A 1 1", name="Period-2 Process") +def period2(machine_type: Any = MealyHMM) -> MealyHMM: + return periodic("01") if machine_type is MealyHMM else _from_string("A B 0 1; B A 1 1", name="Period-2 Process") -def Period4(machine_type: Any = MealyHMM) -> MealyHMM: +def period4(machine_type: Any = MealyHMM) -> MealyHMM: return ( - Periodic("1110") + periodic("1110") if machine_type is MealyHMM else _from_string("A B 1 1; B C 1 1; C D 1 1; D A 0 1", name="Period-4 Process") ) -def Period7(machine_type: Any = MealyHMM) -> MealyHMM: +def period7(machine_type: Any = MealyHMM) -> MealyHMM: if machine_type is not MealyHMM: raise NotImplementedError - return Periodic("10101110") + return periodic("10101110") -def Period8(machine_type: Any = MealyHMM) -> MealyHMM: +def period8(machine_type: Any = MealyHMM) -> MealyHMM: if machine_type is not MealyHMM: raise NotImplementedError - return Periodic("10101110") + return periodic("10101110") -def Period12(machine_type: Any = MealyHMM) -> MealyHMM: +def period12(machine_type: Any = MealyHMM) -> MealyHMM: if machine_type is not MealyHMM: raise NotImplementedError - return Periodic("101011101110") + return periodic("101011101110") -def Period16(machine_type: Any = MealyHMM) -> MealyHMM: +def period16(machine_type: Any = MealyHMM) -> MealyHMM: if machine_type is not MealyHMM: raise NotImplementedError - return Periodic("1010111011101110") + return periodic("1010111011101110") -def PerturbedCoin(p: float = 0.2, q: float | None = None, machine_type: Any = EpsilonMachine) -> MealyHMM: +def perturbed_coin(p: float = 0.2, q: float | None = None, machine_type: Any = EpsilonMachine) -> MealyHMM: if p == 0.5: - return FairCoin() + return fair_coin() if q is None: q = p - if machine_type in (EpsilonMachine, RecurrentEpsilonMachine, None): + if machine_type in (EpsilonMachine, None): return _edge_machine( [("A", "A", "0", 1 - p), ("A", "B", "1", p), ("B", "A", "0", q), ("B", "B", "1", 1 - q)], machine_type=EpsilonMachine, @@ -995,16 +969,16 @@ def PerturbedCoin(p: float = 0.2, q: float | None = None, machine_type: Any = Ep if lowered in {"lohr", "generative"}: if q != p: raise ValueError("Lohr/generative variants only support q == p") - return BMC_lohr(p) if lowered == "lohr" else BMC_gen(p, p) + return bmc_lohr(p) if lowered == "lohr" else bmc_gen(p, p) raise NotImplementedError(f"cannot build machine type {machine_type!r}") -def PSB() -> EpsilonMachine: +def phase_slip_backtrack_cmpy() -> EpsilonMachine: """cmpy's Phase-Slip Backtrack; a different process from :func:`~sofic.examples.epsilon_machines.phase_slip_backtrack`.""" return _from_string("A B 1; A D 0; B B 1; B C 0; C D 0; D A 1", name="Phase-Slip Backtrack") -def RIP(p: float = 0.5, q: float = 0.5, reverse: bool = False) -> EpsilonMachine: +def rip(p: float = 0.5, q: float = 0.5, reverse: bool = False) -> EpsilonMachine: if reverse: spec = f"D E 1 {1 - p * q}; D F 0 {p * q}; E D 1 {(1 - p) / (1 - p * q)}; E G 0 {p * (1 - q) / (1 - p * q)}; F G 0 1; G D 1 1" else: @@ -1012,7 +986,7 @@ def RIP(p: float = 0.5, q: float = 0.5, reverse: bool = False) -> EpsilonMachine return _from_string(spec, name="Random Insertion Process") -def Rn1C(noise: float = 0.5, bias: float = 0.5) -> EpsilonMachine: +def rn1c(noise: float = 0.5, bias: float = 0.5) -> EpsilonMachine: return _edge_machine( [ ("A", "B", "0", bias), @@ -1027,8 +1001,8 @@ def Rn1C(noise: float = 0.5, bias: float = 0.5) -> EpsilonMachine: ) -def Rn1N(machine_type: Any = EpsilonMachine, bias: float = 0.5, noise: float = 0.1) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) +def rn1n(machine_type: Any = EpsilonMachine, bias: float = 0.5, noise: float = 0.1) -> EpsilonMachine: + _require_machine_type(machine_type, EpsilonMachine) return _edge_machine( [ ("A", "B", "0", bias), @@ -1043,8 +1017,8 @@ def Rn1N(machine_type: Any = EpsilonMachine, bias: float = 0.5, noise: float = 0 ) -def RRX(machine_type: Any = EpsilonMachine) -> MealyHMM: - if machine_type in (EpsilonMachine, RecurrentEpsilonMachine, None): +def rrx(machine_type: Any = EpsilonMachine) -> MealyHMM: + if machine_type in (EpsilonMachine, None): edges = [ ("S", "0", "0", 0.5), ("S", "1", "1", 0.5), @@ -1055,7 +1029,7 @@ def RRX(machine_type: Any = EpsilonMachine) -> MealyHMM: ("00|11", "S", "0", 1), ("01|10", "S", "1", 1), ] - return _edge_machine(edges, machine_type=EpsilonMachine, name="RRX", normalize=False) + return _edge_machine(edges, machine_type=EpsilonMachine, name="rrx", normalize=False) if machine_type is MealyHMM: return _edge_machine( [ @@ -1077,19 +1051,19 @@ def RRX(machine_type: Any = EpsilonMachine) -> MealyHMM: raise NotImplementedError -def RRXRO(machine_type: Any = EpsilonMachine) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) +def rrxro(machine_type: Any = EpsilonMachine) -> EpsilonMachine: + _require_machine_type(machine_type, EpsilonMachine) return _from_string( """ S 0 0 .5; S 1 1 .5; 0 00 0 .5; 0 01 1 .5; 1 10 0 .5; 1 11 1 .5; 01 orTrue 1 1; 10 orTrue 1 1; 00 000 0 1; 000 orFalse 0 .5; 000 orTrue 1 .5; 11 110 0 1; 110 orFalse 0 .5; 110 orTrue 1 .5; orFalse S 0 1; orTrue S 1 1; """, - name="RRXRO", + name="rrxro", ) -def SNS(machine_type: Any = MealyHMM) -> MealyHMM: +def sns(machine_type: Any = MealyHMM) -> MealyHMM: _require_machine_type(machine_type, MealyHMM) return _edge_machine( [("A", "A", "1", 0.5), ("A", "B", "1", 0.5), ("B", "A", "0", 0.5), ("B", "B", "1", 0.5)], @@ -1099,10 +1073,10 @@ def SNS(machine_type: Any = MealyHMM) -> MealyHMM: ) -def ThreeHundred( +def three_hundred( machine_type: Any = EpsilonMachine, biases: tuple[float, float, float] = (0.5, 0.5, 0.5) ) -> EpsilonMachine: - _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) + _require_machine_type(machine_type, EpsilonMachine) p, q, r = biases spec = f"A B 0 {p}; A C 1 {1 - p}; B D 0 1; C E 0 1; D F 0 {q}; D F 1 {1 - q}; E A 0 1; F A 0 {r}; F A 1 {1 - r};" return _from_string(spec, name="ThreeHundred Process") @@ -1199,24 +1173,24 @@ def _transducer( return machine -def GMtoEven(bias: float = 0.5, create_using: Any = None) -> MealyMachine: +def gm_to_even(bias: float = 0.5, create_using: Any = None) -> MealyMachine: del bias, create_using return _transducer([("A", "A", "1", "0", 1), ("A", "B", "0", "1", 1), ("B", "A", "1", "1", 1)], name="GM to Even") -def RCT(bias: float = 0.5, create_using: Any = None) -> MealyMachine: +def rct(bias: float = 0.5, create_using: Any = None) -> MealyMachine: del create_using return _transducer( [("A", "B", "0", "0", bias), ("A", "C", "0", "1", bias), ("B", "A", "0", "0", 1), ("C", "A", "1", "1", 1)] ) -def BitFlip(create_using: Any = None) -> MealyMachine: +def bit_flip(create_using: Any = None) -> MealyMachine: del create_using return _transducer([("A", "A", "0", "1", 1), ("A", "A", "1", "0", 1)]) -def FlipEveryOther(parity: str = "Even", create_using: Any = None) -> MealyMachine: +def flip_every_other(parity: str = "Even", create_using: Any = None) -> MealyMachine: del create_using initial = "A" if parity == "Even" else "B" return _transducer( @@ -1225,7 +1199,7 @@ def FlipEveryOther(parity: str = "Even", create_using: Any = None) -> MealyMachi ) -def Delay(length: int = 2, symbols: int | Sequence[Any] = 2, create_using: Any = None) -> MealyMachine: +def delay(length: int = 2, symbols: int | Sequence[Any] = 2, create_using: Any = None) -> MealyMachine: del create_using alphabet = tuple(map(str, _as_alphabet(symbols))) edges = [] @@ -1237,7 +1211,7 @@ def Delay(length: int = 2, symbols: int | Sequence[Any] = 2, create_using: Any = return _transducer(edges) -def TwoPerm(symbols: int | Sequence[Any] = 2, create_using: Any = None) -> MealyMachine: +def two_perm(symbols: int | Sequence[Any] = 2, create_using: Any = None) -> MealyMachine: del create_using alphabet = tuple(map(str, _as_alphabet(symbols))) edges = [] @@ -1250,28 +1224,28 @@ def TwoPerm(symbols: int | Sequence[Any] = 2, create_using: Any = None) -> Mealy return _transducer(edges, initial="??") -def BinaryChannel(p: float = 0.0, q: float = 0.0, create_using: Any = None) -> MealyMachine: +def binary_channel(p: float = 0.0, q: float = 0.0, create_using: Any = None) -> MealyMachine: del create_using return _transducer( [("A", "A", "0", "0", 1 - p), ("A", "A", "0", "1", p), ("A", "A", "1", "1", 1 - q), ("A", "A", "1", "0", q)] ) -def SlidingNOR(create_using: Any = None) -> MealyMachine: +def sliding_nor(create_using: Any = None) -> MealyMachine: del create_using return _transducer( [("A", "A", "0", "1", 1), ("A", "B", "1", "0", 1), ("B", "A", "0", "0", 1), ("B", "B", "1", "0", 1)] ) -def Parity(create_using: Any = None) -> MealyMachine: +def parity(create_using: Any = None) -> MealyMachine: del create_using return _transducer( [("A", "A", "0", "0", 1), ("A", "B", "1", "1", 1), ("B", "A", "0", "0", 1), ("B", "A", "1", "0", 1)] ) -def GME(create_using: Any = None) -> MealyMachine: +def gme(create_using: Any = None) -> MealyMachine: del create_using return _transducer( [ @@ -1285,93 +1259,87 @@ def GME(create_using: Any = None) -> MealyMachine: ) -def BinaryChannelET(p: float = 0.1, q: float = 0.2) -> Any: +def binary_channel_et(p: float = 0.1, q: float = 0.2) -> Any: """Memoryless binary channel as its minimal (single-state) ε-transducer.""" from sofic.generators.epsilon_transducer import EpsilonTransducer - return EpsilonTransducer.from_channel(BinaryChannel(p, q)) + return EpsilonTransducer.from_channel(binary_channel(p, q)) -def GMtoEvenET() -> Any: +def gm_to_even_et() -> Any: """Golden-mean-to-even map as an ε-transducer (Barnett & Crutchfield 2015).""" from sofic.generators.epsilon_transducer import EpsilonTransducer - return EpsilonTransducer.from_channel(GMtoEven()) + return EpsilonTransducer.from_channel(gm_to_even()) processes = [ - "ABC", - "BandMerging", - "BeadsOnNecklace", - "BeforeAfter", - "BiasedCoin", - "BinaryMarkovChain", - "Butterfly", - "Cantor", - "CoupledGMPs", - "GoldenMean", - "IrreversibleTwoState", - "NonunifilarGoldenMean", - "Ehrenfest", - "Even", - "EvenOdd", - "EvenRedundant", - "FairCoin", - "Flower", - "FourStateAlmostIID", - "Girvan_fig6b", - "Girvan_fig6c", - "Girvan_fig6d", - "Ising", - "Lollipop", - "Misiurewicz", - "MisiurewiczSimplified", - "MisiurewiczUniform", - "Multiple3", - "Multiple4", - "Nemo", - "NemoRedundant", - "NoisyPeriod2", - "NRPS", - "Odd", - "Period1", - "Period2", - "Period4", - "Period7", - "Period8", - "Period12", - "Period16", - "PerturbedCoin", - "PSB", - "RandomBiasedCoin", - "RandomGoldenMean", - "RandomEven", - "RIP", - "Rn1C", - "Rn1N", - "RRX", - "RRXRO", - "SNS", - "ThreeHundred", - "ThreEven", - "ThreEvenOdd", + "band_merging", + "beads_on_necklace", + "before_after", + "binary_markov_chain", + "butterfly_two_branch", + "cantor", + "coupled_gmps", + "golden_mean_forbid_00", + "irreversible_two_state", + "nonunifilar_golden_mean", + "ehrenfest", + "even_odd", + "even_redundant", + "flower", + "four_state_almost_iid", + "girvan_fig6b", + "girvan_fig6c", + "girvan_fig6d", + "ising", + "lollipop", + "misiurewicz", + "misiurewicz_simplified", + "misiurewicz_uniform", + "multiple3", + "multiple4", + "nemo_redundant", + "noisy_period2", + "odd", + "period1", + "period2", + "period4", + "period7", + "period8", + "period12", + "period16", + "perturbed_coin", + "phase_slip_backtrack_cmpy", + "random_biased_coin", + "random_golden_mean", + "random_even", + "rip", + "rn1c", + "rn1n", + "rrx", + "rrxro", + "sns", + "three_hundred", + "three_even", + "three_even_odd", ] -nonergodic_generators = ["UncoupledGMPs"] +nonergodic_generators = ["uncoupled_gmps"] transducers = [ - "GMtoEven", - "RCT", - "BitFlip", - "FlipEveryOther", - "Delay", - "TwoPerm", - "BinaryChannel", - "SlidingNOR", - "Parity", - "GME", + "gm_to_even", + "rct", + "bit_flip", + "flip_every_other", + "delay", + "two_perm", + "binary_channel", + "sliding_nor", + "parity", + "gme", ] epsilon_transducers = [ - "BinaryChannelET", - "GMtoEvenET", + "binary_channel_et", + "gm_to_even_et", ] process_list = [globals()[name] for name in processes] @@ -1381,25 +1349,25 @@ def GMtoEvenET() -> Any: __all__ = processes + nonergodic_generators + transducers + epsilon_transducers __all__ += [ - "AFC", - "AFC2", - "BMC_eM", - "BMC_gen", - "BMC_param", - "BMC_lohr", - "CyclicBranching", - "GoldenMeanGHMM", - "IID", - "LogicMachine", + "afc", + "afc2", + "bmc_em", + "bmc_gen", + "bmc_param", + "bmc_lohr", + "cyclic_branching", + "golden_mean_ghmm", + "iid", + "logic_machine", "markov_skeleton", - "MultipleN", - "OddGHMM", - "Period", - "Periodic", - "RestrictedGM", - "StretchedGM", - "RkGM", - "RNGM", + "multiple_n", + "odd_ghmm", + "period", + "periodic", + "restricted_gm", + "stretched_gm", + "rk_gm", + "rn_gm", "uniform_mealyhmm", "uniform_mealymc", "processes", @@ -1408,8 +1376,8 @@ def GMtoEvenET() -> Any: "transducer_list", "epsilon_transducers", "epsilon_transducer_list", - "_BMC_param_get_a_range", - "_BMC_param_get_b_range", - "_BMC_param_check_a_range", - "_BMC_param_check_b_range", + "_bmc_param_get_a_range", + "_bmc_param_get_b_range", + "_bmc_param_check_a_range", + "_bmc_param_check_b_range", ] diff --git a/sofic/generators/base.py b/sofic/generators/base.py index 6b37412..bc28e17 100644 --- a/sofic/generators/base.py +++ b/sofic/generators/base.py @@ -112,15 +112,15 @@ def log_likelihood(self, observations: Sequence[Any]) -> float: return log_likelihood(self, observations) - def forward(self, observations: Sequence[Any], *, scaled: bool = False) -> np.ndarray: + def forward(self, observations: Sequence[Any], *, normalize: bool = False) -> np.ndarray: from sofic.inference.hmm import forward - return forward(self, observations, scaled=scaled) + return forward(self, observations, normalize=normalize) - def backward(self, observations: Sequence[Any], *, scaled: bool = False) -> np.ndarray: + def backward(self, observations: Sequence[Any], *, normalize: bool = False) -> np.ndarray: from sofic.inference.hmm import backward - return backward(self, observations, scaled=scaled) + return backward(self, observations, normalize=normalize) def viterbi(self, observations: Sequence[Any]) -> list[Hashable]: from sofic.inference.hmm import viterbi @@ -192,16 +192,16 @@ def entropy_rate(self) -> float: return entropy_rate_hmm(self) - def joint_block_distribution(self, history_length: int = 1) -> Any: + def joint_block_distribution(self, block_length: int = 2) -> Any: from sofic.generators.measures import joint_block_distribution - return joint_block_distribution(self, history_length=history_length) + return joint_block_distribution(self, block_length=block_length) def words_of_length(self, length: int) -> dict[tuple[Any, ...], float]: """Return observed words of ``length`` and their probabilities.""" - from sofic.generators.words import hmm_words_of_length + from sofic.generators.words import _hmm_words_of_length - return hmm_words_of_length(self, length) + return _hmm_words_of_length(self, length) def word_probability( self, @@ -210,9 +210,9 @@ def word_probability( start: Hashable | Mapping[Hashable, float] | Sequence[float] | np.ndarray | None = None, ) -> float: """Return the probability of an observed finite word.""" - from sofic.generators.words import hmm_word_probability + from sofic.generators.words import _hmm_word_probability - return hmm_word_probability(self, word, start=start) + return _hmm_word_probability(self, word, start=start) def log_word_probability( self, @@ -221,9 +221,9 @@ def log_word_probability( start: Hashable | Mapping[Hashable, float] | Sequence[float] | np.ndarray | None = None, ) -> float: """Return ``log2`` of an observed finite-word probability.""" - from sofic.generators.words import hmm_log_word_probability + from sofic.generators.words import _hmm_log_word_probability - return hmm_log_word_probability(self, word, start=start) + return _hmm_log_word_probability(self, word, start=start) def word_probabilities( self, @@ -233,9 +233,9 @@ def word_probabilities( sparse: bool = True, ) -> dict[tuple[Any, ...], float]: """Return observed-word probabilities for one or more lengths.""" - from sofic.generators.words import hmm_word_probabilities + from sofic.generators.words import _hmm_word_probabilities - return hmm_word_probabilities(self, lengths, start=start, sparse=sparse) + return _hmm_word_probabilities(self, lengths, start=start, sparse=sparse) def conditional_word_probability( self, @@ -245,9 +245,9 @@ def conditional_word_probability( start: Hashable | Mapping[Hashable, float] | Sequence[float] | np.ndarray | None = None, ) -> float: """Return ``P(word | condition)``.""" - from sofic.generators.words import hmm_conditional_word_probability + from sofic.generators.words import _hmm_conditional_word_probability - return hmm_conditional_word_probability(self, word, condition, start=start) + return _hmm_conditional_word_probability(self, word, condition, start=start) def is_equal_process( self, @@ -311,16 +311,16 @@ def stationary_quasidistribution(self) -> np.ndarray: return stationary_quasidistribution(self) - def transition_matrices(self) -> dict[Any, np.ndarray]: - from sofic.generators.quasi_inference import transition_matrices + def symbol_matrices(self) -> dict[Any, np.ndarray]: + from sofic.generators.quasi_inference import symbol_matrices - return transition_matrices(self) + return symbol_matrices(self) def words_of_length(self, length: int) -> dict[tuple[Any, ...], float]: """Return words of ``length`` and their signed quasiprobabilities.""" - from sofic.generators.words import quasi_words_of_length + from sofic.generators.words import _quasi_words_of_length - return quasi_words_of_length(self, length) + return _quasi_words_of_length(self, length) def collision_entropy(self) -> float: from sofic.generators.measures import collision_entropy diff --git a/sofic/generators/channel_measures.py b/sofic/generators/channel_measures.py index 28ac235..0c12e45 100644 --- a/sofic/generators/channel_measures.py +++ b/sofic/generators/channel_measures.py @@ -4,7 +4,7 @@ structural quantities are defined relative to a driving input process. Each measure here drives the transducer with a supplied input generator, forms the joint ``(input, output)`` process via -:func:`~sofic.automata.transducer_operations.compose_tg`, and reads off the +:func:`~sofic.automata.transducer_operations.compose_transducer_generator`, and reads off the quantity -- reusing the directional-flow estimators in :mod:`sofic.generators.directional_flow`. """ @@ -24,9 +24,9 @@ def driven_joint_generator(transducer: MealyMachine, input_process: HiddenMarkovModel) -> MealyHMM: """Return the joint ``(input, output)`` generator induced by ``input_process``.""" - from sofic.automata.transducer_operations import compose_tg + from sofic.automata.transducer_operations import compose_transducer_generator - return compose_tg(transducer, input_process, joint=True) + return compose_transducer_generator(transducer, input_process, joint=True) def channel_statistical_complexity(transducer: MealyMachine, input_process: HiddenMarkovModel) -> float: diff --git a/sofic/generators/epsilon_machine.py b/sofic/generators/epsilon_machine.py index 0aa67ee..b430963 100644 --- a/sofic/generators/epsilon_machine.py +++ b/sofic/generators/epsilon_machine.py @@ -66,22 +66,22 @@ def from_sequence( ``"spectral"`` for Hankel-SVD learning followed by mixed-state extraction. **kwargs - Forwarded to :func:`~sofic.inference.cssr.cssr`, - :func:`~sofic.inference.cssr.subtree_merge`, or - :func:`~sofic.inference.spectral.spectral`. + Forwarded to :func:`~sofic.inference.cssr.learn_epsilon_machine_cssr`, + :func:`~sofic.inference.cssr.learn_epsilon_machine_subtree`, or + :func:`~sofic.inference.spectral.learn_epsilon_machine_spectral`. """ if method == "cssr": - from sofic.inference.cssr.process import cssr + from sofic.inference.cssr.process import learn_epsilon_machine_cssr - return cssr(sequence, **kwargs) + return learn_epsilon_machine_cssr(sequence, **kwargs) if method == "subtree": - from sofic.inference.cssr.subtree import subtree_merge + from sofic.inference.cssr.subtree import learn_epsilon_machine_subtree - return subtree_merge(sequence, **kwargs) + return learn_epsilon_machine_subtree(sequence, **kwargs) if method == "spectral": - from sofic.inference.spectral import spectral + from sofic.inference.spectral import learn_epsilon_machine_spectral - return spectral(sequence, **kwargs) + return learn_epsilon_machine_spectral(sequence, **kwargs) raise ValueError(f"unknown inference method {method!r}") def copy(self) -> Self: diff --git a/sofic/generators/epsilon_transducer.py b/sofic/generators/epsilon_transducer.py index e6406ad..3d091da 100644 --- a/sofic/generators/epsilon_transducer.py +++ b/sofic/generators/epsilon_transducer.py @@ -41,9 +41,9 @@ class EpsilonTransducer(MealyMachine): Examples -------- - >>> from sofic.examples.processes import BinaryChannel + >>> from sofic.examples.processes import binary_channel >>> from sofic import EpsilonTransducer - >>> channel = BinaryChannel(0.1, 0.2) + >>> channel = binary_channel(0.1, 0.2) >>> eps = EpsilonTransducer.from_channel(channel) >>> eps.is_unifilar() True @@ -128,9 +128,9 @@ def from_paired_sequences( **kwargs: Any, ) -> EpsilonTransducer: """Reconstruct an ε-transducer from paired input/output sequences via transCSSR.""" - from sofic.inference.cssr.transducer import transcssr + from sofic.inference.cssr.transducer import learn_epsilon_transducer_cssr - return transcssr(inputs, outputs, **kwargs) + return learn_epsilon_transducer_cssr(inputs, outputs, **kwargs) # -- channel measures ----------------------------------------------------- diff --git a/sofic/generators/markov.py b/sofic/generators/markov.py index 36d6882..60a2651 100644 --- a/sofic/generators/markov.py +++ b/sofic/generators/markov.py @@ -77,9 +77,9 @@ def entropy_rate(self) -> float: def words_of_length(self, length: int) -> dict[tuple[Hashable, ...], float]: """Return visible state paths of ``length`` and their probabilities.""" - from sofic.generators.words import markov_words_of_length + from sofic.generators.words import _markov_words_of_length - return markov_words_of_length(self, length) + return _markov_words_of_length(self, length) def sample_path(self, n: int, rng: np.random.Generator | None = None) -> list[Hashable]: """Sample a state path of length ``n``. diff --git a/sofic/generators/measures.py b/sofic/generators/measures.py index 7220ee7..0bf1013 100644 --- a/sofic/generators/measures.py +++ b/sofic/generators/measures.py @@ -77,23 +77,20 @@ def state_entropy(model: StochasticModel) -> Any: def joint_block_distribution( generator: HiddenMarkovModel, - history_length: int = 1, + block_length: int = 2, ) -> Any: - """Build a ``dit.Distribution`` over observed emission blocks. - - ``history_length`` counts symbols before the present symbol, so the emitted - block length is ``history_length + 1``. - """ + """Build a ``dit.Distribution`` over observed emission blocks of ``block_length`` symbols.""" from sofic.generators.matrices import emission_tensors from sofic.generators.words import _enumerate_words, _matrix_step + if block_length < 1: + raise ValueError("block_length must be at least 1") dit = _require_dit() # Blocks of a stationary process are weighted by the stationary state law, not # the model's initial distribution (which may describe only the transient). pi, joint = emission_tensors(generator, policy="stationary") symbol_list = sorted(generator.observation_alphabet, key=repr) - block_length = max(1, history_length + 1) ones = np.ones(len(pi), dtype=float) outcomes = [] probs = [] @@ -239,7 +236,7 @@ def entropy_rate_markov(chain: MarkovChain) -> Any: def collision_entropy(quasi_model: QuasiStochasticModel) -> float: """Second Renyi entropy rate (bits) from quasi transition matrices.""" - matrices = quasi_model.transition_matrices() + matrices = quasi_model.symbol_matrices() pi = quasi_model.stationary_quasidistribution() total = 0.0 for matrix in matrices.values(): diff --git a/sofic/generators/pfa.py b/sofic/generators/pfa.py index 71c9e2c..dd7251d 100644 --- a/sofic/generators/pfa.py +++ b/sofic/generators/pfa.py @@ -72,9 +72,9 @@ def string_probability(self, word: Sequence[Any]) -> float: def words_of_length(self, length: int) -> dict[tuple[Any, ...], float]: """Return output words of ``length`` and their probabilities.""" - from sofic.generators.words import pfa_words_of_length + from sofic.generators.words import _pfa_words_of_length - return pfa_words_of_length(self, length) + return _pfa_words_of_length(self, length) def sample(self, n: int, rng: np.random.Generator | None = None) -> list[Any]: generator = rng if rng is not None else np.random.default_rng() diff --git a/sofic/generators/quasi_inference.py b/sofic/generators/quasi_inference.py index 5674965..8302a5a 100644 --- a/sofic/generators/quasi_inference.py +++ b/sofic/generators/quasi_inference.py @@ -13,7 +13,7 @@ from sofic.graph import ATTR_QUASIPROB -def transition_matrices(model: QuasiStochasticModel) -> dict[Any, np.ndarray]: +def symbol_matrices(model: QuasiStochasticModel) -> dict[Any, np.ndarray]: matrices, _states = accumulate_matrices( model, attr=ATTR_QUASIPROB, states=model.reindex().states, label=emission_label, symbolic=False ) @@ -27,7 +27,7 @@ def stationary_quasidistribution(model: QuasiStochasticModel) -> np.ndarray: return np.array([], dtype=float) combined = np.zeros((n, n), dtype=float) - for matrix in transition_matrices(model).values(): + for matrix in symbol_matrices(model).values(): combined += matrix distribution = np.zeros(n, dtype=float) @@ -57,7 +57,7 @@ def word_probability(model: QuasiStochasticModel, word: Sequence[Any]) -> float: for state, mass in model.initial_quasidistribution.items(): pi[idx.index(state)] = float(mass) ones = np.ones(n, dtype=float) - matrices = transition_matrices(model) + matrices = symbol_matrices(model) result = pi for symbol in word: matrix = matrices.get(symbol) diff --git a/sofic/generators/quasi_realization.py b/sofic/generators/quasi_realization.py index c7825fd..b52eaa9 100644 --- a/sofic/generators/quasi_realization.py +++ b/sofic/generators/quasi_realization.py @@ -34,7 +34,7 @@ def validate_quasistochastic(self) -> None: if not np.isclose(self.pi.sum(), 1.0): raise QuasiStochasticValidationError(f"pi sums to {self.pi.sum()}, not 1") - def transition_matrices(self) -> dict[Any, np.ndarray]: + def symbol_matrices(self) -> dict[Any, np.ndarray]: return dict(self.symbol_maps) def stationary_quasidistribution(self) -> np.ndarray: diff --git a/sofic/generators/topological_epsilon_enumeration.py b/sofic/generators/topological_epsilon_enumeration.py index 72e7e76..80c4d9b 100644 --- a/sofic/generators/topological_epsilon_enumeration.py +++ b/sofic/generators/topological_epsilon_enumeration.py @@ -16,10 +16,10 @@ IDFAEnumerationError, _delta_table, idfa_string_to_topological_graph, + idfa_transition_count, iter_idfa_strings, rank_idfa_string, reroot_idfa_string, - transition_count, validate_idfa_string, ) from sofic.exceptions import SoficValidationError @@ -274,7 +274,7 @@ def is_topological_epsilon_string( ) -> bool: """Return whether ``transitions`` passes the structural ε-machine tests.""" validate_idfa_string(transitions, n=n, k=k) - defined = transition_count(transitions) + defined = idfa_transition_count(transitions) if defined < n: return False if n > 1 and defined >= n * k: diff --git a/sofic/generators/words.py b/sofic/generators/words.py index 2cb2ba1..904471a 100644 --- a/sofic/generators/words.py +++ b/sofic/generators/words.py @@ -63,7 +63,7 @@ def step(mass: np.ndarray, symbol: Any) -> np.ndarray | None: return step -def hmm_words_of_length(hmm: HiddenMarkovModel, length: int) -> dict[tuple[Any, ...], float]: +def _hmm_words_of_length(hmm: HiddenMarkovModel, length: int) -> dict[tuple[Any, ...], float]: """Return observed words of ``length`` and their probabilities.""" if length < 0: raise ValueError("length must be nonnegative") @@ -83,7 +83,7 @@ def hmm_words_of_length(hmm: HiddenMarkovModel, length: int) -> dict[tuple[Any, return distribution -def hmm_word_probability( +def _hmm_word_probability( hmm: HiddenMarkovModel, word: Sequence[Any], *, @@ -109,20 +109,20 @@ def hmm_word_probability( return float(mass.sum()) -def hmm_log_word_probability( +def _hmm_log_word_probability( hmm: HiddenMarkovModel, word: Sequence[Any], *, start: Hashable | Mapping[Hashable, float] | Sequence[float] | np.ndarray | None = None, ) -> float: """Return ``log2(P(word))`` or ``-inf`` for forbidden words.""" - probability = hmm_word_probability(hmm, word, start=start) + probability = _hmm_word_probability(hmm, word, start=start) if probability <= 0.0: return float("-inf") return float(np.log2(probability)) -def hmm_word_probabilities( +def _hmm_word_probabilities( hmm: HiddenMarkovModel, lengths: int | Sequence[int], *, @@ -139,18 +139,18 @@ def hmm_word_probabilities( distribution: dict[tuple[Any, ...], float] = {} for length in requested: if length == 0: - probability = hmm_word_probability(mealy, (), start=start) + probability = _hmm_word_probability(mealy, (), start=start) if not sparse or abs(probability) > _TOL: distribution[()] = probability continue for word, _ in _enumerate_words(alphabet, length): - probability = hmm_word_probability(mealy, word, start=start) + probability = _hmm_word_probability(mealy, word, start=start) if not sparse or abs(probability) > _TOL: distribution[word] = probability return distribution -def hmm_conditional_word_probability( +def _hmm_conditional_word_probability( hmm: HiddenMarkovModel, word: Sequence[Any], condition: Sequence[Any], @@ -158,14 +158,14 @@ def hmm_conditional_word_probability( start: Hashable | Mapping[Hashable, float] | Sequence[float] | np.ndarray | None = None, ) -> float: """Return ``P(word | condition)`` from the requested start distribution.""" - condition_probability = hmm_word_probability(hmm, condition, start=start) + condition_probability = _hmm_word_probability(hmm, condition, start=start) if condition_probability <= _TOL: raise ZeroDivisionError("condition has zero probability") joint_word = tuple(condition) + tuple(word) - return hmm_word_probability(hmm, joint_word, start=start) / condition_probability + return _hmm_word_probability(hmm, joint_word, start=start) / condition_probability -def pfa_words_of_length(pfa: ProbabilisticFiniteAutomaton, length: int) -> dict[tuple[Any, ...], float]: +def _pfa_words_of_length(pfa: ProbabilisticFiniteAutomaton, length: int) -> dict[tuple[Any, ...], float]: """Return output words of ``length`` and their probabilities.""" if length < 0: raise ValueError("length must be nonnegative") @@ -183,7 +183,7 @@ def pfa_words_of_length(pfa: ProbabilisticFiniteAutomaton, length: int) -> dict[ return distribution -def quasi_words_of_length(model: QuasiStochasticModel, length: int) -> dict[tuple[Any, ...], float]: +def _quasi_words_of_length(model: QuasiStochasticModel, length: int) -> dict[tuple[Any, ...], float]: """Return words of ``length`` and their signed quasiprobabilities.""" if length < 0: raise ValueError("length must be nonnegative") @@ -201,7 +201,7 @@ def quasi_words_of_length(model: QuasiStochasticModel, length: int) -> dict[tupl return distribution -def markov_words_of_length(chain: MarkovChain, length: int) -> dict[tuple[Hashable, ...], float]: +def _markov_words_of_length(chain: MarkovChain, length: int) -> dict[tuple[Hashable, ...], float]: """Return visible state paths of ``length`` and their probabilities.""" if length < 0: raise ValueError("length must be nonnegative") @@ -223,7 +223,7 @@ def _quasi_alphabet(model: QuasiStochasticModel) -> tuple[Any, ...]: alphabet = getattr(model, name, None) if alphabet: return tuple(alphabet) - return tuple(model.transition_matrices()) + return tuple(model.symbol_matrices()) def _markov_start(chain: MarkovChain) -> dict[Hashable, float]: diff --git a/sofic/inference/__init__.py b/sofic/inference/__init__.py index 219ab3d..aa0d8d2 100644 --- a/sofic/inference/__init__.py +++ b/sofic/inference/__init__.py @@ -1,20 +1,22 @@ """Inference algorithms for stochastic generators. -The names ``cssr`` and ``spectral`` bound here are the learner functions; they -shadow the :mod:`sofic.inference.cssr` and :mod:`sofic.inference.spectral` -modules as attributes, so import from those modules with ``from ... import``. +Learners are named ``learn__``; the subpackages and modules +(:mod:`~sofic.inference.bayesian`, :mod:`~sofic.inference.cssr`, +:mod:`~sofic.inference.diagnostics`, :mod:`~sofic.inference.hmm`, +:mod:`~sofic.inference.model_selection`, :mod:`~sofic.inference.spectral`) are +bound under their module names. """ -from sofic.inference import bayesian, hmm +from sofic.inference import bayesian, cssr, diagnostics, hmm, model_selection, spectral from sofic.inference.cssr import ( - cssr, - fit_stack_hmm_mle, + learn_epsilon_machine_cssr, + learn_epsilon_machine_subtree, + learn_epsilon_transducer_cssr, + learn_stack_hmm_cssr, + learn_stack_hmm_mle, learn_stack_hmm_papni, - stack_cssr, - stack_subtree_merge, - subtree_merge, - suggest_lmax, - transcssr, + learn_stack_hmm_subtree, + suggest_max_history, ) from sofic.inference.diagnostics import ( GoodnessOfFit, @@ -52,18 +54,22 @@ from sofic.inference.spectral import ( SpectralInferenceError, hankel_matrices, + learn_epsilon_machine_spectral, learn_spectral_wfa, project_to_epsilon_machine, project_to_mealy, project_to_nmachine, - spectral, spectral_singular_values, ) __all__ = [ "bayesian", - "hmm", "cssr", + "diagnostics", + "hmm", + "model_selection", + "spectral", + "learn_epsilon_machine_cssr", "GoodnessOfFit", "StructureStability", "goodness_of_fit", @@ -86,14 +92,14 @@ "project_to_epsilon_machine", "project_to_mealy", "project_to_nmachine", - "spectral", + "learn_epsilon_machine_spectral", "spectral_singular_values", - "subtree_merge", - "suggest_lmax", - "transcssr", - "stack_cssr", - "stack_subtree_merge", - "fit_stack_hmm_mle", + "learn_epsilon_machine_subtree", + "suggest_max_history", + "learn_epsilon_transducer_cssr", + "learn_stack_hmm_cssr", + "learn_stack_hmm_subtree", + "learn_stack_hmm_mle", "learn_stack_hmm_papni", "baum_welch", "viterbi", diff --git a/sofic/inference/bayesian/diversity.py b/sofic/inference/bayesian/diversity.py index d473c4d..73135e4 100644 --- a/sofic/inference/bayesian/diversity.py +++ b/sofic/inference/bayesian/diversity.py @@ -35,7 +35,6 @@ from sofic.generators.base import HiddenMarkovModel from sofic.generators.process_equivalence import _HistoryFutureWordList -from sofic.generators.words import hmm_words_of_length from sofic.inference.bayesian.counts import BayesianInferenceError from sofic.inference.bayesian.epsilon import EpsilonMachinePosterior @@ -177,7 +176,7 @@ def posterior_mean_word_distribution( machine = posterior.posterior_mean_machine(start_node) if machine is None: continue - words = hmm_words_of_length(machine, length) + words = machine.words_of_length(length) for word, prob in words.items(): distribution[word] = distribution.get(word, 0.0) + start_weight * float(prob) return distribution @@ -240,7 +239,7 @@ def posterior_process_diversity( distributions = [] for _ in range(n_samples): _start, machine = comparison.generate_sample(rng=generator) - distributions.append(hmm_words_of_length(machine, length)) + distributions.append(machine.words_of_length(length)) weights = [1.0 / n_samples] * n_samples process_div = _jsd_from_word_distributions(distributions, weights, alphabet, length) return PosteriorDiversityResult( diff --git a/sofic/inference/cssr/__init__.py b/sofic/inference/cssr/__init__.py index e954d0f..56f6284 100644 --- a/sofic/inference/cssr/__init__.py +++ b/sofic/inference/cssr/__init__.py @@ -14,7 +14,7 @@ StackSuffixCounts, SuffixCounts, ) -from sofic.inference.cssr.process import cssr, suggest_lmax +from sofic.inference.cssr.process import learn_epsilon_machine_cssr, suggest_max_history from sofic.inference.cssr.significance import ( MorphTest, TableTest, @@ -23,13 +23,13 @@ morphs_differ, ) from sofic.inference.cssr.stack import ( - fit_stack_hmm_mle, + learn_stack_hmm_cssr, + learn_stack_hmm_mle, learn_stack_hmm_papni, - stack_cssr, - stack_subtree_merge, + learn_stack_hmm_subtree, ) -from sofic.inference.cssr.subtree import subtree_merge -from sofic.inference.cssr.transducer import transcssr +from sofic.inference.cssr.subtree import learn_epsilon_machine_subtree +from sofic.inference.cssr.transducer import learn_epsilon_transducer_cssr __all__ = [ "ConfigurationHistory", @@ -41,14 +41,14 @@ "SuffixCounts", "TableTest", "aggregates_differ", - "cssr", - "fit_stack_hmm_mle", + "learn_epsilon_machine_cssr", + "learn_stack_hmm_mle", "learn_stack_hmm_papni", "morph_test_score", "morphs_differ", - "stack_cssr", - "stack_subtree_merge", - "subtree_merge", - "suggest_lmax", - "transcssr", + "learn_stack_hmm_cssr", + "learn_stack_hmm_subtree", + "learn_epsilon_machine_subtree", + "suggest_max_history", + "learn_epsilon_transducer_cssr", ] diff --git a/sofic/inference/cssr/process.py b/sofic/inference/cssr/process.py index e75783b..16f6185 100644 --- a/sofic/inference/cssr/process.py +++ b/sofic/inference/cssr/process.py @@ -24,7 +24,7 @@ ) -def _cssr_default_lmax(n: int, alphabet_size: int) -> int: +def _default_max_history(n: int, alphabet_size: int) -> int: """A third of ``log_k n``, between 1 and 10. Each length-``L`` word is then seen about ``n ** (2/3)`` times. Longer suffixes @@ -37,12 +37,12 @@ def _cssr_default_lmax(n: int, alphabet_size: int) -> int: def _suffix_homogenize( counts: SuffixCounts, *, - Lmax: int, + max_history: int, alpha: float, test: MorphTest, min_count: int = 1, ) -> list[set[History]]: - """CSSR homogenization: grow suffixes one symbol into the past, up to length ``Lmax``. + """CSSR homogenization: grow suffixes one symbol into the past, up to length ``max_history``. Each child suffix ``a x`` stays in its parent's state unless its next-symbol distribution differs significantly; then it joins the most similar state that @@ -51,7 +51,7 @@ def _suffix_homogenize( times are not tested: the significance test is unreliable on so few counts. """ states: list[set[History]] = [{()}] - for length in range(Lmax): + for length in range(max_history): for parent_id in range(len(states)): parent = states[parent_id] for history in sorted((h for h in parent if len(h) == length), key=repr): @@ -78,10 +78,10 @@ def _suffix_homogenize( return states -def _suffix_successor(history: History, symbol: Any, Lmax: int) -> History: - """The suffix that follows ``history`` on ``symbol``: extended, or truncated at ``Lmax``.""" +def _suffix_successor(history: History, symbol: Any, max_history: int) -> History: + """The suffix that follows ``history`` on ``symbol``: extended, or truncated at ``max_history``.""" extended = (*history, symbol) - return extended[1:] if len(extended) > Lmax else extended + return extended[1:] if len(extended) > max_history else extended def _suffix_edges( @@ -89,17 +89,17 @@ def _suffix_edges( counts: SuffixCounts, alive: set[int], *, - Lmax: int, + max_history: int, alpha: float, test: MorphTest, resolve: bool = True, ) -> dict[int, dict[Any, dict[int, set[History]]]]: """Successor states of each alive state, by symbol, with the suffixes that lead there. - A suffix shorter than ``Lmax`` moves to the state holding its one-symbol extension. - A length-``Lmax`` suffix must drop its oldest symbol, which can forget the phase + A suffix shorter than ``max_history`` moves to the state holding its one-symbol extension. + A length-``max_history`` suffix must drop its oldest symbol, which can forget the phase of a non-Markovian process: for the even process, the truncation of ``0111`` is - ``111``, whose parity is unknown. So the length-``Lmax + 1`` suffix is tested + ``111``, whose parity is unknown. So the length-``max_history + 1`` suffix is tested against the truncated suffix's state, and if its morph differs, it moves to the alive state whose morph it matches best instead. ``resolve=False`` always truncates. """ @@ -112,7 +112,7 @@ def _suffix_edges( if count == 0: continue extended = (*history, symbol) - if len(extended) <= Lmax: + if len(extended) <= max_history: target = history_to_state.get(extended) else: target = history_to_state.get(extended[1:]) @@ -163,7 +163,7 @@ def _suffix_determinize( counts: SuffixCounts, alive: set[int], *, - Lmax: int, + max_history: int, alpha: float, test: MorphTest, resolve: bool = True, @@ -175,7 +175,7 @@ def _suffix_determinize( states = [set(h) for h in states] alive = set(alive) while True: - edges = _suffix_edges(states, counts, alive, Lmax=Lmax, alpha=alpha, test=test, resolve=resolve) + edges = _suffix_edges(states, counts, alive, max_history=max_history, alpha=alpha, test=test, resolve=resolve) split = None for index in sorted(alive): for symbol in sorted(edges[index], key=repr): @@ -200,19 +200,19 @@ def _suffix_machine( sequence: Sequence[Any], alive: set[int], *, - Lmax: int, + max_history: int, alpha: float, test: MorphTest, resolve: bool = True, ) -> EpsilonMachine: """Build the ε-machine on the most-visited recurrent class of the alive states.""" - edges = _suffix_edges(states, counts, alive, Lmax=Lmax, alpha=alpha, test=test, resolve=resolve) + edges = _suffix_edges(states, counts, alive, max_history=max_history, alpha=alpha, test=test, resolve=resolve) history_to_state = {h: index for index in alive for h in states[index]} visits: Counter[int] = Counter() seq = tuple(sequence) for t in range(len(seq) + 1): - for length in range(min(t, Lmax), -1, -1): + for length in range(min(t, max_history), -1, -1): state = history_to_state.get(seq[t - length : t]) if state is not None: visits[state] += 1 @@ -220,7 +220,7 @@ def _suffix_machine( classes = _recurrent_states(edges) if not classes: - raise StochasticValidationError("no recurrent inferred states; the sample is too short for this Lmax") + raise StochasticValidationError("no recurrent inferred states; the sample is too short for this max_history") keep = max(classes, key=lambda members: (sum(visits[s] for s in members), -min(members))) labels = {state: f"s{rank}" for rank, state in enumerate(sorted(keep))} @@ -267,7 +267,7 @@ def _suffix_reconstruct( counts: SuffixCounts, sequence: Sequence[Any], *, - Lmax: int, + max_history: int, alpha: float, test: MorphTest, ) -> EpsilonMachine: @@ -275,20 +275,26 @@ def _suffix_reconstruct( def reconstruct(resolve: bool) -> EpsilonMachine: everything = set(range(len(states))) - edges = _suffix_edges(states, counts, everything, Lmax=Lmax, alpha=alpha, test=test, resolve=resolve) + edges = _suffix_edges( + states, counts, everything, max_history=max_history, alpha=alpha, test=test, resolve=resolve + ) alive = set().union(*_recurrent_states(edges)) or everything - split, alive = _suffix_determinize(states, counts, alive, Lmax=Lmax, alpha=alpha, test=test, resolve=resolve) - return _suffix_machine(split, counts, sequence, alive, Lmax=Lmax, alpha=alpha, test=test, resolve=resolve) + split, alive = _suffix_determinize( + states, counts, alive, max_history=max_history, alpha=alpha, test=test, resolve=resolve + ) + return _suffix_machine( + split, counts, sequence, alive, max_history=max_history, alpha=alpha, test=test, resolve=resolve + ) machine = reconstruct(resolve=True) - # Resolving truncated successors needs Lmax at least the synchronization length. When it + # Resolving truncated successors needs max_history at least the synchronization length. When it # is shorter, resolution can close off a state that never emits some observed symbol. if {t.data[ATTR_EMISSION] for t in machine.transitions()} < set(sequence): machine = reconstruct(resolve=False) return machine -def suggest_lmax( +def suggest_max_history( sequence: Sequence[Any], *, alpha: float = 0.01, @@ -297,7 +303,7 @@ def suggest_lmax( n_surrogates: int = 999, seed: int = 0, ) -> int: - """A data-driven ``Lmax`` for :func:`cssr`: the estimated Markov order, at least 1. + """A data-driven ``max_history`` for :func:`learn_epsilon_machine_cssr`: the estimated Markov order, at least 1. Orders ``0, 1, ...`` are tested against the next order with :func:`dit.inference.select_markov_order`. The default ``"exact"`` method @@ -333,7 +339,7 @@ def suggest_lmax( select_markov_order = getattr(dit.inference, "select_markov_order", None) if select_markov_order is None: # pragma: no cover - depends on the installed dit - raise ImportError("suggest_lmax requires a dit release with dit.inference.select_markov_order") + raise ImportError("suggest_max_history requires a dit release with dit.inference.select_markov_order") codes: dict[Any, str] = {} seq = [codes.setdefault(symbol, str(len(codes))) for symbol in sequence] if max_order is None: @@ -343,11 +349,11 @@ def suggest_lmax( return max(1, int(order)) -def cssr( +def learn_epsilon_machine_cssr( sequence: Sequence[Any], *, alphabet: Sequence[Any] | None = None, - Lmax: int | Literal["auto"] | None = None, + max_history: int | Literal["auto"] | None = None, alpha: float = 0.01, test: MorphTest = "g", min_count: int = 5, @@ -355,7 +361,7 @@ def cssr( ) -> EpsilonMachine: """Reconstruct an ε-machine by Causal-State Splitting Reconstruction :cite:`Shalizi2004`. - Suffixes are grown one symbol into the past up to length ``Lmax`` and grouped by + Suffixes are grown one symbol into the past up to length ``max_history`` and grouped by their next-symbol distributions (homogenization), then states are split until every transition is deterministic (determinization). The result is restricted to its most-visited closed class, so it is always a valid recurrent machine. @@ -366,16 +372,16 @@ def cssr( Observed symbols. alphabet Symbol alphabet; defaults to the symbols in ``sequence``. - Lmax + max_history Longest suffix considered; by default a third of ``log_k len(sequence)``, between 1 and 10. It should be at least the synchronization length of the source (for a Markov source, its order). Larger values run many more significance tests, and some of them split states by chance. ``"auto"`` - uses :func:`suggest_lmax`, the Markov order estimated by exact tests. + uses :func:`suggest_max_history`, the Markov order estimated by exact tests. alpha Significance level of each morph-equality test. The worked example of :cite:`Shalizi2002` uses 0.01; smaller values guard against spurious states - when ``Lmax`` is large. + when ``max_history`` is large. test ``"g"`` (G-test), ``"chi2"``, ``"tv"`` (total-variation threshold), or ``"exact"`` (Monte Carlo exact G-test when expected counts are small; see @@ -391,25 +397,25 @@ def cssr( Notes ----- A process that is not exactly synchronizable (no finite past determines its - state, such as :func:`~sofic.examples.processes.ABC`) has no finite-``Lmax`` + state, such as :func:`~sofic.examples.alternating_biased_coins`) has no finite-``max_history`` reconstruction. CSSR then returns more states than the ε-machine, with an - entropy rate that approaches the true one from above as ``Lmax`` grows. + entropy rate that approaches the true one from above as ``max_history`` grows. """ seq = tuple(sequence) if len(seq) < 2: raise ValueError("sequence must contain at least two symbols") alphabet_size = len(set(seq)) if alphabet is None else len(tuple(alphabet)) - if Lmax == "auto": - max_length = suggest_lmax(seq, alpha=alpha) + if max_history == "auto": + max_length = suggest_max_history(seq, alpha=alpha) else: - max_length = Lmax if Lmax is not None else _cssr_default_lmax(len(seq), alphabet_size) + max_length = max_history if max_history is not None else _default_max_history(len(seq), alphabet_size) if max_length < 0: - raise ValueError("Lmax must be non-negative") + raise ValueError("max_history must be non-negative") counts = SuffixCounts.from_sequence(seq, alphabet=alphabet, max_length=max_length + 1) if correction == "bonferroni": alpha = _bonferroni_alpha(counts, alpha, max_length=max_length, min_count=min_count) elif correction is not None: raise ValueError(f"unknown correction {correction!r}") - homogeneous = _suffix_homogenize(counts, Lmax=max_length, alpha=alpha, test=test, min_count=min_count) - return _suffix_reconstruct(homogeneous, counts, seq, Lmax=max_length, alpha=alpha, test=test) + homogeneous = _suffix_homogenize(counts, max_history=max_length, alpha=alpha, test=test, min_count=min_count) + return _suffix_reconstruct(homogeneous, counts, seq, max_history=max_length, alpha=alpha, test=test) diff --git a/sofic/inference/cssr/stack.py b/sofic/inference/cssr/stack.py index 02299ec..1d8d605 100644 --- a/sofic/inference/cssr/stack.py +++ b/sofic/inference/cssr/stack.py @@ -11,16 +11,16 @@ from sofic.generators.stack_hmm import HiddenMarkovStackModel from sofic.graph import ATTR_SYMBOL from sofic.inference.cssr.counts import ConfigurationHistory, StackSuffixCounts, _push -from sofic.inference.cssr.process import _cssr_default_lmax, _recurrent_states, suggest_lmax +from sofic.inference.cssr.process import _default_max_history, _recurrent_states, suggest_max_history from sofic.inference.cssr.significance import MorphTest, _bonferroni_alpha, morph_test_score, morphs_differ from sofic.inference.cssr.subtree import _cluster_histories_by_morph from sofic.shifts.sofic_dyck import SoficDyckShift, TransitionRef, transition_ref __all__ = [ - "fit_stack_hmm_mle", + "learn_stack_hmm_mle", "learn_stack_hmm_papni", - "stack_cssr", - "stack_subtree_merge", + "learn_stack_hmm_cssr", + "learn_stack_hmm_subtree", ] @@ -90,7 +90,7 @@ def _stack_homogenize( counts: StackSuffixCounts, *, alphabet: DyckAlphabet, - Lmax: int, + max_history: int, alpha: float, test: MorphTest, max_stack_depth: int, @@ -101,7 +101,7 @@ def _stack_homogenize( Every observed stack contributes a root ``((), stack)``; suffixes then grow one symbol into the past with their stack fixed, exactly as in flat CSSR. Growing forward from the empty configuration instead only reaches stacks of depth at - most ``Lmax``, so deeper configurations had no state and their transitions were + most ``max_history``, so deeper configurations had no state and their transitions were dropped. Morphs are compared with return symbols collapsed (see :func:`_control_counts`). """ control = _control_counts(counts, alphabet) @@ -132,7 +132,7 @@ def observed(history: ConfigurationHistory) -> bool: for stack in sorted(roots, key=lambda stack: (len(stack), repr(stack))): if observed(((), stack)): place(((), stack), 0) - for length in range(Lmax): + for length in range(max_history): for state_id in sorted(states): for suffix, stack in sorted((h for h in states[state_id] if len(h[0]) == length), key=repr): for symbol in counts.alphabet: @@ -371,11 +371,11 @@ def legal(symbol: Any, stack: tuple[Any, ...]) -> bool: return model -def stack_cssr( +def learn_stack_hmm_cssr( sequence: Sequence[Any], *, alphabet: DyckAlphabet, - Lmax: int | Literal["auto"] | None = None, + max_history: int | Literal["auto"] | None = None, max_stack_depth: int = 8, alpha: float = 0.05, test: MorphTest = "g", @@ -384,20 +384,22 @@ def stack_cssr( ) -> HiddenMarkovStackModel: """Reconstruct a stack HMM via configuration-lifted CSSR. - ``Lmax="auto"`` uses :func:`~sofic.inference.cssr.suggest_lmax` + ``max_history="auto"`` uses :func:`~sofic.inference.cssr.suggest_max_history` on the observed symbols. Stack processes generally have infinite Markov order, so treat it as a lower bound on the suffix length the data support. ``test="exact"`` and ``correction="bonferroni"`` are as in - :func:`~sofic.inference.cssr.cssr`; the correction counts + :func:`~sofic.inference.cssr.learn_epsilon_machine_cssr`; the correction counts eligible (suffix, stack) configurations. """ seq = tuple(sequence) if len(seq) < 2: raise ValueError("sequence must contain at least two symbols") - if Lmax == "auto": - max_length = suggest_lmax(seq, alpha=alpha) + if max_history == "auto": + max_length = suggest_max_history(seq, alpha=alpha) else: - max_length = Lmax if Lmax is not None else _cssr_default_lmax(len(seq), len(alphabet.symbol_alphabet)) + max_length = ( + max_history if max_history is not None else _default_max_history(len(seq), len(alphabet.symbol_alphabet)) + ) counts = StackSuffixCounts.from_sequence( seq, alphabet=alphabet, @@ -417,7 +419,7 @@ def stack_cssr( states, history_to_state = _stack_homogenize( counts, alphabet=alphabet, - Lmax=max_length, + max_history=max_length, alpha=alpha, test=test, max_stack_depth=max_stack_depth, @@ -447,27 +449,27 @@ def stack_cssr( ) -def stack_subtree_merge( +def learn_stack_hmm_subtree( sequence: Sequence[Any], *, alphabet: DyckAlphabet, - L: int, + max_history: int, max_stack_depth: int = 8, delta: float = 0.0, ) -> HiddenMarkovStackModel: - """Reconstruct a stack HMM by merging depth-``L`` configuration subtrees.""" - if L < 0: - raise ValueError("L must be non-negative") + """Reconstruct a stack HMM by merging depth-``max_history`` configuration subtrees.""" + if max_history < 0: + raise ValueError("max_history must be non-negative") seq = tuple(sequence) if len(seq) < 2: raise ValueError("sequence must contain at least two symbols") counts = StackSuffixCounts.from_sequence( seq, alphabet=alphabet, - max_length=L + 1, + max_length=max_history + 1, max_stack_depth=max_stack_depth, ) - histories = {history for history in counts.history_counts if len(history[0]) <= L} + histories = {history for history in counts.history_counts if len(history[0]) <= max_history} histories.add(((), ())) proxy = counts.restricted_to(histories) states = _cluster_histories_by_morph(proxy, histories, delta=delta) @@ -476,7 +478,7 @@ def stack_subtree_merge( states, history_to_state, counts, - length=L, + length=max_history, alphabet=alphabet, max_stack_depth=max_stack_depth, ) @@ -488,7 +490,7 @@ def stack_subtree_merge( history: state_id for state_id, histories_in_state in states.items() for history in histories_in_state } states = _stack_drop_transient( - states, history_to_state, counts, length=L, alphabet=alphabet, max_stack_depth=max_stack_depth + states, history_to_state, counts, length=max_history, alphabet=alphabet, max_stack_depth=max_stack_depth ) history_to_state = { history: state_id for state_id, histories_in_state in states.items() for history in histories_in_state @@ -499,12 +501,12 @@ def stack_subtree_merge( history_to_state, seq, alphabet=alphabet, - length=L, + length=max_history, max_stack_depth=max_stack_depth, ) -def fit_stack_hmm_mle( +def learn_stack_hmm_mle( shift: SoficDyckShift, sequence: Sequence[Any], *, @@ -568,4 +570,4 @@ def learn_stack_hmm_papni( fit_source = max((tuple(word) for word in positive if is_well_matched(word, alphabet)), key=len, default=()) if not fit_source: raise ValueError("no sequence available for parameter fitting") - return fit_stack_hmm_mle(shift, fit_source) + return learn_stack_hmm_mle(shift, fit_source) diff --git a/sofic/inference/cssr/subtree.py b/sofic/inference/cssr/subtree.py index 1e0ee52..9278fcd 100644 --- a/sofic/inference/cssr/subtree.py +++ b/sofic/inference/cssr/subtree.py @@ -11,7 +11,7 @@ from sofic.generators.epsilon_machine import EpsilonMachine from sofic.inference.cssr.counts import History, SuffixCounts -from sofic.inference.cssr.process import _suffix_reconstruct, suggest_lmax +from sofic.inference.cssr.process import _suffix_reconstruct, suggest_max_history from sofic.inference.cssr.significance import MorphTest, morphs_differ #: Significance level used by subtree merging when ``delta = 0`` and to resolve truncated successors. @@ -83,38 +83,38 @@ def union(left: History, right: History) -> None: return states -def subtree_merge( +def learn_epsilon_machine_subtree( sequence: Sequence[Any], *, - L: int | Literal["auto"], + max_history: int | Literal["auto"], delta: float = 0.0, alphabet: Sequence[Any] | None = None, alpha: float = _SUBTREE_ALPHA, test: MorphTest = "g", correction: Literal["bonferroni"] | None = None, ) -> EpsilonMachine: - """Reconstruct an ε-machine by merging depth-``L`` subtrees (Crutchfield--Young). + """Reconstruct an ε-machine by merging depth-``max_history`` subtrees (Crutchfield--Young). - Histories up to length ``L`` are clustered by next-symbol distribution: within + Histories up to length ``max_history`` are clustered by next-symbol distribution: within total-variation distance ``delta``, or, when ``delta = 0``, unless a morph test (``test``, at level ``alpha``) tells them apart. The clusters are then - determinized as in :func:`cssr`. + determinized as in :func:`learn_epsilon_machine_cssr`. - ``L="auto"`` uses :func:`suggest_lmax`. ``correction="bonferroni"`` divides + ``max_history="auto"`` uses :func:`suggest_max_history`. ``correction="bonferroni"`` divides ``alpha`` by the number of history pairs compared, so that no pair is split apart by chance; since a rejected test *separates* histories, this makes the reconstruction more conservative (fewer states). """ - if L == "auto": - L = suggest_lmax(sequence, alpha=alpha) - if L < 0: - raise ValueError("L must be non-negative") + if max_history == "auto": + max_history = suggest_max_history(sequence, alpha=alpha) + if max_history < 0: + raise ValueError("max_history must be non-negative") seq = tuple(sequence) if len(seq) < 2: raise ValueError("sequence must contain at least two symbols") - counts = SuffixCounts.from_sequence(seq, alphabet=alphabet, max_length=L + 1) + counts = SuffixCounts.from_sequence(seq, alphabet=alphabet, max_length=max_history + 1) - histories = {history for history in counts.history_counts if len(history) <= L} + histories = {history for history in counts.history_counts if len(history) <= max_history} histories.add(()) if correction == "bonferroni": alpha /= max(1, len(histories) * (len(histories) - 1) // 2) @@ -122,4 +122,4 @@ def subtree_merge( raise ValueError(f"unknown correction {correction!r}") states = list(_cluster_histories_by_morph(counts, histories, delta=delta, alpha=alpha, test=test).values()) - return _suffix_reconstruct(states, counts, seq, Lmax=L, alpha=alpha, test=test) + return _suffix_reconstruct(states, counts, seq, max_history=max_history, alpha=alpha, test=test) diff --git a/sofic/inference/cssr/transducer.py b/sofic/inference/cssr/transducer.py index f3cd4cd..e7588c5 100644 --- a/sofic/inference/cssr/transducer.py +++ b/sofic/inference/cssr/transducer.py @@ -27,7 +27,7 @@ _merge_aggregate, _state_aggregate, ) -from sofic.inference.cssr.process import _recurrent_states, suggest_lmax +from sofic.inference.cssr.process import _recurrent_states, suggest_max_history from sofic.inference.cssr.significance import TableTest, _aggregate_score, aggregates_differ @@ -38,7 +38,7 @@ def _observed(counts: JointSuffixCounts, history: JointHistory) -> int: def _homogenize( counts: JointSuffixCounts, *, - Lmax: int, + max_history: int, alpha: float, test: TableTest, min_count: int, @@ -59,7 +59,7 @@ def differ(left: StateAggregate, right: StateAggregate) -> bool: left, right, input_alphabet=in_alpha, output_alphabet=out_alpha, alpha=alpha, test=test ) - for length in range(Lmax): + for length in range(max_history): for parent_id in range(len(states)): for history in sorted((h for h in states[parent_id] if len(h) == length), key=repr): for pair in pairs: @@ -105,14 +105,14 @@ def _edges( counts: JointSuffixCounts, alive: set[int], *, - Lmax: int, + max_history: int, alpha: float, test: TableTest, ) -> dict[int, dict[tuple[Any, Any], dict[int, set[JointHistory]]]]: """Successor states by ``(input, output)`` pair, with the histories that lead there. - Shorter histories move to their one-pair extension; length-``Lmax`` histories - drop their oldest pair, and the length-``Lmax + 1`` history is re-tested against + Shorter histories move to their one-pair extension; length-``max_history`` histories + drop their oldest pair, and the length-``max_history + 1`` history is re-tested against the truncated history's state (see :func:`sofic.inference.cssr.process._suffix_edges`). """ @@ -127,7 +127,7 @@ def _edges( if not _history_emits(counts, history, pair): continue extended = (*history, pair) - if len(extended) <= Lmax: + if len(extended) <= max_history: target = history_to_state.get(extended) else: target = history_to_state.get(extended[1:]) @@ -170,7 +170,7 @@ def _determinize( counts: JointSuffixCounts, alive: set[int], *, - Lmax: int, + max_history: int, alpha: float, test: TableTest, ) -> tuple[list[set[JointHistory]], set[int]]: @@ -178,7 +178,7 @@ def _determinize( states = [set(h) for h in states] alive = set(alive) while True: - edges = _edges(states, counts, alive, Lmax=Lmax, alpha=alpha, test=test) + edges = _edges(states, counts, alive, max_history=max_history, alpha=alpha, test=test) split = next( ( (index, pair) @@ -205,17 +205,17 @@ def _build_transducer( inputs: Sequence[Any], outputs: Sequence[Any], *, - Lmax: int, + max_history: int, alpha: float, test: TableTest, ) -> EpsilonTransducer: - edges = _edges(states, counts, alive, Lmax=Lmax, alpha=alpha, test=test) + edges = _edges(states, counts, alive, max_history=max_history, alpha=alpha, test=test) history_to_state = {h: index for index in alive for h in states[index]} visits: Counter[int] = Counter() pairs = tuple(zip(inputs, outputs, strict=True)) for t in range(len(pairs) + 1): - for hist_len in range(min(t, Lmax), -1, -1): + for hist_len in range(min(t, max_history), -1, -1): state = history_to_state.get(pairs[t - hist_len : t]) if state is not None: visits[state] += 1 @@ -223,7 +223,7 @@ def _build_transducer( classes = _recurrent_states(edges) if not classes: - raise StochasticValidationError("no recurrent inferred states; the sample is too short for this Lmax") + raise StochasticValidationError("no recurrent inferred states; the sample is too short for this max_history") keep = max(classes, key=lambda members: (sum(visits[s] for s in members), -min(members))) graph = TransitionGraph() @@ -271,7 +271,7 @@ def _build_transducer( return result -def _default_lmax(n: int, alphabet_size: int, min_count: int) -> int: +def _default_max_history(n: int, alphabet_size: int, min_count: int) -> int: if alphabet_size <= 0: return 1 # The joint (input, output) history space grows as ``alphabet_size ** L``, so @@ -279,13 +279,13 @@ def _default_lmax(n: int, alphabet_size: int, min_count: int) -> int: return max(1, min(5, n // max(1, alphabet_size * min_count))) -def transcssr( +def learn_epsilon_transducer_cssr( inputs: Sequence[Any], outputs: Sequence[Any], *, input_alphabet: Sequence[Any] | None = None, output_alphabet: Sequence[Any] | None = None, - Lmax: int | Literal["auto"] | None = None, + max_history: int | Literal["auto"] | None = None, alpha: float = 0.001, test: TableTest = "g", min_count: int = 5, @@ -295,10 +295,10 @@ def transcssr( ``alpha`` is the per-test significance level for the causal-state split decision; the transCSSR/CSSR default of ``0.001`` favors fewer, more robust - states. ``Lmax`` bounds the joint-history depth and ``min_count`` the minimum + states. ``max_history`` bounds the joint-history depth and ``min_count`` the minimum occurrences before a history is eligible to seed a new state. - ``Lmax="auto"`` applies :func:`~sofic.inference.cssr.suggest_lmax` + ``max_history="auto"`` applies :func:`~sofic.inference.cssr.suggest_max_history` to the joint ``(input, output)`` sequence. ``test="exact"`` uses the Monte Carlo exact G-test when expected counts are small (see :func:`~sofic.inference.cssr.morphs_differ`), and @@ -316,10 +316,12 @@ def transcssr( if input_alphabet is None or output_alphabet is None else len(tuple(input_alphabet)) * len(tuple(output_alphabet)) ) - if Lmax == "auto": - max_length = suggest_lmax(list(zip(xs, ys, strict=True)), alpha=alpha) + if max_history == "auto": + max_length = suggest_max_history(list(zip(xs, ys, strict=True)), alpha=alpha) else: - max_length = Lmax if Lmax is not None else _default_lmax(len(xs), joint_alphabet_size, min_count) + max_length = ( + max_history if max_history is not None else _default_max_history(len(xs), joint_alphabet_size, min_count) + ) counts = JointSuffixCounts.from_sequences( xs, ys, @@ -337,9 +339,9 @@ def transcssr( elif correction is not None: raise ValueError(f"unknown correction {correction!r}") - states = _homogenize(counts, Lmax=max_length, alpha=alpha, test=test, min_count=min_count) + states = _homogenize(counts, max_history=max_length, alpha=alpha, test=test, min_count=min_count) everything = set(range(len(states))) - edges = _edges(states, counts, everything, Lmax=max_length, alpha=alpha, test=test) + edges = _edges(states, counts, everything, max_history=max_length, alpha=alpha, test=test) alive = set().union(*_recurrent_states(edges)) or everything - states, alive = _determinize(states, counts, alive, Lmax=max_length, alpha=alpha, test=test) - return _build_transducer(states, counts, alive, xs, ys, Lmax=max_length, alpha=alpha, test=test) + states, alive = _determinize(states, counts, alive, max_history=max_length, alpha=alpha, test=test) + return _build_transducer(states, counts, alive, xs, ys, max_history=max_length, alpha=alpha, test=test) diff --git a/sofic/inference/diagnostics.py b/sofic/inference/diagnostics.py index 9b97732..81a1d86 100644 --- a/sofic/inference/diagnostics.py +++ b/sofic/inference/diagnostics.py @@ -37,13 +37,13 @@ class GoodnessOfFit: ---------- statistic ``"g"`` or ``"entropy_rate"``. - L + block_length The word length compared. value The statistic on the observed data. pvalue ``(1 + #{simulated >= observed}) / (1 + n_samples)``. Small values mean - the machine does not reproduce the data's length-``L`` statistics. + the machine does not reproduce the data's length-``block_length`` statistics. null The statistic on each sequence simulated from the machine. forbidden_words @@ -51,7 +51,7 @@ class GoodnessOfFit: """ statistic: str - L: int + block_length: int value: float pvalue: float null: np.ndarray @@ -92,7 +92,7 @@ def goodness_of_fit( machine: Any, data: Sequence[Any], *, - L: int | None = None, + block_length: int | None = None, statistic: Literal["g", "entropy_rate"] = "g", n_samples: int = 199, burn_in: int = 100, @@ -101,8 +101,8 @@ def goodness_of_fit( """Parametric-bootstrap test that ``machine`` generated ``data``. Sequences as long as ``data`` are simulated from ``machine`` (after - ``burn_in`` steps, so they start near stationarity), and a length-``L`` word - statistic of the data is compared with its distribution over the simulations. + ``burn_in`` steps, so they start near stationarity), and a length-``block_length`` + word statistic of the data is compared with its distribution over the simulations. Because the null distribution is simulated, the overlap between successive words is accounted for; no chi-squared approximation is used. @@ -110,14 +110,14 @@ def goodness_of_fit( ---------- machine A fitted generator with ``sample``, ``word_probabilities`` and - ``stationary_distribution`` (e.g. an ε-machine from :func:`cssr`). + ``stationary_distribution`` (e.g. an ε-machine from :func:`learn_epsilon_machine_cssr`). data The observed sequence the machine was fitted to. - L + block_length Word length; by default the longest with about ten observations per possible word (between 1 and 6). statistic - ``"g"`` is the G statistic of the observed length-``L`` word counts + ``"g"`` is the G statistic of the observed length-``block_length`` word counts against the machine's stationary word probabilities. ``"entropy_rate"`` is ``|h_hat - h_L|``, the gap between the plug-in conditional entropy ``H[X_{L-1} | X_{0:L-1}]`` and the machine's value. @@ -137,7 +137,7 @@ def goodness_of_fit( Fitting and testing on the same data makes the test conservative, as in any parametric bootstrap without refitting. A small p-value is still evidence that the reconstruction misses structure. For CSSR that usually means - ``Lmax`` is shorter than the source's synchronization length, which happens + ``max_history`` is shorter than the source's synchronization length, which happens for strictly sofic sources. An observed word that the machine forbids gives ``G = inf`` and the smallest possible p-value. """ @@ -145,17 +145,17 @@ def goodness_of_fit( seq = tuple(data) n = len(seq) alphabet = set(seq) | set(machine.observation_alphabet) - if L is None: - L = _default_word_length(n, len(alphabet)) - if L < 1 or n < L: - raise ValueError("L must be between 1 and len(data)") + if block_length is None: + block_length = _default_word_length(n, len(alphabet)) + if block_length < 1 or n < block_length: + raise ValueError("block_length must be between 1 and len(data)") pi = np.asarray(machine.stationary_distribution(), dtype=float) - probabilities = {tuple(w): float(p) for w, p in machine.word_probabilities(L, start=pi).items()} + probabilities = {tuple(w): float(p) for w, p in machine.word_probabilities(block_length, start=pi).items()} if statistic == "g": def compute(sample: Sequence[Any]) -> float: - counts = _word_counts(sample, L) + counts = _word_counts(sample, block_length) total = sum(counts.values()) g = 0.0 for word, count in counts.items(): @@ -168,7 +168,7 @@ def compute(sample: Sequence[Any]) -> float: target = _model_conditional_entropy(probabilities) def compute(sample: Sequence[Any]) -> float: - return abs(_conditional_entropy(_word_counts(sample, L)) - target) + return abs(_conditional_entropy(_word_counts(sample, block_length)) - target) else: raise ValueError(f"unknown statistic {statistic!r}") @@ -179,8 +179,10 @@ def compute(sample: Sequence[Any]) -> float: null[i] = compute(simulated[burn_in:]) tol = 1e-12 * max(1.0, abs(value)) if np.isfinite(value) else 0.0 pvalue = float((1 + np.sum(null >= value - tol)) / (1 + n_samples)) - forbidden = tuple(sorted((w for w in _word_counts(seq, L) if probabilities.get(w, 0.0) <= 0.0), key=repr)) - return GoodnessOfFit(statistic, int(L), float(value), pvalue, null, forbidden) + forbidden = tuple( + sorted((w for w in _word_counts(seq, block_length) if probabilities.get(w, 0.0) <= 0.0), key=repr) + ) + return GoodnessOfFit(statistic, int(block_length), float(value), pvalue, null, forbidden) def topology_key(machine: Any) -> tuple[int, tuple[tuple[int, str, int], ...]]: @@ -301,7 +303,7 @@ def structure_stability( rng Seed or generator. **kwargs - Forwarded to the reconstruction (e.g. ``Lmax``, ``alpha``). + Forwarded to the reconstruction (e.g. ``max_history``, ``alpha``). Returns ------- @@ -351,24 +353,24 @@ def reconstruction_sweep( sequence: Sequence[Any], *, alphas: Sequence[float] = (0.05, 0.01, 0.001), - lmaxes: Sequence[int] = (1, 2, 3, 4), + max_histories: Sequence[int] = (1, 2, 3, 4), method: Literal["cssr"] | Callable[..., Any] = "cssr", **kwargs: Any, ) -> dict[tuple[float, int], tuple[int, tuple[tuple[int, str, int], ...]] | None]: """Reconstruct over a grid of significance levels and history lengths. - Returns ``{(alpha, Lmax): topology_key}``, with ``None`` where reconstruction - failed. A structure that persists across a range of ``alpha`` and ``Lmax`` is + Returns ``{(alpha, max_history): topology_key}``, with ``None`` where reconstruction + failed. A structure that persists across a range of ``alpha`` and ``max_history`` is better supported than one that appears at a single setting. For a Markov - source, it should be stable for every ``Lmax`` at or above the source's order. + source, it should be stable for every ``max_history`` at or above the source's order. """ results: dict[tuple[float, int], tuple[int, tuple[tuple[int, str, int], ...]] | None] = {} for alpha in alphas: - for lmax in lmaxes: + for history in max_histories: try: - machine = _reconstruct(sequence, method, {**kwargs, "alpha": alpha, "Lmax": lmax}) + machine = _reconstruct(sequence, method, {**kwargs, "alpha": alpha, "max_history": history}) except (StochasticValidationError, ValueError): - results[alpha, lmax] = None + results[alpha, history] = None continue - results[alpha, lmax] = topology_key(machine) + results[alpha, history] = topology_key(machine) return results diff --git a/sofic/inference/hmm/filtering.py b/sofic/inference/hmm/filtering.py index 337b44c..59ce68d 100644 --- a/sofic/inference/hmm/filtering.py +++ b/sofic/inference/hmm/filtering.py @@ -55,15 +55,15 @@ def _forward_scaled( return alpha_hat, log_scales -def forward(hmm: HiddenMarkovModel, observations: Sequence[Any], *, scaled: bool = False) -> np.ndarray: +def forward(hmm: HiddenMarkovModel, observations: Sequence[Any], *, normalize: bool = False) -> np.ndarray: """Return forward messages ``alpha[t, s]`` for ``len(observations)+1`` rows. - With ``scaled=True`` each row is normalized to sum to one (the numerically + With ``normalize=True`` each row is normalized to sum to one (the numerically stable message used for posteriors); otherwise the raw messages are returned. """ pi, joint = emission_tensors(hmm) obs = list(observations) - if scaled: + if normalize: alpha_hat, _log_scales = _forward_scaled(pi, joint, obs) return alpha_hat n = len(pi) @@ -78,10 +78,10 @@ def forward(hmm: HiddenMarkovModel, observations: Sequence[Any], *, scaled: bool return alpha -def backward(hmm: HiddenMarkovModel, observations: Sequence[Any], *, scaled: bool = False) -> np.ndarray: +def backward(hmm: HiddenMarkovModel, observations: Sequence[Any], *, normalize: bool = False) -> np.ndarray: """Return backward messages ``beta[t, s]`` for ``len(observations)+1`` rows. - With ``scaled=True`` each row is normalized to sum to one. The smoothed + With ``normalize=True`` each row is normalized to sum to one. The smoothed posterior is then ``normalize(alpha_hat[t] * beta_hat[t])`` (the per-row scaling constants cancel on renormalization). """ @@ -96,7 +96,7 @@ def backward(hmm: HiddenMarkovModel, observations: Sequence[Any], *, scaled: boo beta[t] = 0.0 else: beta[t] = matrix @ beta[t + 1] - if scaled: + if normalize: total = float(beta[t].sum()) if total > 0.0: beta[t] = beta[t] / total diff --git a/sofic/inference/spectral.py b/sofic/inference/spectral.py index beccbd7..d24d454 100644 --- a/sofic/inference/spectral.py +++ b/sofic/inference/spectral.py @@ -49,7 +49,7 @@ "project_to_epsilon_machine", "project_to_mealy", "project_to_nmachine", - "spectral", + "learn_epsilon_machine_spectral", "spectral_singular_values", ] @@ -568,7 +568,7 @@ def register(state: MixedState) -> MixedState: ) -def spectral( +def learn_epsilon_machine_spectral( sequences: Iterable[Any] | None = None, *, word_probability: Callable[[Sequence[Any]], float] | None = None, diff --git a/sofic/serialization.py b/sofic/serialization.py index a01cdca..1d6d4ea 100644 --- a/sofic/serialization.py +++ b/sofic/serialization.py @@ -50,11 +50,6 @@ def model_from_yaml(text: str, *, validate: bool = True) -> StateMachine: return model_from_dict(loaded, validate=validate) -def from_yaml(text: str, *, validate: bool = True) -> StateMachine: - """Alias for :func:`model_from_yaml`.""" - return model_from_yaml(text, validate=validate) - - def read_yaml(path: str | Path, *, validate: bool = True) -> StateMachine: """Read a sofic model from a YAML file.""" return model_from_yaml(Path(path).read_text(encoding="utf-8"), validate=validate) @@ -308,9 +303,9 @@ def _specs() -> tuple[_ModelSpec, ...]: from sofic.automata.transducers import MealyMachine, MooreMachine from sofic.automata.unifilar import UnifilarAutomaton from sofic.automata.vpa import ( - CallDrivenAutomaton, CanonicalVisiblyPushdownAutomaton, DeterministicVisiblyPushdownAutomaton, + ModularVisiblyPushdownAutomaton, MultipleEntryVisiblyPushdownAutomaton, SingleEntryVisiblyPushdownAutomaton, VisiblyPushdownAutomaton, @@ -336,10 +331,10 @@ def _specs() -> tuple[_ModelSpec, ...]: WheelerCover, ) from sofic.shifts.markov_dyck import MarkovDyckShift + from sofic.shifts.product_alphabet_shift import ProductAlphabetShift from sofic.shifts.sft import ShiftOfFiniteType from sofic.shifts.sofic import SoficShift from sofic.shifts.sofic_dyck import SoficDyckShift - from sofic.shifts.sofic_relation import SoficRelation from sofic.shifts.tmc import TopologicalMarkovChain labeled = ("input_alphabet", "initial_states", "accepting_states") @@ -402,7 +397,7 @@ def _specs() -> tuple[_ModelSpec, ...]: _spec(NestedWordAutomaton, nwa), _spec(VisiblyPushdownAutomaton, vpa), _spec(DeterministicVisiblyPushdownAutomaton, vpa), - _spec(CallDrivenAutomaton, cda), + _spec(ModularVisiblyPushdownAutomaton, cda), _spec(MultipleEntryVisiblyPushdownAutomaton, (*cda, "entry_states")), _spec(SingleEntryVisiblyPushdownAutomaton, (*cda, "entry_states")), _spec(CanonicalVisiblyPushdownAutomaton, (*vpa, "summary_representatives")), @@ -440,7 +435,7 @@ def _specs() -> tuple[_ModelSpec, ...]: _spec(QuasiRealization, (*quasi, "pi", "tau", "symbol_maps")), _spec(SymbolicModel, symbolic), _spec(SoficShift, symbolic), - _spec(SoficRelation, symbolic), + _spec(ProductAlphabetShift, symbolic), _spec(TopologicalMarkovChain, symbolic), _spec(ShiftOfFiniteType, (*symbolic, "_forbidden_words", "_has_forbidden_word_spec"), builder="sft"), _spec(SoficDyckShift, dyck), diff --git a/sofic/shifts/__init__.py b/sofic/shifts/__init__.py index 157ad34..1012474 100644 --- a/sofic/shifts/__init__.py +++ b/sofic/shifts/__init__.py @@ -17,11 +17,11 @@ shift_to_dyck_graph_string, ) from sofic.shifts.markov_dyck import MarkovDyckShift +from sofic.shifts.product_alphabet_shift import ProductAlphabetShift from sofic.shifts.sft import ShiftOfFiniteType from sofic.shifts.sliding_block_code import SlidingBlockCode, full_shift from sofic.shifts.sofic import SoficShift from sofic.shifts.sofic_dyck import SoficDyckShift -from sofic.shifts.sofic_relation import SoficRelation from sofic.shifts.textile import TextileSystem from sofic.shifts.tmc import TopologicalMarkovChain from sofic.shifts.wheeler import ( @@ -49,7 +49,7 @@ "SoficShift", "shift_to_dyck_graph_string", "SoficDyckShift", - "SoficRelation", + "ProductAlphabetShift", "SymbolicModel", "TextileSystem", "TopologicalMarkovChain", diff --git a/sofic/shifts/cover_construction.py b/sofic/shifts/cover_construction.py index e1a0b5f..ddbf5f1 100644 --- a/sofic/shifts/cover_construction.py +++ b/sofic/shifts/cover_construction.py @@ -117,7 +117,7 @@ def _mirror(cls: type[SoficShift], cover: SoficShift) -> SoficShift: return cls(graph=cover.graph.reverse(), symbol_alphabet=cover.symbol_alphabet) -def right_fischer_from_sofic(shift: SoficShift) -> RightFischerCover: +def right_fischer_cover(shift: SoficShift) -> RightFischerCover: """Return the minimal right-resolving presentation of an irreducible sofic shift. Raises :class:`~sofic.exceptions.SoficValidationError` when the shift is @@ -146,9 +146,9 @@ def right_fischer_from_sofic(shift: SoficShift) -> RightFischerCover: return _quotient_shift(RightFischerCover, shift, vertices, delta, classes) -def left_fischer_from_sofic(shift: SoficShift) -> LeftFischerCover: +def left_fischer_cover(shift: SoficShift) -> LeftFischerCover: """Return the minimal left-resolving presentation (mirror of the right Fischer cover).""" - return _mirror(LeftFischerCover, right_fischer_from_sofic(shift.reverse())) + return _mirror(LeftFischerCover, right_fischer_cover(shift.reverse())) def _ray_terminal_sets(successors: dict[Hashable, dict[Any, set[Hashable]]]) -> set[Subset]: @@ -187,7 +187,7 @@ def prepend(symbol: Any, relation: Relation) -> Relation: return {frozenset(r for _q, r in relation) for relation in recurrent} -def right_krieger_from_sofic(shift: SoficShift) -> RightKriegerCover: +def right_krieger_cover(shift: SoficShift) -> RightKriegerCover: """Return the right Krieger (future) cover of ``shift``.""" trimmed = shift.trim_transient() successors = _labeled_successors(trimmed) @@ -199,6 +199,6 @@ def right_krieger_from_sofic(shift: SoficShift) -> RightKriegerCover: return _quotient_shift(RightKriegerCover, shift, vertices, delta, classes) -def left_krieger_from_sofic(shift: SoficShift) -> LeftKriegerCover: +def left_krieger_cover(shift: SoficShift) -> LeftKriegerCover: """Return the left Krieger (past) cover: the mirror of the right Krieger cover.""" - return _mirror(LeftKriegerCover, right_krieger_from_sofic(shift.reverse())) + return _mirror(LeftKriegerCover, right_krieger_cover(shift.reverse())) diff --git a/sofic/shifts/covers.py b/sofic/shifts/covers.py index 839e883..f9099d0 100644 --- a/sofic/shifts/covers.py +++ b/sofic/shifts/covers.py @@ -11,40 +11,40 @@ class LeftFischerCover(SoficShift): """Left Fischer cover presentation.""" @classmethod - def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> LeftFischerCover: - from sofic.shifts.cover_construction import left_fischer_from_sofic + def from_presentation(cls, shift: SoficShift, **kwargs: Any) -> LeftFischerCover: + from sofic.shifts.cover_construction import left_fischer_cover - return left_fischer_from_sofic(shift) + return left_fischer_cover(shift) class RightFischerCover(SoficShift): """Right Fischer cover presentation.""" @classmethod - def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> RightFischerCover: - from sofic.shifts.cover_construction import right_fischer_from_sofic + def from_presentation(cls, shift: SoficShift, **kwargs: Any) -> RightFischerCover: + from sofic.shifts.cover_construction import right_fischer_cover - return right_fischer_from_sofic(shift) + return right_fischer_cover(shift) class LeftKriegerCover(SoficShift): """Left Krieger cover presentation.""" @classmethod - def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> LeftKriegerCover: - from sofic.shifts.cover_construction import left_krieger_from_sofic + def from_presentation(cls, shift: SoficShift, **kwargs: Any) -> LeftKriegerCover: + from sofic.shifts.cover_construction import left_krieger_cover - return left_krieger_from_sofic(shift) + return left_krieger_cover(shift) class RightKriegerCover(SoficShift): """Right Krieger cover presentation.""" @classmethod - def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> RightKriegerCover: - from sofic.shifts.cover_construction import right_krieger_from_sofic + def from_presentation(cls, shift: SoficShift, **kwargs: Any) -> RightKriegerCover: + from sofic.shifts.cover_construction import right_krieger_cover - return right_krieger_from_sofic(shift) + return right_krieger_cover(shift) class WheelerCover(SoficShift): @@ -57,7 +57,7 @@ class WheelerCover(SoficShift): """ @classmethod - def from_sofic(cls, shift: SoficShift, **kwargs: Any) -> WheelerCover: + def from_presentation(cls, shift: SoficShift, **kwargs: Any) -> WheelerCover: from sofic.shifts.wheeler import wheeler_cover return wheeler_cover(shift, **kwargs) diff --git a/sofic/shifts/sofic_relation.py b/sofic/shifts/product_alphabet_shift.py similarity index 90% rename from sofic/shifts/sofic_relation.py rename to sofic/shifts/product_alphabet_shift.py index 3bc1d8a..add06b2 100644 --- a/sofic/shifts/sofic_relation.py +++ b/sofic/shifts/product_alphabet_shift.py @@ -1,7 +1,7 @@ -"""Sofic relations: subshifts over a product alphabet ``X x Y``. +"""Product-alphabet shifts: sofic subshifts over a product alphabet ``X x Y``. The topological support of a transducer is a subshift of the product shift on -``X x Y`` -- a "sofic relation" whose input and output projections are the +``X x Y`` -- a sofic relation whose input and output projections are the transducer's domain and range subshifts. This is the symbolic-dynamics reading of a transducer, complementary to the sliding block code (Lind & Marcus, *An Introduction to Symbolic Dynamics and Coding* (1995), ch. 6). @@ -15,11 +15,11 @@ from sofic.shifts.sofic import SoficShift -class SoficRelation(SoficShift): +class ProductAlphabetShift(SoficShift): """A sofic subshift whose symbols are ``(input, output)`` pairs.""" @classmethod - def from_transducer(cls, transducer: Any) -> SoficRelation: + def from_transducer(cls, transducer: Any) -> ProductAlphabetShift: """Build the topological support of a transducer (probabilities dropped).""" relation = cls() pairs: set[tuple[Any, Any]] = set() diff --git a/sofic/shifts/textile.py b/sofic/shifts/textile.py index 0a1f821..4c61f97 100644 --- a/sofic/shifts/textile.py +++ b/sofic/shifts/textile.py @@ -35,19 +35,19 @@ def to_transducer(self) -> Any: """Return the underlying Mealy machine.""" return self._transducer.copy() - def to_sofic_relation(self) -> Any: + def to_product_alphabet_shift(self) -> Any: """Return the product-alphabet subshift of paired labels.""" - from sofic.shifts.sofic_relation import SoficRelation + from sofic.shifts.product_alphabet_shift import ProductAlphabetShift - return SoficRelation.from_transducer(self._transducer) + return ProductAlphabetShift.from_transducer(self._transducer) def input_shift(self) -> SoficShift: """Return the input subshift (``p`` labeling).""" - return self.to_sofic_relation().input_shift() + return self.to_product_alphabet_shift().input_shift() def output_shift(self) -> SoficShift: """Return the output subshift (``q`` labeling).""" - return self.to_sofic_relation().output_shift() + return self.to_product_alphabet_shift().output_shift() def induced_code(self, *, max_window: int = 4) -> SlidingBlockCode: """Return the induced sliding block code (memory only), if it has finite window. diff --git a/sofic/shifts/topological_anatomy.py b/sofic/shifts/topological_anatomy.py index 3909ec2..a2f07a9 100644 --- a/sofic/shifts/topological_anatomy.py +++ b/sofic/shifts/topological_anatomy.py @@ -85,7 +85,7 @@ def _right_resolving(shift: SoficShift) -> SoficShift: """Return a right-resolving (unifilar) presentation of ``shift``. If ``shift`` is already unifilar it is returned unchanged. Otherwise the right - Fischer cover (:meth:`~sofic.shifts.covers.RightFischerCover.from_sofic`) is + Fischer cover (:meth:`~sofic.shifts.covers.RightFischerCover.from_presentation`) is built and its duplicate labeled edges merged (:func:`_dedup_symbol_edges`). The cover is exact for irreducible shifts; a reducible non-unifilar presentation raises :class:`~sofic.exceptions.SoficValidationError` from the @@ -95,7 +95,7 @@ def _right_resolving(shift: SoficShift) -> SoficShift: return shift from sofic.shifts.covers import RightFischerCover - cover = _dedup_symbol_edges(RightFischerCover.from_sofic(shift)) + cover = _dedup_symbol_edges(RightFischerCover.from_presentation(shift)) if not cover.is_unifilar(): raise UnifilarityError( "could not derive a right-resolving presentation of the sofic shift " diff --git a/tests/test_bayesian_inference.py b/tests/test_bayesian_inference.py index c3e6c24..da2a9f4 100644 --- a/tests/test_bayesian_inference.py +++ b/tests/test_bayesian_inference.py @@ -5,7 +5,8 @@ import numpy as np import pytest -from sofic.examples.processes import SNS, Even +from sofic.examples import even_process +from sofic.examples.processes import sns from sofic.inference.bayesian import ( BayesianInferenceError, InferEM, @@ -58,22 +59,22 @@ def test_model_comparison_mc_probabilities_normalize(): def test_path_count_em_rejects_nonunifilar_topology(): with pytest.raises(BayesianInferenceError): - PathCountEM(SNS(), list("01")) + PathCountEM(sns(), list("01")) def test_path_count_em_counts_even_process(): - data = list("1111101100") - counts = PathCountEM(Even(), data) + data = [int(symbol) for symbol in "1111101100"] + counts = PathCountEM(even_process(), data) assert counts.get_possible_start_nodes() == ["B"] - assert counts.get_edge_count("B", ("A", "0")) == 3 - assert counts.get_edge_count("B", ("A", "1")) == 3 - assert counts.get_edge_count("B", ("B", "1")) == 4 + assert counts.get_edge_count("B", ("A", 0)) == 3 + assert counts.get_edge_count("B", ("A", 1)) == 3 + assert counts.get_edge_count("B", ("B", 1)) == 4 assert counts.get_node_count("B", "A") == 6 assert counts.get_node_count("B", "B") == 4 def test_infer_em_start_marginalization_and_sample(): - posterior = InferEM(Even(), list("1111101100")) + posterior = InferEM(even_process(), [int(symbol) for symbol in "1111101100"]) assert posterior.start_node_probabilities() == {"B": pytest.approx(1.0)} assert posterior.log_evidence() < 0 start, machine = posterior.generate_sample(rng=np.random.default_rng(0)) diff --git a/tests/test_channel_measures.py b/tests/test_channel_measures.py index 9c5c801..f40589e 100644 --- a/tests/test_channel_measures.py +++ b/tests/test_channel_measures.py @@ -3,7 +3,7 @@ import pytest from sofic import EpsilonTransducer, MealyHMM -from sofic.examples.processes import BinaryChannel, Delay +from sofic.examples.processes import binary_channel, delay from sofic.generators.channel_measures import ( channel_statistical_complexity, directed_information, @@ -22,34 +22,34 @@ def _iid_input() -> MealyHMM: def test_memoryless_channel_zero_complexity(): - eps = EpsilonTransducer.from_channel(BinaryChannel(0.1, 0.2)) + eps = EpsilonTransducer.from_channel(binary_channel(0.1, 0.2)) assert channel_statistical_complexity(eps, _iid_input()) == pytest.approx(0.0, abs=1e-9) def test_memoryless_channel_zero_transfer_entropy(): - eps = EpsilonTransducer.from_channel(BinaryChannel(0.1, 0.2)) + eps = EpsilonTransducer.from_channel(binary_channel(0.1, 0.2)) assert transfer_entropy(eps, _iid_input()) == pytest.approx(0.0, abs=1e-9) def test_memory_channel_positive_complexity(): - eps = EpsilonTransducer.from_channel(Delay(1)) + eps = EpsilonTransducer.from_channel(delay(1)) assert channel_statistical_complexity(eps, _iid_input()) > 0.5 def test_driven_entropy_rate_bounds(): - eps = EpsilonTransducer.from_channel(BinaryChannel(0.1, 0.2)) + eps = EpsilonTransducer.from_channel(binary_channel(0.1, 0.2)) rate = driven_entropy_rate(eps, _iid_input()) assert 0.0 <= rate <= 1.0 + 1e-9 def test_directed_information_positive(): - eps = EpsilonTransducer.from_channel(BinaryChannel(0.1, 0.2)) + eps = EpsilonTransducer.from_channel(binary_channel(0.1, 0.2)) assert directed_information(eps, _iid_input(), length=2) > 0.0 def test_complete_skips_unused_reject_sink(): """Already-total channels must not gain an unused absorbing ``?`` state.""" - eps = EpsilonTransducer.from_channel(BinaryChannel(0.1, 0.2)) + eps = EpsilonTransducer.from_channel(binary_channel(0.1, 0.2)) completed = eps.complete(frozenset({"0", "1"})) assert "?" not in completed.states() assert list(completed.states()) == list(eps.states()) @@ -58,22 +58,22 @@ def test_complete_skips_unused_reject_sink(): def test_directed_information_ignores_unreachable_error_class(): """Reducible joints with an unused error sink must keep positive DI. - ``compose_tg(..., complete=True)`` used to always attach an absorbing ``?`` + ``compose_transducer_generator(..., complete=True)`` used to always attach an absorbing ``?`` component; the eigenvector stationary law is then non-unique and can put all mass on ``?`` (PYTHONHASHSEED-dependent), zeroing directed information. """ - from sofic.automata.transducer_operations import compose_tg + from sofic.automata.transducer_operations import compose_transducer_generator from sofic.generators.directional_flow import directed_information as di_flow from sofic.generators.matrices import emission_tensors - eps = EpsilonTransducer.from_channel(BinaryChannel(0.1, 0.2)) + eps = EpsilonTransducer.from_channel(binary_channel(0.1, 0.2)) # Force the historical reducible joint even after complete() stops adding an # unused sink: compose with an explicit completed copy that includes ``?``. completed = eps.copy() completed.graph.add_state("?") completed.add_transition("?", "?", "0", "?", prob=1.0) completed.add_transition("?", "?", "1", "?", prob=1.0) - joint = compose_tg(completed, _iid_input(), joint=True, complete=False) + joint = compose_transducer_generator(completed, _iid_input(), joint=True, complete=False) assert ("S", "?") in list(joint.states()) pi, _tensors = emission_tensors(joint, policy="stationary") idx = joint.reindex() @@ -83,7 +83,7 @@ def test_directed_information_ignores_unreachable_error_class(): def test_method_dispatch_matches_functions(): - eps = EpsilonTransducer.from_channel(BinaryChannel(0.1, 0.2)) + eps = EpsilonTransducer.from_channel(binary_channel(0.1, 0.2)) inp = _iid_input() assert eps.statistical_complexity(inp) == pytest.approx(channel_statistical_complexity(eps, inp)) assert eps.transfer_entropy(inp) == pytest.approx(transfer_entropy(eps, inp)) diff --git a/tests/test_covers.py b/tests/test_covers.py index b1129dc..20d2044 100644 --- a/tests/test_covers.py +++ b/tests/test_covers.py @@ -53,7 +53,7 @@ def _is_left_resolving(shift: SoficShift) -> bool: @pytest.mark.parametrize("builder", [_golden_mean, _even_shift, _nondeterministic_even_shift]) def test_right_fischer_cover_is_minimal_right_resolving_and_presents_shift(builder): shift = builder() - cover = RightFischerCover.from_sofic(shift) + cover = RightFischerCover.from_presentation(shift) cover.validate() assert cover.is_unifilar() assert nx.is_strongly_connected(cover.graph.nx) @@ -64,15 +64,15 @@ def test_right_fischer_cover_is_minimal_right_resolving_and_presents_shift(build @pytest.mark.parametrize("builder", [_golden_mean, _even_shift, _nondeterministic_even_shift]) def test_left_fischer_cover_is_left_resolving_and_presents_shift(builder): shift = builder() - cover = LeftFischerCover.from_sofic(shift) + cover = LeftFischerCover.from_presentation(shift) assert _is_left_resolving(cover) assert _language(cover) == _language(shift) def test_even_shift_krieger_cover_has_three_vertices_and_contains_fischer_cover(): shift = _even_shift() - krieger = RightKriegerCover.from_sofic(shift) - fischer = RightFischerCover.from_sofic(shift) + krieger = RightKriegerCover.from_presentation(shift) + fischer = RightFischerCover.from_presentation(shift) assert krieger.is_unifilar() assert len(list(krieger.states())) == 3 assert _language(krieger) == _language(shift) @@ -84,14 +84,14 @@ def test_even_shift_krieger_cover_has_three_vertices_and_contains_fischer_cover( def test_golden_mean_krieger_cover_equals_fischer_cover_size(): shift = _golden_mean() - assert len(list(RightKriegerCover.from_sofic(shift).states())) == 2 - assert len(list(LeftKriegerCover.from_sofic(shift).states())) == 2 + assert len(list(RightKriegerCover.from_presentation(shift).states())) == 2 + assert len(list(LeftKriegerCover.from_presentation(shift).states())) == 2 @pytest.mark.parametrize("builder", [_golden_mean, _even_shift, _nondeterministic_even_shift]) def test_left_krieger_cover_is_left_resolving_and_presents_shift(builder): shift = builder() - cover = LeftKriegerCover.from_sofic(shift) + cover = LeftKriegerCover.from_presentation(shift) assert _is_left_resolving(cover) assert _language(cover) == _language(shift) @@ -99,6 +99,6 @@ def test_left_krieger_cover_is_left_resolving_and_presents_shift(builder): def test_fischer_cover_rejects_reducible_shift(): reducible = _shift([("A", "A", "0"), ("B", "B", "1")]) with pytest.raises(SoficValidationError, match="irreducible"): - RightFischerCover.from_sofic(reducible) - krieger = RightKriegerCover.from_sofic(reducible) + RightFischerCover.from_presentation(reducible) + krieger = RightKriegerCover.from_presentation(reducible) assert _language(krieger) == _language(reducible) diff --git a/tests/test_edge_machine.py b/tests/test_edge_machine.py index 62da4e5..9f3eecf 100644 --- a/tests/test_edge_machine.py +++ b/tests/test_edge_machine.py @@ -71,14 +71,14 @@ def test_edge_machine_preserves_block_distribution(builder): hmm = builder() edge = hmm.to_edge_machine() symbols = sorted(hmm.observation_alphabet, key=repr) - dist_hmm0 = hmm.joint_block_distribution(history_length=0) - dist_edge0 = edge.joint_block_distribution(history_length=0) + dist_hmm0 = hmm.joint_block_distribution(block_length=1) + dist_edge0 = edge.joint_block_distribution(block_length=1) for symbol in symbols: outcome = (symbol,) assert dist_hmm0[outcome] == pytest.approx(dist_edge0[outcome], abs=1e-9) - dist_hmm1 = hmm.joint_block_distribution(history_length=1) - dist_edge1 = edge.joint_block_distribution(history_length=1) + dist_hmm1 = hmm.joint_block_distribution(block_length=2) + dist_edge1 = edge.joint_block_distribution(block_length=2) for past in symbols: for present in symbols: outcome = (past, present) diff --git a/tests/test_epsilon_inference.py b/tests/test_epsilon_inference.py index 0519367..66a49dc 100644 --- a/tests/test_epsilon_inference.py +++ b/tests/test_epsilon_inference.py @@ -13,8 +13,8 @@ from sofic.generators.epsilon_machine import EpsilonMachine from sofic.generators.sampling import sample from sofic.graph import ATTR_EMISSION, ATTR_PROB -from sofic.inference.cssr import cssr, subtree_merge -from sofic.inference.spectral import spectral +from sofic.inference.cssr import learn_epsilon_machine_cssr, learn_epsilon_machine_subtree +from sofic.inference.spectral import learn_epsilon_machine_spectral def _transition_signature(hmm: EpsilonMachine) -> dict[Hashable, tuple[tuple[Any, Hashable, float], ...]]: @@ -86,7 +86,7 @@ def rng() -> np.random.Generator: def test_cssr_bernoulli_single_state(rng: np.random.Generator): oracle = bernoulli(0.3) observations, _ = sample(oracle, 500, rng) - inferred = cssr(observations, alpha=0.01) + inferred = learn_epsilon_machine_cssr(observations, alpha=0.01) inferred.validate() assert len(list(inferred.states())) == 1 @@ -102,7 +102,7 @@ def test_from_sequence_cssr_dispatch(rng: np.random.Generator): def test_cssr_golden_mean_recovers_two_states(rng: np.random.Generator): oracle = golden_mean(0.5) observations, _ = sample(oracle, 8000, rng) - inferred = cssr(observations, Lmax=3, alpha=0.001) + inferred = learn_epsilon_machine_cssr(observations, max_history=3, alpha=0.001) inferred.validate() assert len(list(inferred.states())) == 2 assert _signatures_isomorphic(inferred, oracle, prob_tol=0.1) @@ -111,7 +111,7 @@ def test_cssr_golden_mean_recovers_two_states(rng: np.random.Generator): def test_cssr_even_process_recovers_two_states(rng: np.random.Generator): oracle = even_process(0.5) observations, _ = sample(oracle, 12000, rng) - inferred = cssr(observations, Lmax=4, alpha=0.001) + inferred = learn_epsilon_machine_cssr(observations, max_history=4, alpha=0.001) inferred.validate() assert len(list(inferred.states())) == 2 assert _signatures_isomorphic(inferred, oracle, prob_tol=0.12) @@ -120,7 +120,7 @@ def test_cssr_even_process_recovers_two_states(rng: np.random.Generator): def test_subtree_merge_golden_mean(rng: np.random.Generator): oracle = golden_mean(0.5) observations, _ = sample(oracle, 10000, rng) - inferred = subtree_merge(observations, L=2, delta=0.0) + inferred = learn_epsilon_machine_subtree(observations, max_history=2, delta=0.0) inferred.validate() assert len(list(inferred.states())) == 2 assert _signatures_isomorphic(inferred, oracle, prob_tol=0.12) @@ -129,7 +129,7 @@ def test_subtree_merge_golden_mean(rng: np.random.Generator): def test_from_sequence_subtree_dispatch(rng: np.random.Generator): oracle = golden_mean(0.5) observations, _ = sample(oracle, 6000, rng) - inferred = EpsilonMachine.from_sequence(observations, method="subtree", L=2) + inferred = EpsilonMachine.from_sequence(observations, method="subtree", max_history=2) inferred.validate() assert len(list(inferred.states())) >= 2 @@ -143,7 +143,7 @@ def test_cssr_initial_distribution_matches_occupation(rng: np.random.Generator): """ oracle = golden_mean(0.5) observations, _ = sample(oracle, 8000, rng) - inferred = cssr(observations, Lmax=3, alpha=0.001) + inferred = learn_epsilon_machine_cssr(observations, max_history=3, alpha=0.001) inferred.validate() idx = inferred.reindex() @@ -155,18 +155,20 @@ def test_cssr_initial_distribution_matches_occupation(rng: np.random.Generator): def test_cssr_short_sequence_raises(): with pytest.raises(ValueError, match="at least two"): - cssr([0]) + learn_epsilon_machine_cssr([0]) def test_subtree_merge_short_sequence_raises(): with pytest.raises(ValueError, match="at least two"): - subtree_merge([1], L=1) + learn_epsilon_machine_subtree([1], max_history=1) def test_spectral_bernoulli_single_state(): oracle = bernoulli(0.3) alphabet = sorted(oracle.observation_alphabet, key=repr) - inferred = spectral(word_probability=oracle.word_probability, alphabet=alphabet, prefix_length=2, rank=1) + inferred = learn_epsilon_machine_spectral( + word_probability=oracle.word_probability, alphabet=alphabet, prefix_length=2, rank=1 + ) inferred.validate() assert len(list(inferred.states())) == 1 assert inferred.entropy_rate() == pytest.approx(oracle.entropy_rate(), abs=1e-9) @@ -175,7 +177,9 @@ def test_spectral_bernoulli_single_state(): def test_spectral_golden_mean_recovers_two_states(): oracle = golden_mean(0.5) alphabet = sorted(oracle.observation_alphabet, key=repr) - inferred = spectral(word_probability=oracle.word_probability, alphabet=alphabet, prefix_length=3, rank=2) + inferred = learn_epsilon_machine_spectral( + word_probability=oracle.word_probability, alphabet=alphabet, prefix_length=3, rank=2 + ) inferred.validate() assert len(list(inferred.states())) == 2 assert inferred.entropy_rate() == pytest.approx(oracle.entropy_rate(), abs=1e-6) @@ -186,7 +190,9 @@ def test_spectral_golden_mean_recovers_two_states(): def test_spectral_even_process_recovers_two_states(): oracle = even_process(0.5) alphabet = sorted(oracle.observation_alphabet, key=repr) - inferred = spectral(word_probability=oracle.word_probability, alphabet=alphabet, prefix_length=3, rank=2) + inferred = learn_epsilon_machine_spectral( + word_probability=oracle.word_probability, alphabet=alphabet, prefix_length=3, rank=2 + ) inferred.validate() assert len(list(inferred.states())) == 2 assert inferred.entropy_rate() == pytest.approx(oracle.entropy_rate(), abs=1e-6) @@ -208,17 +214,17 @@ def test_from_sequence_unknown_method(): @pytest.mark.parametrize( - ("name", "Lmax"), - [("Even", 3), ("Even", 5), ("GoldenMean", 3), ("Nemo", 4), ("RkGM", 5)], + ("name", "max_history"), + [("even_process", 3), ("even_process", 5), ("golden_mean_forbid_00", 3), ("nemo_process", 4), ("rk_gm", 5)], ) -def test_cssr_recovers_synchronizable_processes(name: str, Lmax: int): +def test_cssr_recovers_synchronizable_processes(name: str, max_history: int): """Regression: appended (not prepended) suffixes and untruncated successors dropped edges, so these raised StochasticValidationError or returned h_mu = 0.""" - from sofic.examples import processes + from sofic import examples - oracle = processes.RkGM(5, 3) if name == "RkGM" else getattr(processes, name)() + oracle = examples.rk_gm(5, 3) if name == "rk_gm" else getattr(examples, name)() observations, _ = sample(oracle, 20000, np.random.default_rng(5)) - inferred = cssr(observations, Lmax=Lmax, alpha=0.001) + inferred = learn_epsilon_machine_cssr(observations, max_history=max_history, alpha=0.001) inferred.validate() assert inferred.is_unifilar() assert len(list(inferred.states())) == len(list(oracle.states())) @@ -226,51 +232,53 @@ def test_cssr_recovers_synchronizable_processes(name: str, Lmax: int): def test_cssr_even_process_ignores_truncated_nonsynchronizing_suffix(): - """At Lmax = 3 the successor of ``011`` on ``1`` truncates to the ambiguous ``111``.""" + """At max_history = 3 the successor of ``011`` on ``1`` truncates to the ambiguous ``111``.""" oracle = even_process(0.5) observations, _ = sample(oracle, 20000, np.random.default_rng(5)) - inferred = cssr(observations, Lmax=3, alpha=0.01) + inferred = learn_epsilon_machine_cssr(observations, max_history=3, alpha=0.01) assert _signatures_isomorphic(inferred, oracle, prob_tol=0.03) def test_cssr_default_lmax_does_not_oversplit(): for oracle, n_states in [(even_process(0.5), 2), (bernoulli(0.3), 1)]: observations, _ = sample(oracle, 20000, np.random.default_rng(7)) - inferred = cssr(observations) + inferred = learn_epsilon_machine_cssr(observations) inferred.validate() assert len(list(inferred.states())) == n_states def test_cssr_short_lmax_still_emits_every_symbol(): - """Lmax below the Markov order cannot recover RkGM(5, 3), but must not collapse to a trap state.""" + """max_history below the Markov order cannot recover rk_gm(5, 3), but must not collapse to a trap state.""" from sofic.examples import processes - observations, _ = sample(processes.RkGM(5, 3), 20000, np.random.default_rng(5)) - inferred = cssr(observations, Lmax=3, alpha=0.001) + observations, _ = sample(processes.rk_gm(5, 3), 20000, np.random.default_rng(5)) + inferred = learn_epsilon_machine_cssr(observations, max_history=3, alpha=0.001) inferred.validate() assert {t.data[ATTR_EMISSION] for t in inferred.transitions()} == {"0", "1"} assert inferred.entropy_rate() > 0.0 def test_cssr_non_synchronizable_process_returns_valid_machine(): - from sofic.examples import processes + from sofic.examples import alternating_biased_coins - oracle = processes.ABC() + oracle = alternating_biased_coins(0.25, 0.75) observations, _ = sample(oracle, 20000, np.random.default_rng(5)) - inferred = cssr(observations, Lmax=4, alpha=0.001) + inferred = learn_epsilon_machine_cssr(observations, max_history=4, alpha=0.001) inferred.validate() assert inferred.entropy_rate() >= oracle.entropy_rate() - 0.02 -@pytest.mark.parametrize(("name", "L", "n_states"), [("Even", 3, 2), ("GoldenMean", 2, 2), ("RkGM", 5, 8)]) +@pytest.mark.parametrize( + ("name", "L", "n_states"), [("even_process", 3, 2), ("golden_mean_forbid_00", 2, 2), ("rk_gm", 5, 8)] +) def test_subtree_merge_default_delta_recovers_process(name: str, L: int, n_states: int): """Regression: the default delta = 0 compared sampled morphs to within 1e-3 and successors were never truncated, so this raised StochasticValidationError.""" - from sofic.examples import processes + from sofic import examples - oracle = processes.RkGM(5, 3) if name == "RkGM" else getattr(processes, name)() + oracle = examples.rk_gm(5, 3) if name == "rk_gm" else getattr(examples, name)() observations, _ = sample(oracle, 20000, np.random.default_rng(5)) - inferred = subtree_merge(observations, L=L) + inferred = learn_epsilon_machine_subtree(observations, max_history=L) inferred.validate() assert len(list(inferred.states())) == n_states assert inferred.entropy_rate() == pytest.approx(oracle.entropy_rate(), abs=0.02) @@ -290,28 +298,28 @@ def _has_markov_order_selection() -> bool: @needs_markov_order def test_suggest_lmax_markov_sources(): from sofic.examples import processes - from sofic.inference.cssr import suggest_lmax + from sofic.inference.cssr import suggest_max_history observations, _ = sample(golden_mean(0.5), 4000, np.random.default_rng(1)) - assert suggest_lmax(observations) == 1 - observations, _ = sample(processes.RkGM(3, 2), 20000, np.random.default_rng(2)) - assert suggest_lmax(observations, method="bic") == 3 + assert suggest_max_history(observations) == 1 + observations, _ = sample(processes.rk_gm(3, 2), 20000, np.random.default_rng(2)) + assert suggest_max_history(observations, method="bic") == 3 @needs_markov_order def test_suggest_lmax_grows_for_even_process(): """The even process has infinite Markov order, so the suggestion grows with data.""" - from sofic.inference.cssr import suggest_lmax + from sofic.inference.cssr import suggest_max_history short, _ = sample(even_process(0.5), 300, np.random.default_rng(3)) long, _ = sample(even_process(0.5), 30000, np.random.default_rng(3)) - assert suggest_lmax(long, method="bic") > suggest_lmax(short, method="bic") + assert suggest_max_history(long, method="bic") > suggest_max_history(short, method="bic") @needs_markov_order def test_cssr_auto_lmax_golden_mean(rng: np.random.Generator): observations, _ = sample(golden_mean(0.5), 8000, rng) - inferred = cssr(observations, Lmax="auto", alpha=0.001) + inferred = learn_epsilon_machine_cssr(observations, max_history="auto", alpha=0.001) assert len(list(inferred.states())) == 2 assert _signatures_isomorphic(inferred, golden_mean(0.5), prob_tol=0.1) @@ -337,31 +345,31 @@ def test_exact_morph_test_small_counts(): def test_exact_morph_test_is_deterministic(rng: np.random.Generator): observations, _ = sample(golden_mean(0.5), 3000, rng) - first = cssr(observations, Lmax=3, alpha=0.01, test="exact") - second = cssr(observations, Lmax=3, alpha=0.01, test="exact") + first = learn_epsilon_machine_cssr(observations, max_history=3, alpha=0.01, test="exact") + second = learn_epsilon_machine_cssr(observations, max_history=3, alpha=0.01, test="exact") assert _transition_signature(first) == _transition_signature(second) assert len(list(first.states())) == 2 def test_cssr_bonferroni_reduces_spurious_states(): - """An i.i.d. source with a long Lmax: the corrected test keeps a single state.""" + """An i.i.d. source with a long max_history: the corrected test keeps a single state.""" observations, _ = sample(bernoulli(0.3), 3000, np.random.default_rng(6)) - inferred = cssr(observations, Lmax=6, alpha=0.05, correction="bonferroni") + inferred = learn_epsilon_machine_cssr(observations, max_history=6, alpha=0.05, correction="bonferroni") assert len(list(inferred.states())) == 1 with pytest.raises(ValueError, match="unknown correction"): - cssr(observations, Lmax=2, correction="holm") + learn_epsilon_machine_cssr(observations, max_history=2, correction="holm") @pytest.mark.parametrize("kwargs", [{"test": "exact"}, {"correction": "bonferroni"}, {"alpha": 0.001}]) def test_subtree_merge_options_golden_mean(kwargs): observations, _ = sample(golden_mean(0.5), 6000, np.random.default_rng(7)) - inferred = subtree_merge(observations, L=2, **kwargs) + inferred = learn_epsilon_machine_subtree(observations, max_history=2, **kwargs) assert len(list(inferred.states())) == 2 with pytest.raises(ValueError, match="unknown correction"): - subtree_merge(observations, L=2, correction="holm") + learn_epsilon_machine_subtree(observations, max_history=2, correction="holm") @needs_markov_order def test_subtree_merge_auto_depth(): observations, _ = sample(golden_mean(0.5), 6000, np.random.default_rng(8)) - assert len(list(subtree_merge(observations, L="auto").states())) == 2 + assert len(list(learn_epsilon_machine_subtree(observations, max_history="auto").states())) == 2 diff --git a/tests/test_epsilon_transducer.py b/tests/test_epsilon_transducer.py index 9d7ce8b..0a82d7f 100644 --- a/tests/test_epsilon_transducer.py +++ b/tests/test_epsilon_transducer.py @@ -3,8 +3,8 @@ import pytest from sofic import EpsilonTransducer, MealyHMM -from sofic.automata.transducer_operations import compose_tg -from sofic.examples.processes import GME, RCT, BinaryChannel, GMtoEven +from sofic.automata.transducer_operations import compose_transducer_generator +from sofic.examples.processes import binary_channel, gm_to_even, gme, rct from sofic.exceptions import StochasticValidationError, UnifilarityError from sofic.graph import ATTR_OUTPUT, ATTR_PROB, ATTR_SYMBOL @@ -21,10 +21,10 @@ def _iid_input() -> MealyHMM: @pytest.mark.parametrize( ("channel", "expected_states"), [ - (BinaryChannel(0.1, 0.2), 1), - (GMtoEven(), 2), - (RCT(0.5), 3), - (GME(), 2), + (binary_channel(0.1, 0.2), 1), + (gm_to_even(), 2), + (rct(0.5), 3), + (gme(), 2), ], ) def test_from_channel_minimizes(channel, expected_states): @@ -35,7 +35,7 @@ def test_from_channel_minimizes(channel, expected_states): def test_memoryless_channel_is_single_causal_state(): - eps = EpsilonTransducer.from_channel(BinaryChannel(0.1, 0.2)) + eps = EpsilonTransducer.from_channel(binary_channel(0.1, 0.2)) assert len(eps.causal_states()) == 1 rows = {} for transition in eps.transitions(): @@ -48,7 +48,7 @@ def test_memoryless_channel_is_single_causal_state(): def test_initial_distribution_normalized(): - eps = EpsilonTransducer.from_channel(GMtoEven()) + eps = EpsilonTransducer.from_channel(gm_to_even()) assert sum(eps.initial_distribution.values()) == pytest.approx(1.0) assert set(eps.initial_distribution) <= set(eps.states()) @@ -79,7 +79,7 @@ def _non_unifilar_channel(): def test_validate_rejects_bad_initial_distribution(): - eps = EpsilonTransducer.from_channel(BinaryChannel(0.1, 0.2)) + eps = EpsilonTransducer.from_channel(binary_channel(0.1, 0.2)) eps.initial_distribution = {next(iter(eps.states())): 0.5} with pytest.raises(StochasticValidationError): eps.validate() @@ -103,7 +103,7 @@ def test_validate_rejects_non_unifilar_direct(): def test_from_joint_generator_recovers_memoryless(): - joint = compose_tg(BinaryChannel(0.1, 0.2), _iid_input(), joint=True) + joint = compose_transducer_generator(binary_channel(0.1, 0.2), _iid_input(), joint=True) eps = EpsilonTransducer.from_joint_generator(joint) eps.validate() assert len(list(eps.states())) == 1 @@ -111,13 +111,13 @@ def test_from_joint_generator_recovers_memoryless(): def test_from_iohmm_alias(): - eps = EpsilonTransducer.from_iohmm(GMtoEven()) + eps = EpsilonTransducer.from_iohmm(gm_to_even()) assert isinstance(eps, EpsilonTransducer) assert len(list(eps.states())) == 2 def test_yaml_round_trip(): - eps = EpsilonTransducer.from_channel(RCT(0.5)) + eps = EpsilonTransducer.from_channel(rct(0.5)) restored = EpsilonTransducer.from_yaml(eps.to_yaml()) assert isinstance(restored, EpsilonTransducer) assert len(list(restored.states())) == len(list(eps.states())) @@ -125,7 +125,7 @@ def test_yaml_round_trip(): def test_wfst_round_trip_preserves_structure(): - eps = EpsilonTransducer.from_channel(GMtoEven()) + eps = EpsilonTransducer.from_channel(gm_to_even()) wfst = eps.to_wfst() recovered = EpsilonTransducer.from_wfst(wfst) assert len(list(recovered.states())) == len(list(eps.states())) diff --git a/tests/test_epsilon_transducer_inference.py b/tests/test_epsilon_transducer_inference.py index 9c331ba..9a84939 100644 --- a/tests/test_epsilon_transducer_inference.py +++ b/tests/test_epsilon_transducer_inference.py @@ -7,9 +7,9 @@ from hypothesis.extra import numpy as hnp from sofic import EpsilonTransducer, MealyHMM -from sofic.automata.transducer_operations import compose_tg -from sofic.examples.processes import BinaryChannel, Delay -from sofic.inference.cssr import JointSuffixCounts, transcssr +from sofic.automata.transducer_operations import compose_transducer_generator +from sofic.examples.processes import binary_channel, delay +from sofic.inference.cssr import JointSuffixCounts, learn_epsilon_transducer_cssr def _iid_input() -> MealyHMM: @@ -22,7 +22,7 @@ def _iid_input() -> MealyHMM: def _paired_samples(channel, n, seed): - joint = compose_tg(channel, _iid_input(), joint=True) + joint = compose_transducer_generator(channel, _iid_input(), joint=True) observations, _ = joint.sample(n, np.random.default_rng(seed)) xs = [pair[0] for pair in observations] ys = [pair[1] for pair in observations] @@ -30,7 +30,7 @@ def _paired_samples(channel, n, seed): def _memoryless_reconstruction(n: int = 20000, *, max_seeds: int = 8) -> EpsilonTransducer: - """Recover a single-state ε-transducer for ``BinaryChannel(0.1, 0.2)``. + """Recover a single-state ε-transducer for ``binary_channel(0.1, 0.2)``. CSSR's χ² split decision is float-sensitive across platforms, so a fixed ``(n, seed)`` can over-split on some runners. Cap history depth at 1 (enough @@ -38,13 +38,13 @@ def _memoryless_reconstruction(n: int = 20000, *, max_seeds: int = 8) -> Epsilon single-state. """ for seed in range(max_seeds): - xs, ys = _paired_samples(BinaryChannel(0.1, 0.2), n, seed=seed) - candidate = transcssr( + xs, ys = _paired_samples(binary_channel(0.1, 0.2), n, seed=seed) + candidate = learn_epsilon_transducer_cssr( xs, ys, input_alphabet=("0", "1"), output_alphabet=("0", "1"), - Lmax=1, + max_history=1, ) if len(list(candidate.states())) == 1: return candidate @@ -68,7 +68,7 @@ def test_recovers_memoryless_channel(): @pytest.mark.parametrize("seed", [0, 1, 2]) def test_recovers_delay_memory(seed): - xs, ys = _paired_samples(Delay(1), 10000, seed=seed) + xs, ys = _paired_samples(delay(1), 10000, seed=seed) eps = EpsilonTransducer.from_paired_sequences(xs, ys, input_alphabet=("0", "1"), output_alphabet=("0", "1")) eps.validate() assert len(list(eps.states())) == 2 @@ -86,7 +86,7 @@ def test_reconstruction_reproduces_conditional_law(): def test_rejects_mismatched_lengths(): with pytest.raises(ValueError): - transcssr("010", "01") + learn_epsilon_transducer_cssr("010", "01") def _held_out_bits_per_symbol(eps: EpsilonTransducer, xs, ys, burn: int = 20) -> float: @@ -110,12 +110,12 @@ def _held_out_bits_per_symbol(eps: EpsilonTransducer, xs, ys, burn: int = 20) -> def test_recovers_two_step_delay(): """Regression: joint suffixes grew forward and successors were never truncated, - so Delay(2) gave 5-19 states that forbade valid input-output pairs.""" - xs, ys = _paired_samples(Delay(2), 10000, seed=0) - eps = transcssr(xs, ys, input_alphabet=("0", "1"), output_alphabet=("0", "1")) + so delay(2) gave 5-19 states that forbade valid input-output pairs.""" + xs, ys = _paired_samples(delay(2), 10000, seed=0) + eps = learn_epsilon_transducer_cssr(xs, ys, input_alphabet=("0", "1"), output_alphabet=("0", "1")) eps.validate() assert len(list(eps.states())) == 4 - test_xs, test_ys = _paired_samples(Delay(2), 3000, seed=1) + test_xs, test_ys = _paired_samples(delay(2), 3000, seed=1) assert _held_out_bits_per_symbol(eps, test_xs, test_ys) == pytest.approx(0.0, abs=1e-9) @@ -127,27 +127,37 @@ def _has_markov_order_selection() -> bool: @pytest.mark.parametrize("seed", [0, 1]) def test_exact_and_bonferroni_recover_delay_memory(seed): - xs, ys = _paired_samples(Delay(1), 6000, seed=seed) + xs, ys = _paired_samples(delay(1), 6000, seed=seed) for kwargs in ({"test": "exact"}, {"correction": "bonferroni"}): - eps = transcssr(xs, ys, input_alphabet=("0", "1"), output_alphabet=("0", "1"), Lmax=2, **kwargs) + eps = learn_epsilon_transducer_cssr( + xs, ys, input_alphabet=("0", "1"), output_alphabet=("0", "1"), max_history=2, **kwargs + ) eps.validate() assert len(list(eps.states())) == 2 def test_bonferroni_keeps_memoryless_channel_single_state(): - xs, ys = _paired_samples(BinaryChannel(0.1, 0.2), 4000, seed=3) - eps = transcssr( - xs, ys, input_alphabet=("0", "1"), output_alphabet=("0", "1"), Lmax=4, alpha=0.05, correction="bonferroni" + xs, ys = _paired_samples(binary_channel(0.1, 0.2), 4000, seed=3) + eps = learn_epsilon_transducer_cssr( + xs, + ys, + input_alphabet=("0", "1"), + output_alphabet=("0", "1"), + max_history=4, + alpha=0.05, + correction="bonferroni", ) assert len(list(eps.states())) == 1 with pytest.raises(ValueError, match="unknown correction"): - transcssr(xs, ys, Lmax=1, correction="holm") + learn_epsilon_transducer_cssr(xs, ys, max_history=1, correction="holm") @pytest.mark.skipif(not _has_markov_order_selection(), reason="needs dit.inference.select_markov_order") def test_auto_lmax_delay(): - xs, ys = _paired_samples(Delay(1), 6000, seed=4) - eps = transcssr(xs, ys, input_alphabet=("0", "1"), output_alphabet=("0", "1"), Lmax="auto") + xs, ys = _paired_samples(delay(1), 6000, seed=4) + eps = learn_epsilon_transducer_cssr( + xs, ys, input_alphabet=("0", "1"), output_alphabet=("0", "1"), max_history="auto" + ) assert len(list(eps.states())) == 2 diff --git a/tests/test_examples_nrps.py b/tests/test_examples_nrps.py index 8f5f643..a419ac8 100644 --- a/tests/test_examples_nrps.py +++ b/tests/test_examples_nrps.py @@ -5,11 +5,6 @@ import pytest from sofic.examples import even_process, golden_mean, noisy_random_phase_slip -from sofic.examples.processes import NRPS - - -def test_nrps_alias_matches_canonical_constructor(): - assert NRPS().is_equal_process(noisy_random_phase_slip()) def test_nrps_topology(): diff --git a/tests/test_hmm_inference.py b/tests/test_hmm_inference.py index 7260272..bef622d 100644 --- a/tests/test_hmm_inference.py +++ b/tests/test_hmm_inference.py @@ -97,7 +97,7 @@ def test_log_likelihood_long_sequence_stays_finite(): def test_forward_scaled_rows_are_normalized(): coin = fair_coin() - alpha = forward(coin, ["0", "1", "0"], scaled=True) + alpha = forward(coin, ["0", "1", "0"], normalize=True) assert alpha.shape == (4, 1) assert np.allclose(alpha.sum(axis=1), 1.0) @@ -333,8 +333,10 @@ def test_seeded_sample_is_reproducible_across_hash_seeds(): script = ( "import numpy as np\n" - "from sofic.examples.processes import Nemo\n" - "print(''.join(Nemo().sample(200, rng=np.random.default_rng(5))[0]))\n" + "from sofic.examples import nemo_process\n" + "from sofic.examples._construction import _relabel\n" + "nemo = _relabel(nemo_process(), symbols={0: '0', 1: '1'})\n" + "print(''.join(nemo.sample(200, rng=np.random.default_rng(5))[0]))\n" ) outputs = { subprocess.run( diff --git a/tests/test_icdfa.py b/tests/test_icdfa.py index edb1f22..0c60843 100644 --- a/tests/test_icdfa.py +++ b/tests/test_icdfa.py @@ -10,19 +10,19 @@ from sofic.automata.enumeration.icdfa import ( ICDFAEnumerationError, _upper_bound_at, - count_flag_sequences, count_icdfa, count_icdfa_empty, dfa_to_icdfa_string, first_icdfa_empty_string, - flags_from_string, + icdfa_count_flag_sequences, + icdfa_flags_from_string, + icdfa_next_flags, + icdfa_string_from_flags, icdfa_string_to_dfa, iter_icdfa, iter_icdfa_empty_strings, last_icdfa_empty_string, - next_flags, next_icdfa_empty_string, - string_from_flags, validate_icdfa_empty_string, ) @@ -30,8 +30,8 @@ def test_count_flag_sequences() -> None: - assert count_flag_sequences(2, 3) == 5 - assert count_flag_sequences(3, 4) == 55 + assert icdfa_count_flag_sequences(2, 3) == 5 + assert icdfa_count_flag_sequences(3, 4) == 55 def test_count_icdfa_empty() -> None: @@ -60,7 +60,7 @@ def test_first_last_boundaries() -> None: flags = [1, 3] current = list(first_icdfa_empty_string(n=3, k=2)) - transitions = list(string_from_flags(flags, n=3, k=2)) + transitions = list(icdfa_string_from_flags(flags, n=3, k=2)) last_for_flags = list(transitions) for index in range(6): if index in flags: @@ -84,10 +84,10 @@ def test_flag_iteration_matches_count() -> None: while True: count += 1 try: - next_flags(flags, k=k) + icdfa_next_flags(flags, k=k) except StopIteration: break - assert count == count_flag_sequences(k, n) + assert count == icdfa_count_flag_sequences(k, n) def test_round_trip_codec() -> None: @@ -120,8 +120,8 @@ def test_invalid_strings() -> None: def test_flags_from_string() -> None: transitions = first_icdfa_empty_string(n=3, k=2) - assert flags_from_string(transitions, n=3) == (1, 3) - rebuilt = string_from_flags((1, 3), n=3, k=2) + assert icdfa_flags_from_string(transitions, n=3) == (1, 3) + rebuilt = icdfa_string_from_flags((1, 3), n=3, k=2) assert rebuilt == transitions diff --git a/tests/test_inference_diagnostics.py b/tests/test_inference_diagnostics.py index fbc5ab3..5653df0 100644 --- a/tests/test_inference_diagnostics.py +++ b/tests/test_inference_diagnostics.py @@ -7,7 +7,7 @@ from sofic.examples.epsilon_machines import even_process, golden_mean from sofic.generators.sampling import sample -from sofic.inference.cssr import cssr +from sofic.inference.cssr import learn_epsilon_machine_cssr from sofic.inference.diagnostics import ( goodness_of_fit, reconstruction_sweep, @@ -24,8 +24,8 @@ def even_sample(): @pytest.mark.parametrize("statistic", ["g", "entropy_rate"]) def test_goodness_of_fit_accepts_correct_machine(even_sample, statistic): - machine = cssr(even_sample, Lmax=4, alpha=0.001) - result = goodness_of_fit(machine, even_sample, L=5, statistic=statistic, n_samples=49, rng=1) + machine = learn_epsilon_machine_cssr(even_sample, max_history=4, alpha=0.001) + result = goodness_of_fit(machine, even_sample, block_length=5, statistic=statistic, n_samples=49, rng=1) assert result.pvalue > 0.05 assert result.null.shape == (49,) assert result.forbidden_words == () @@ -33,31 +33,31 @@ def test_goodness_of_fit_accepts_correct_machine(even_sample, statistic): @pytest.mark.parametrize("statistic", ["g", "entropy_rate"]) def test_goodness_of_fit_rejects_short_lmax(even_sample, statistic): - """CSSR with Lmax=1 cannot capture the even process's parity.""" - machine = cssr(even_sample, Lmax=1, alpha=0.001) - result = goodness_of_fit(machine, even_sample, L=6, statistic=statistic, n_samples=49, rng=1) + """CSSR with max_history=1 cannot capture the even process's parity.""" + machine = learn_epsilon_machine_cssr(even_sample, max_history=1, alpha=0.001) + result = goodness_of_fit(machine, even_sample, block_length=6, statistic=statistic, n_samples=49, rng=1) assert result.pvalue <= 0.05 def test_goodness_of_fit_forbidden_word(): observations = [0, 1, 1, 0, 1, 0, 0, 1] * 20 - result = goodness_of_fit(golden_mean(0.5), observations, L=2, n_samples=9, rng=0) + result = goodness_of_fit(golden_mean(0.5), observations, block_length=2, n_samples=9, rng=0) assert result.value == float("inf") assert (1, 1) in result.forbidden_words assert result.pvalue == pytest.approx(0.1) with pytest.raises(ValueError): - goodness_of_fit(golden_mean(0.5), observations, L=2, statistic="nope") + goodness_of_fit(golden_mean(0.5), observations, block_length=2, statistic="nope") def test_topology_key_is_isomorphism_invariant(even_sample): - inferred = cssr(even_sample, Lmax=4, alpha=0.001) + inferred = learn_epsilon_machine_cssr(even_sample, max_history=4, alpha=0.001) assert topology_key(inferred) == topology_key(even_process(0.5)) assert topology_key(golden_mean(0.3)) == topology_key(golden_mean(0.7)) assert topology_key(golden_mean(0.5)) != topology_key(even_process(0.5)) def test_structure_stability_subsample(even_sample): - result = structure_stability(even_sample, n_resamples=12, rng=0, Lmax=4, alpha=0.001) + result = structure_stability(even_sample, n_resamples=12, rng=0, max_history=4, alpha=0.001) assert result.reference == topology_key(even_process(0.5)) assert result.reference_fraction >= 0.6 assert result.n_resamples == 12 @@ -73,15 +73,15 @@ def _has_stationary_bootstrap() -> bool: @pytest.mark.skipif(not _has_stationary_bootstrap(), reason="needs dit.inference.stationary_bootstrap") def test_structure_stability_block_runs(even_sample): result = structure_stability( - even_sample, n_resamples=4, rng=0, resample="block", mean_block_length=500, Lmax=3, alpha=0.001 + even_sample, n_resamples=4, rng=0, resample="block", mean_block_length=500, max_history=3, alpha=0.001 ) assert result.n_resamples == 4 with pytest.raises(ValueError): - structure_stability(even_sample, n_resamples=1, resample="nope", Lmax=2) + structure_stability(even_sample, n_resamples=1, resample="nope", max_history=2) def test_reconstruction_sweep_golden_mean(): observations, _ = sample(golden_mean(0.5), 4000, np.random.default_rng(2)) - sweep = reconstruction_sweep(observations, alphas=(0.01, 0.001), lmaxes=(1, 2, 3)) + sweep = reconstruction_sweep(observations, alphas=(0.01, 0.001), max_histories=(1, 2, 3)) target = topology_key(golden_mean(0.5)) assert all(key == target for key in sweep.values()) diff --git a/tests/test_information_anatomy.py b/tests/test_information_anatomy.py index 3d4a350..f817161 100644 --- a/tests/test_information_anatomy.py +++ b/tests/test_information_anatomy.py @@ -7,7 +7,6 @@ import pytest from sofic.examples import ( - NRPS, TENT_MAP_MISIUREWICZ_PARTITIONS, bernoulli, butterfly_process, @@ -16,6 +15,7 @@ golden_mean_forward, golden_mean_reverse, nemo_process, + noisy_random_phase_slip, tent_map_misiurewicz_a, tent_map_misiurewicz_bidirectional, tent_map_misiurewicz_forward, @@ -205,7 +205,7 @@ def _five_variable_processes(): "even": even_process(0.5).to_bidirectional(), "butterfly": butterfly_process().to_bidirectional(), "nemo": nemo_process().to_bidirectional(), - "nrps": NRPS().to_bidirectional(), + "nrps": noisy_random_phase_slip().to_bidirectional(), "tent": tent_map_misiurewicz_bidirectional(), } @@ -340,7 +340,7 @@ def test_golden_mean_ephemeral_is_pure_joint(): def test_nrps_ephemeral_is_pure_reverse_arrow_of_time(): """NRPS: forward-only ephemeral vanishes while reverse-only does not — an arrow of time.""" pytest.importorskip("dit") - bidir = NRPS().to_bidirectional() + bidir = noisy_random_phase_slip().to_bidirectional() r_mu = bidir.ephemeral_information() assert bidir.forward_only_structural_ephemeral() == pytest.approx(0.0, abs=1e-9) assert bidir.reverse_only_structural_ephemeral() == pytest.approx(r_mu, abs=1e-9) diff --git a/tests/test_information_diagram.py b/tests/test_information_diagram.py index 0057346..d60ae77 100644 --- a/tests/test_information_diagram.py +++ b/tests/test_information_diagram.py @@ -5,13 +5,13 @@ import pytest from sofic.examples import ( - NRPS, bernoulli, butterfly_process, even_process, golden_mean_forward, golden_mean_reverse, nemo_process, + noisy_random_phase_slip, ) from sofic.generators.bidirectional_epsilon_machine import BidirectionalEpsilonMachine from sofic.generators.information_diagram import ( @@ -32,7 +32,7 @@ def _processes() -> dict[str, BidirectionalEpsilonMachine]: "even": even_process(0.5).to_bidirectional(), "butterfly": butterfly_process().to_bidirectional(), "nemo": nemo_process().to_bidirectional(), - "nrps": NRPS().to_bidirectional(), + "nrps": noisy_random_phase_slip().to_bidirectional(), } diff --git a/tests/test_measures.py b/tests/test_measures.py index 43600f5..e5e806b 100644 --- a/tests/test_measures.py +++ b/tests/test_measures.py @@ -80,16 +80,16 @@ def test_entropy_rate_moore(): assert hmm.entropy_rate() == pytest.approx(1.0, abs=1e-6) -def test_joint_block_distribution_respects_history_length_mealy(): +def test_joint_block_distribution_respects_block_length_mealy(): eps = _epsilon() - dist = eps.joint_block_distribution(history_length=2) + dist = eps.joint_block_distribution(block_length=3) assert all(len(outcome) == 3 for outcome in dist.outcomes) assert dist[("0", "1", "0")] == pytest.approx(0.125, abs=1e-12) def test_joint_block_distribution_supports_moore_hmms(): hmm = _moore() - dist = hmm.joint_block_distribution(history_length=2) + dist = hmm.joint_block_distribution(block_length=3) assert all(len(outcome) == 3 for outcome in dist.outcomes) assert dist[("0", "1", "0")] == pytest.approx(0.125, abs=1e-12) diff --git a/tests/test_posterior_diversity.py b/tests/test_posterior_diversity.py index 32cee03..be96465 100644 --- a/tests/test_posterior_diversity.py +++ b/tests/test_posterior_diversity.py @@ -5,8 +5,9 @@ import numpy as np import pytest -from sofic.examples import fair_coin -from sofic.examples.processes import Even, EvenRedundant +from sofic.examples import even_process, fair_coin +from sofic.examples._construction import _relabel +from sofic.examples.processes import even_redundant from sofic.inference.bayesian import ( InferEM, ModelComparisonEM, @@ -16,10 +17,14 @@ ) +def _even(): + return _relabel(even_process(), symbols={0: "0", 1: "1"}) + + def test_single_topology_has_zero_diversity(): pytest.importorskip("dit") data = list("1111101100") - comparison = ModelComparisonEM([Even()], data) + comparison = ModelComparisonEM([_even()], data) result = posterior_process_diversity(comparison) assert result.n_components == 1 assert result.process_diversity == pytest.approx(0.0, abs=1e-12) @@ -30,8 +35,8 @@ def test_single_topology_has_zero_diversity(): def test_duplicate_topology_has_zero_process_diversity(): pytest.importorskip("dit") data = list("1111101100") - even1 = Even() - even2 = Even() + even1 = _even() + even2 = _even() even2.name = "Even-copy" comparison = ModelComparisonEM([even1, even2], data) assert len(comparison.em_dict) == 2 @@ -43,19 +48,19 @@ def test_duplicate_topology_has_zero_process_diversity(): def test_same_process_different_topologies_have_low_process_diversity(): pytest.importorskip("dit") data = list("1111101100") - comparison = ModelComparisonEM([Even(), EvenRedundant()], data) + comparison = ModelComparisonEM([_even(), even_redundant()], data) assert len(comparison.em_dict) == 2 result = posterior_process_diversity(comparison) assert result.machine_diversity > 0.5 assert result.process_diversity < result.machine_diversity - different = posterior_process_diversity(ModelComparisonEM([Even(), fair_coin()], data)) + different = posterior_process_diversity(ModelComparisonEM([_even(), fair_coin()], data)) assert result.process_diversity < different.process_diversity def test_different_processes_have_positive_process_diversity(): pytest.importorskip("dit") data = list("1111101100") - comparison = ModelComparisonEM([Even(), fair_coin()], data) + comparison = ModelComparisonEM([_even(), fair_coin()], data) assert len(comparison.em_dict) == 2 result = posterior_process_diversity(comparison) assert result.process_diversity > 1e-6 @@ -64,7 +69,7 @@ def test_different_processes_have_positive_process_diversity(): def test_monte_carlo_is_reproducible_with_rng(): pytest.importorskip("dit") data = list("1111101100") - comparison = ModelComparisonEM([Even(), EvenRedundant()], data) + comparison = ModelComparisonEM([_even(), even_redundant()], data) rng = np.random.default_rng(0) first = posterior_process_diversity(comparison, method="monte_carlo", n_samples=32, rng=rng) rng = np.random.default_rng(0) @@ -77,7 +82,7 @@ def test_monte_carlo_is_reproducible_with_rng(): def test_word_length_override_is_respected(): pytest.importorskip("dit") data = list("1111101100") - comparison = ModelComparisonEM([Even(), EvenRedundant()], data) + comparison = ModelComparisonEM([_even(), even_redundant()], data) assert process_identification_word_length(comparison, word_length=2) == 2 result = posterior_process_diversity(comparison, word_length=2) assert result.word_length == 2 @@ -86,7 +91,7 @@ def test_word_length_override_is_respected(): def test_process_identification_word_length_conventions(): pytest.importorskip("dit") data = list("1111101100") - comparison = ModelComparisonEM([Even(), EvenRedundant()], data) + comparison = ModelComparisonEM([_even(), even_redundant()], data) n_max = max(len(p.dirichlet.nodes) for p in comparison.em_dict.values()) assert process_identification_word_length(comparison, convention="paz") == 2 * n_max - 1 assert process_identification_word_length(comparison, convention="conservative") == 2 * n_max + 1 @@ -97,7 +102,7 @@ def test_process_identification_word_length_conventions(): def test_model_comparison_convenience_methods_match_module(): pytest.importorskip("dit") data = list("1111101100") - comparison = ModelComparisonEM([Even(), EvenRedundant()], data) + comparison = ModelComparisonEM([_even(), even_redundant()], data) module_result = posterior_process_diversity(comparison) method_result = comparison.process_diversity() assert method_result == module_result @@ -107,7 +112,7 @@ def test_model_comparison_convenience_methods_match_module(): def test_posterior_mean_and_monte_carlo_same_order_of_magnitude(): pytest.importorskip("dit") data = list("1111101100") - comparison = ModelComparisonEM([Even(), fair_coin()], data) + comparison = ModelComparisonEM([_even(), fair_coin()], data) mean_result = posterior_process_diversity(comparison, method="posterior_mean") mc_result = posterior_process_diversity( comparison, @@ -126,7 +131,7 @@ def test_infer_em_posterior_mean_word_distribution_matches_machine(): from sofic.inference.bayesian.diversity import posterior_mean_word_distribution data = list("1111101100") - posterior = InferEM(Even(), data) + posterior = InferEM(_even(), data) length = 3 distribution = posterior_mean_word_distribution(posterior, length) start = max(posterior.start_node_probabilities(), key=posterior.start_node_probabilities().get) diff --git a/tests/test_processes_port.py b/tests/test_processes_port.py index 5c3137f..dd49c7a 100644 --- a/tests/test_processes_port.py +++ b/tests/test_processes_port.py @@ -6,44 +6,41 @@ import sofic.examples.processes as processes from sofic.automata.transducers import MealyMachine +from sofic.examples import fair_coin from sofic.generators.base import HiddenMarkovModel from sofic.graph import EPSILON def test_cmpy_process_constructor_names_are_exported(): expected = { - "ABC", - "AFC", - "AFC2", - "BandMerging", - "BeadsOnNecklace", - "BeforeAfter", - "BinaryMarkovChain", - "Butterfly", - "Cantor", - "CoupledGMPs", - "Ehrenfest", - "Even", - "FairCoin", - "GoldenMean", - "GoldenMeanGHMM", - "LogicMachine", - "Nemo", - "Odd", - "Period", - "Periodic", - "PerturbedCoin", - "RandomEven", - "RandomGoldenMean", - "RIP", - "SNS", - "UncoupledGMPs", + "afc", + "afc2", + "band_merging", + "beads_on_necklace", + "before_after", + "binary_markov_chain", + "butterfly_two_branch", + "cantor", + "coupled_gmps", + "ehrenfest", + "golden_mean_forbid_00", + "golden_mean_ghmm", + "logic_machine", + "odd", + "period", + "periodic", + "perturbed_coin", + "random_even", + "random_golden_mean", + "rip", + "sns", + "uncoupled_gmps", "uniform_mealyhmm", "uniform_mealymc", - "GMtoEven", - "BitFlip", - "BinaryChannel", - "Parity", + "gm_to_even", + "bit_flip", + "binary_channel", + "parity", } assert expected <= set(processes.__all__) for name in expected: @@ -53,27 +50,23 @@ def test_cmpy_process_constructor_names_are_exported(): @pytest.mark.parametrize( "constructor", [ - processes.ABC, - processes.BandMerging, - processes.BeadsOnNecklace, - processes.BeforeAfter, - processes.BinaryMarkovChain, - processes.Butterfly, - processes.Cantor, - processes.Ehrenfest, - processes.Even, - processes.FairCoin, - processes.GoldenMean, - processes.Nemo, - processes.Odd, - processes.PerturbedCoin, - processes.RIP, - processes.Rn1C, - processes.Rn1N, - processes.RRX, - processes.RRXRO, - processes.SNS, - processes.ThreeHundred, + processes.band_merging, + processes.beads_on_necklace, + processes.before_after, + processes.binary_markov_chain, + processes.butterfly_two_branch, + processes.cantor, + processes.ehrenfest, + processes.golden_mean_forbid_00, + processes.odd, + processes.perturbed_coin, + processes.rip, + processes.rn1c, + processes.rn1n, + processes.rrx, + processes.rrxro, + processes.sns, + processes.three_hundred, ], ) def test_default_process_constructors_validate(constructor): @@ -83,7 +76,7 @@ def test_default_process_constructors_validate(constructor): def test_golden_mean_cmpy_topology(): - gm = processes.GoldenMean(bias=0.25) + gm = processes.golden_mean_forbid_00(bias=0.25) edges = {(t.source, t.data["emission"], t.target): t.data["prob"] for t in gm.transitions()} assert edges[("A", "1", "A")] == pytest.approx(0.75) assert edges[("A", "0", "B")] == pytest.approx(0.25) @@ -110,13 +103,13 @@ def test_all_transducer_constructors_have_cmpy_style_alphabets_and_rows(): def test_delay_transducer_delays_symbols(): - delay = processes.Delay(length=2, symbols=["0", "1"]) + delay = processes.delay(length=2, symbols=["0", "1"]) assert delay.transduce(("1", "0")) == {("0", "0")} def test_cmpy_style_instance_composition_and_generator_transduction(): - transducer = processes.BitFlip().compose(processes.BitFlip()) + transducer = processes.bit_flip().compose(processes.bit_flip()) assert transducer.transduce(("0", "1")) == {("0", "1")} - output = processes.BinaryChannel(p=0.25, q=0.5).transduce_generator(processes.FairCoin()) + output = processes.binary_channel(p=0.25, q=0.5).transduce_generator(fair_coin()) assert output.word_probability(("1",)) == pytest.approx(0.375) diff --git a/tests/test_sofic_relation.py b/tests/test_product_alphabet_shift.py similarity index 59% rename from tests/test_sofic_relation.py rename to tests/test_product_alphabet_shift.py index 76b44c3..a6f5394 100644 --- a/tests/test_sofic_relation.py +++ b/tests/test_product_alphabet_shift.py @@ -1,18 +1,18 @@ -"""Tests for sofic relations (product-alphabet subshifts).""" +"""Tests for product-alphabet shifts (sofic relations).""" -from sofic import SoficRelation, SoficShift -from sofic.examples.processes import GMtoEven +from sofic import ProductAlphabetShift, SoficShift +from sofic.examples.processes import gm_to_even def test_from_transducer_symbols_are_pairs(): - rel = SoficRelation.from_transducer(GMtoEven()) + rel = ProductAlphabetShift.from_transducer(gm_to_even()) assert all(isinstance(symbol, tuple) and len(symbol) == 2 for symbol in rel.symbol_alphabet) assert rel.input_alphabet() == frozenset({"0", "1"}) assert rel.output_alphabet() == frozenset({"0", "1"}) def test_projections_are_sofic_shifts(): - rel = SoficRelation.from_transducer(GMtoEven()) + rel = ProductAlphabetShift.from_transducer(gm_to_even()) input_shift = rel.input_shift() output_shift = rel.output_shift() assert isinstance(input_shift, SoficShift) @@ -21,8 +21,8 @@ def test_projections_are_sofic_shifts(): def test_input_shift_matches_transducer_input_language(): - transducer = GMtoEven() - rel = SoficRelation.from_transducer(transducer) + transducer = gm_to_even() + rel = ProductAlphabetShift.from_transducer(transducer) input_words = set(rel.input_shift().factor_language(3)) assert input_words # non-empty # every relation input word is a valid transducer input word @@ -30,12 +30,12 @@ def test_input_shift_matches_transducer_input_language(): def test_yaml_round_trip(): - rel = SoficRelation.from_transducer(GMtoEven()) - restored = SoficRelation.from_yaml(rel.to_yaml()) - assert isinstance(restored, SoficRelation) + rel = ProductAlphabetShift.from_transducer(gm_to_even()) + restored = ProductAlphabetShift.from_yaml(rel.to_yaml()) + assert isinstance(restored, ProductAlphabetShift) assert restored.symbol_alphabet == rel.symbol_alphabet def test_transducer_bridge_method(): - rel = GMtoEven().to_sofic_relation() - assert isinstance(rel, SoficRelation) + rel = gm_to_even().to_product_alphabet_shift() + assert isinstance(rel, ProductAlphabetShift) diff --git a/tests/test_sliding_block_code.py b/tests/test_sliding_block_code.py index 25ca3f4..935ae9f 100644 --- a/tests/test_sliding_block_code.py +++ b/tests/test_sliding_block_code.py @@ -61,9 +61,9 @@ def test_to_transducer_realizes_code(): def test_memoryless_transducer_round_trip(): - from sofic.examples.processes import BitFlip + from sofic.examples.processes import bit_flip - code = BitFlip().to_sliding_block_code() + code = bit_flip().to_sliding_block_code() assert code.memory == 0 assert code.apply_word(["0"]) == ("1",) diff --git a/tests/test_stack_inference.py b/tests/test_stack_inference.py index 037cc2d..4293d63 100644 --- a/tests/test_stack_inference.py +++ b/tests/test_stack_inference.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from sofic.automata.learning.papni import DyckAlphabet, is_well_matched, learn_sofic_dyck_shift_papni, papni_encode +from sofic.automata.learning.papni import DyckAlphabet, encode_dyck_word, is_well_matched, learn_sofic_dyck_shift_papni from sofic.automata.learning.rpni import learn_dfa_rpni from sofic.examples.shifts import dyck_shift_order, motzkin_shift from sofic.generators.stack_hmm import HiddenMarkovStackModel @@ -13,12 +13,12 @@ ModelComparisonStackHMM, StackHMMPosterior, ) -from sofic.inference.cssr import cssr +from sofic.inference.cssr import learn_epsilon_machine_cssr from sofic.inference.cssr.stack import ( - fit_stack_hmm_mle, + learn_stack_hmm_cssr, + learn_stack_hmm_mle, learn_stack_hmm_papni, - stack_cssr, - stack_subtree_merge, + learn_stack_hmm_subtree, ) from sofic.shifts.dyck_enumeration import ( count_dyck_graph_strings, @@ -43,11 +43,11 @@ def _uniform_probabilities(shift): return {ref: 1.0 / len(refs) for ref in refs} -def test_is_well_matched_and_papni_encode(): +def test_is_well_matched_and_encode_dyck_word(): alphabet = _balanced_dyck_alphabet() assert is_well_matched(("(", ")"), alphabet) assert not is_well_matched((")", "("), alphabet) - assert papni_encode(("(", ")"), alphabet) == ("(", (")", "(")) + assert encode_dyck_word(("(", ")"), alphabet) == ("(", (")", "(")) def test_rpni_learns_balanced_parentheses_language(): @@ -72,7 +72,7 @@ def test_papni_recovers_dyck_shift_topology(): def test_fit_stack_hmm_mle_assigns_positive_mass(): shift = dyck_shift_order(1, call_symbols=("(",), return_symbols=(")",)) sequence = ("(", ")", "(", "(", ")", ")") - model = fit_stack_hmm_mle(shift, sequence) + model = learn_stack_hmm_mle(shift, sequence) model.validate() assert model.word_probability(sequence) > 0.0 @@ -101,7 +101,7 @@ def test_stack_cssr_recovers_motzkin_structure(): return_alphabet=shift.return_alphabet, internal_alphabet=shift.internal_alphabet, ) - inferred = stack_cssr(observations, alphabet=alphabet, Lmax=3, max_stack_depth=4, alpha=0.001) + inferred = learn_stack_hmm_cssr(observations, alphabet=alphabet, max_history=3, max_stack_depth=4, alpha=0.001) inferred.validate() assert inferred.matched_edges prefix = tuple(observations[:12]) @@ -119,7 +119,7 @@ def test_stack_subtree_merge_runs_on_sample(): return_alphabet=shift.return_alphabet, internal_alphabet=shift.internal_alphabet, ) - inferred = stack_subtree_merge(observations, alphabet=alphabet, L=2, max_stack_depth=3) + inferred = learn_stack_hmm_subtree(observations, alphabet=alphabet, max_history=2, max_stack_depth=3) inferred.validate() @@ -156,7 +156,7 @@ def test_dyck_graph_round_trip(): assert count_dyck_graph_strings(call_symbols=("(",), return_symbols=(")",)) > 0 -@pytest.mark.parametrize("method", ["papni", "stack_cssr", "flat_cssr"]) +@pytest.mark.parametrize("method", ["papni", "learn_stack_hmm_cssr", "flat_cssr"]) def test_benchmark_passive_paths(method: str): shift = dyck_shift_order(1, call_symbols=("(",), return_symbols=(")",)) probs = _uniform_probabilities(shift) @@ -183,10 +183,10 @@ def test_benchmark_passive_paths(method: str): if not positive: positive = [("(", ")")] inferred = learn_stack_hmm_papni(positive, alphabet=alphabet_bm, sequence=observations) - elif method == "stack_cssr": - inferred = stack_cssr(observations, alphabet=alphabet, Lmax=3, max_stack_depth=4, alpha=0.001) + elif method == "learn_stack_hmm_cssr": + inferred = learn_stack_hmm_cssr(observations, alphabet=alphabet, max_history=3, max_stack_depth=4, alpha=0.001) else: - flat = cssr(observations, Lmax=3, alpha=0.001) + flat = learn_epsilon_machine_cssr(observations, max_history=3, alpha=0.001) inferred = HiddenMarkovStackModel( call_alphabet=alphabet.call_alphabet, return_alphabet=alphabet.return_alphabet, @@ -225,9 +225,9 @@ def _held_out_bits_per_symbol(model, word) -> float: return -np.log2(model.word_probability(tuple(word))) / len(word) -@pytest.mark.parametrize("Lmax", [2, 3]) -def test_stack_cssr_matches_motzkin_likelihood(Lmax: int): - """Regression: homogenization only reached stacks of depth <= Lmax and return +@pytest.mark.parametrize("max_history", [2, 3]) +def test_stack_cssr_matches_motzkin_likelihood(max_history: int): + """Regression: homogenization only reached stacks of depth <= max_history and return edges were paired with unobserved calls, so held-out words got probability 0.""" shift = motzkin_shift() oracle = HiddenMarkovStackModel.from_sofic_dyck_shift(shift, _uniform_probabilities(shift)) @@ -238,7 +238,9 @@ def test_stack_cssr_matches_motzkin_likelihood(Lmax: int): ) observations, _ = oracle.sample(5000, rng=np.random.default_rng(0)) held_out, _ = oracle.sample(60, rng=np.random.default_rng(99)) - inferred = stack_cssr(observations, alphabet=alphabet, Lmax=Lmax, max_stack_depth=4, alpha=0.001) + inferred = learn_stack_hmm_cssr( + observations, alphabet=alphabet, max_history=max_history, max_stack_depth=4, alpha=0.001 + ) inferred.validate() assert len(list(inferred.states())) == 1 assert _held_out_bits_per_symbol(inferred, held_out) == pytest.approx( @@ -256,11 +258,13 @@ def test_stack_cssr_exact_and_bonferroni_run(): internal_alphabet=shift.internal_alphabet, ) for kwargs in ({"test": "exact"}, {"correction": "bonferroni"}): - inferred = stack_cssr(observations, alphabet=alphabet, Lmax=3, max_stack_depth=4, alpha=0.001, **kwargs) + inferred = learn_stack_hmm_cssr( + observations, alphabet=alphabet, max_history=3, max_stack_depth=4, alpha=0.001, **kwargs + ) inferred.validate() assert oracle.word_probability(tuple(observations[:12])) > 0.0 with pytest.raises(ValueError, match="unknown correction"): - stack_cssr(observations, alphabet=alphabet, Lmax=2, correction="holm") + learn_stack_hmm_cssr(observations, alphabet=alphabet, max_history=2, correction="holm") @pytest.mark.parametrize("seed", [1, 5]) @@ -280,7 +284,7 @@ def test_stack_cssr_keeps_histories_when_splitting(seed): return_alphabet=shift.return_alphabet, internal_alphabet=shift.internal_alphabet, ) - inferred = stack_cssr(observations, alphabet=alphabet, Lmax=3, max_stack_depth=4, alpha=0.01) + inferred = learn_stack_hmm_cssr(observations, alphabet=alphabet, max_history=3, max_stack_depth=4, alpha=0.01) inferred.validate() held_out, _ = oracle.sample(400, rng=np.random.default_rng(100 + seed)) windows = [tuple(held_out[i : i + 6]) for i in range(0, 390, 6)] diff --git a/tests/test_subsequential.py b/tests/test_subsequential.py index 70c2f96..25bb25b 100644 --- a/tests/test_subsequential.py +++ b/tests/test_subsequential.py @@ -5,7 +5,7 @@ import pytest from sofic.automata.subsequential import SubsequentialTransducer, WeightedFiniteStateTransducer -from sofic.examples.processes import BinaryChannel, BitFlip +from sofic.examples.processes import binary_channel, bit_flip from sofic.exceptions import SoficValidationError from sofic.properties import is_sequential_transducer, is_subsequential_transducer @@ -44,27 +44,27 @@ def test_subsequential_rejects_nondeterministic(): def test_wfst_probability_weight(): - w = WeightedFiniteStateTransducer.from_transducer(BinaryChannel(0.1, 0.2), semiring="probability") + w = WeightedFiniteStateTransducer.from_transducer(binary_channel(0.1, 0.2), semiring="probability") assert w.weight(["0"], ["0"]) == pytest.approx(0.9) assert w.weight(["0"], ["1"]) == pytest.approx(0.1) assert w.weight(["0", "1"], ["0", "1"]) == pytest.approx(0.9 * 0.8) def test_wfst_tropical_weight(): - w = WeightedFiniteStateTransducer.from_transducer(BinaryChannel(0.1, 0.2), semiring="tropical") + w = WeightedFiniteStateTransducer.from_transducer(binary_channel(0.1, 0.2), semiring="tropical") assert w.weight(["0"], ["0"]) == pytest.approx(-math.log(0.9)) assert math.isinf(w.weight(["0"], ["0", "0"])) def test_wfst_bad_semiring(): - w = WeightedFiniteStateTransducer.from_transducer(BitFlip(), semiring="probability") + w = WeightedFiniteStateTransducer.from_transducer(bit_flip(), semiring="probability") w.semiring = "nonsense" with pytest.raises(SoficValidationError): w.validate() def test_wfst_yaml_round_trip(): - w = WeightedFiniteStateTransducer.from_transducer(BinaryChannel(0.1, 0.2), semiring="tropical") + w = WeightedFiniteStateTransducer.from_transducer(binary_channel(0.1, 0.2), semiring="tropical") restored = WeightedFiniteStateTransducer.from_yaml(w.to_yaml()) assert restored.semiring == "tropical" assert restored.weight(["1"], ["1"]) == pytest.approx(w.weight(["1"], ["1"])) diff --git a/tests/test_symbolic_hmm.py b/tests/test_symbolic_hmm.py index a9a766c..0f38758 100644 --- a/tests/test_symbolic_hmm.py +++ b/tests/test_symbolic_hmm.py @@ -29,7 +29,6 @@ is_symbolic, probs_equal, ) -from sofic.generators.words import hmm_word_probability @pytest.mark.parametrize("partition", TENT_MAP_MISIUREWICZ_PARTITIONS) @@ -117,8 +116,8 @@ def test_fig6_hmm_matches_fig7_word_probabilities(): hmm = tent_map_misiurewicz_hmm() fwd = tent_map_misiurewicz_forward() for word in [(0, 0), (0, 1), (1, 0), (1, 1), (0, 1, 0), (1, 1, 0)]: - assert hmm_word_probability(hmm, word) == pytest.approx( - hmm_word_probability(fwd, word), + assert hmm.word_probability(word) == pytest.approx( + fwd.word_probability(word), abs=1e-10, ) diff --git a/tests/test_textile.py b/tests/test_textile.py index 79de4d8..860ddf3 100644 --- a/tests/test_textile.py +++ b/tests/test_textile.py @@ -3,37 +3,37 @@ import pytest from sofic import SoficShift, TextileSystem -from sofic.examples.processes import BinaryChannel, BitFlip, SlidingNOR +from sofic.examples.processes import binary_channel, bit_flip, sliding_nor from sofic.shifts.sliding_block_code import SlidingBlockCode def test_induced_code_recovers_memory(): - textile = TextileSystem.from_transducer(SlidingNOR()) + textile = TextileSystem.from_transducer(sliding_nor()) code = textile.induced_code() assert isinstance(code, SlidingBlockCode) assert code.memory == 1 def test_induced_code_memoryless(): - textile = TextileSystem.from_transducer(BitFlip()) + textile = TextileSystem.from_transducer(bit_flip()) code = textile.induced_code() assert code.memory == 0 assert code.apply_word(["0"]) == ("1",) def test_input_output_shifts(): - textile = TextileSystem.from_transducer(SlidingNOR()) + textile = TextileSystem.from_transducer(sliding_nor()) assert isinstance(textile.input_shift(), SoficShift) assert isinstance(textile.output_shift(), SoficShift) def test_to_transducer_round_trip(): - textile = TextileSystem.from_transducer(SlidingNOR()) + textile = TextileSystem.from_transducer(sliding_nor()) machine = textile.to_transducer() - assert len(list(machine.states())) == len(list(SlidingNOR().states())) + assert len(list(machine.states())) == len(list(sliding_nor().states())) def test_stochastic_channel_has_no_induced_code(): - textile = TextileSystem.from_transducer(BinaryChannel(0.1, 0.2)) + textile = TextileSystem.from_transducer(binary_channel(0.1, 0.2)) with pytest.raises(ValueError): textile.induced_code() diff --git a/tests/test_topological_anatomy.py b/tests/test_topological_anatomy.py index 0a7a2b3..fb4967c 100644 --- a/tests/test_topological_anatomy.py +++ b/tests/test_topological_anatomy.py @@ -153,7 +153,7 @@ def test_nondeterministic_presentation_is_resolved_by_exact_fischer_cover(): shift.add_transition("u", "v", 1) shift.add_transition("v", "u", 0) assert not shift.is_unifilar() - cover = RightFischerCover.from_sofic(shift) + cover = RightFischerCover.from_presentation(shift) assert cover.is_unifilar() anatomy = shift.topological_anatomy() assert anatomy["h_top"] == pytest.approx(cover.topological_entropy(), abs=1e-9) diff --git a/tests/test_transducer_composition.py b/tests/test_transducer_composition.py index 15cc1bf..db9be07 100644 --- a/tests/test_transducer_composition.py +++ b/tests/test_transducer_composition.py @@ -5,11 +5,12 @@ import pytest import sofic.examples.processes as processes -from sofic.automata.transducer_operations import compose_tg, compose_tt, transduce_generator +from sofic.automata.transducer_operations import compose_transducer_generator, compose_transducers, transduce_generator +from sofic.examples import fair_coin def test_bitflip_composed_with_bitflip_is_identity(): - composed = compose_tt((processes.BitFlip(), processes.BitFlip())) + composed = compose_transducers((processes.bit_flip(), processes.bit_flip())) composed.validate() composed.validate_stochastic() @@ -17,36 +18,36 @@ def test_bitflip_composed_with_bitflip_is_identity(): def test_serial_composition_passes_outputs_to_next_transducer(): - composed = compose_tt((processes.GMtoEven(), processes.BitFlip())) + composed = compose_transducers((processes.gm_to_even(), processes.bit_flip())) assert composed.transduce(("0", "1")) == {("0", "0")} assert composed.transduce(("1", "0", "1")) == {("1", "0", "0")} def test_transducer_completion_emits_error_symbol_for_missing_input(): - completed = processes.GMtoEven().complete(frozenset({"0", "1"})) + completed = processes.gm_to_even().complete(frozenset({"0", "1"})) completed.validate() assert completed.transduce(("0", "0")) == {("1", "?")} def test_compose_tg_keeps_joint_input_output_emissions(): - joint = compose_tg(processes.GMtoEven(), processes.GoldenMean(0.5)) + joint = compose_transducer_generator(processes.gm_to_even(), processes.golden_mean_forbid_00(0.5)) assert joint.word_probability((("0", "1"), ("1", "1"))) == pytest.approx(1 / 3) assert joint.word_probability((("0", "1"), ("0", "1"))) == pytest.approx(0.0) def test_golden_mean_through_gm_to_even_generator(): - output = transduce_generator(processes.GMtoEven(), processes.GoldenMean(0.5)) + output = transduce_generator(processes.gm_to_even(), processes.golden_mean_forbid_00(0.5)) assert output.word_probability(("1", "1")) == pytest.approx(1 / 3) assert output.word_probability(("1", "0")) == pytest.approx(0.0) def test_binary_channel_preserves_output_probabilities(): - channel = processes.BinaryChannel(p=0.25, q=0.5) - output = transduce_generator(channel, processes.FairCoin()) + channel = processes.binary_channel(p=0.25, q=0.5) + output = transduce_generator(channel, fair_coin()) assert output.word_probability(("1",)) == pytest.approx(0.375) assert output.word_probability(("0",)) == pytest.approx(0.625) diff --git a/tests/test_vpa.py b/tests/test_vpa.py index fe5bcf5..39c0150 100644 --- a/tests/test_vpa.py +++ b/tests/test_vpa.py @@ -4,9 +4,9 @@ from sofic.automata.dfa import DFA from sofic.automata.vpa import ( - CallDrivenAutomaton, CanonicalVisiblyPushdownAutomaton, DeterministicVisiblyPushdownAutomaton, + ModularVisiblyPushdownAutomaton, MultipleEntryVisiblyPushdownAutomaton, SingleEntryVisiblyPushdownAutomaton, VisiblyPushdownAutomaton, @@ -132,8 +132,8 @@ def test_deterministic_validation_rejects_conflicting_calls(): vpa.validate() -def _cda() -> CallDrivenAutomaton: - vpa = CallDrivenAutomaton( +def _cda() -> ModularVisiblyPushdownAutomaton: + vpa = ModularVisiblyPushdownAutomaton( call_alphabet=frozenset({"c", "d"}), return_alphabet=frozenset({"r"}), stack_alphabet=frozenset({"m"}), diff --git a/tests/test_wheeler.py b/tests/test_wheeler.py index 1fc22de..ecaa4db 100644 --- a/tests/test_wheeler.py +++ b/tests/test_wheeler.py @@ -15,15 +15,15 @@ WheelerError, check_wheeler_axioms, colex_width, + determinize_wheeler, is_input_consistent, is_wheeler, labeled_graph, - minimum_wdfa, + minimize_wheeler, wheeler_canonical_form, wheeler_isomorphic, wheeler_order, wheeler_state_index, - wnfa_to_wdfa, ) from sofic.automata.wheeler_index import WheelerIndex from sofic.examples.epsilon_machines import ( @@ -285,18 +285,18 @@ def test_interval_power_automaton_is_polynomially_bounded(): # -- Minimization and determinization -------------------------------------- -def test_minimum_wdfa_collapses_a_de_bruijn_presentation(): +def test_minimize_wheeler_collapses_a_de_bruijn_presentation(): dfa = sigma_star_dfa(2) assert is_wheeler(dfa) - minimal = minimum_wdfa(dfa) + minimal = minimize_wheeler(dfa) assert len(list(minimal.states())) == 3 assert is_wheeler(minimal) assert equivalent(dfa, minimal, frozenset("ab")) # Already minimal, so minimizing again is a no-op. - assert len(list(minimum_wdfa(minimal).states())) == 3 + assert len(list(minimize_wheeler(minimal).states())) == 3 -def test_minimum_wdfa_rejects_non_wheeler_input(): +def test_minimize_wheeler_rejects_non_wheeler_input(): dfa = DFA(input_alphabet=BINARY, initial_states=frozenset({"A"}), accepting_states=frozenset({"A"})) for state in ("A", "B"): dfa.graph.add_state(state) @@ -304,13 +304,13 @@ def test_minimum_wdfa_rejects_non_wheeler_input(): dfa.add_transition("A", "B", 1) dfa.add_transition("B", "A", 1) with pytest.raises(WheelerError): - minimum_wdfa(dfa) + minimize_wheeler(dfa) -def test_wnfa_to_wdfa_stays_within_the_interval_bound(): +def test_determinize_wheeler_stays_within_the_interval_bound(): nfa = substring_wnfa("abra") assert is_wheeler(nfa) - dfa = wnfa_to_wdfa(nfa) + dfa = determinize_wheeler(nfa) states, arity = len(list(nfa.states())), len(nfa.input_alphabet) assert len(list(dfa.states())) <= 2 * states - 1 - arity assert is_wheeler(dfa) diff --git a/tests/test_yaml.py b/tests/test_yaml.py index 3dceaed..47b68c2 100644 --- a/tests/test_yaml.py +++ b/tests/test_yaml.py @@ -11,9 +11,9 @@ from sofic.automata.nwa import NestedWordAutomaton from sofic.automata.transducers import MealyMachine, MooreMachine from sofic.automata.vpa import ( - CallDrivenAutomaton, CanonicalVisiblyPushdownAutomaton, DeterministicVisiblyPushdownAutomaton, + ModularVisiblyPushdownAutomaton, MultipleEntryVisiblyPushdownAutomaton, SingleEntryVisiblyPushdownAutomaton, VisiblyPushdownAutomaton, @@ -159,7 +159,7 @@ def test_vpa_variants_round_trip(): _round_trip(_base_vpa()) _round_trip(_base_vpa(DeterministicVisiblyPushdownAutomaton)) - cda = CallDrivenAutomaton( + cda = ModularVisiblyPushdownAutomaton( call_alphabet=frozenset({"c"}), return_alphabet=frozenset({"r"}), stack_alphabet=frozenset({"m"}), @@ -357,7 +357,7 @@ def test_cover_and_symbolic_models_round_trip(): shift.graph.add_state(source) shift.graph.add_transition(source, target, **{ATTR_SYMBOL: symbol}) for cls in (LeftFischerCover, RightFischerCover, LeftKriegerCover, RightKriegerCover, WheelerCover): - cover = cls.from_sofic(shift) + cover = cls.from_presentation(shift) restored = _round_trip(cover) assert type(restored) is cls assert sorted(map(repr, restored.states())) == sorted(map(repr, cover.states()))