diff --git a/pensive/automata/transducer_operations.py b/pensive/automata/transducer_operations.py index 1274011..649b6d6 100644 --- a/pensive/automata/transducer_operations.py +++ b/pensive/automata/transducer_operations.py @@ -43,7 +43,10 @@ def cartesian_product_gg( edge_map: dict[tuple[tuple[Hashable, ...], tuple[Hashable, ...], tuple[Any, ...]], float] = defaultdict(float) for source_tuple in product(*states_by_model): - outgoing_groups = [list(generator.graph.out_transitions(state)) for generator, state in zip(mealy_generators, source_tuple, strict=True)] + outgoing_groups = [ + list(generator.graph.out_transitions(state)) + for generator, state in zip(mealy_generators, source_tuple, strict=True) + ] for edge_tuple in product(*outgoing_groups): target = tuple(edge.target for edge in edge_tuple) emission = tuple(edge.data.get(ATTR_EMISSION) for edge in edge_tuple) @@ -73,7 +76,10 @@ def cartesian_product_tt( edges: list[tuple[Hashable, Hashable, Any, Any, float]] = [] for source_tuple in product(*states_by_model): - outgoing_groups = [list(transducer.graph.out_transitions(state)) for transducer, state in zip(transducers, source_tuple, strict=True)] + outgoing_groups = [ + list(transducer.graph.out_transitions(state)) + for transducer, state in zip(transducers, source_tuple, strict=True) + ] for edge_tuple in product(*outgoing_groups): target = tuple(edge.target for edge in edge_tuple) input_symbol = tuple(_input(edge.data) for edge in edge_tuple) diff --git a/pensive/automata/transducers.py b/pensive/automata/transducers.py index cc4a2a1..a4a0440 100644 --- a/pensive/automata/transducers.py +++ b/pensive/automata/transducers.py @@ -144,8 +144,7 @@ def complete( for symbol in symbols: if not any( - transition.data.get(ATTR_SYMBOL) == symbol - for transition in result.graph.out_transitions(reject) + transition.data.get(ATTR_SYMBOL) == symbol for transition in result.graph.out_transitions(reject) ): result.add_transition(reject, reject, symbol, error_output, prob=1.0) @@ -232,9 +231,7 @@ def validate_stochastic(self) -> None: rows[(transition.source, transition.data.get(ATTR_SYMBOL, EPSILON))] += prob for (state, symbol), total in rows.items(): if not np.isclose(total, 1.0): - raise StochasticValidationError( - f"transducer row ({state!r}, {symbol!r}) sums to {total}, not 1" - ) + raise StochasticValidationError(f"transducer row ({state!r}, {symbol!r}) sums to {total}, not 1") def validate(self) -> None: super().validate() diff --git a/pensive/examples/processes.py b/pensive/examples/processes.py index b96a257..6dce6f8 100644 --- a/pensive/examples/processes.py +++ b/pensive/examples/processes.py @@ -458,7 +458,9 @@ def Cantor(machine_type: Any = MealyHMM) -> MealyHMM: return _edge_machine(edges, machine_type=MealyHMM, name="Cantor Process", normalize=False) -def CoupledGMPs(epsilon: float = 0.01, p: float = 0.5, alt: bool = True, machine_type: Any = EpsilonMachine) -> EpsilonMachine: +def CoupledGMPs( + epsilon: float = 0.01, p: float = 0.5, alt: bool = True, machine_type: Any = EpsilonMachine +) -> EpsilonMachine: if epsilon < 0 or p < 0: raise ValueError("epsilon and p cannot be less than zero") if alt: @@ -492,7 +494,13 @@ def CoupledGMPs(epsilon: float = 0.01, p: float = 0.5, alt: bool = True, machine initial = None if epsilon == 0: initial = {"A": p * 2 / 3, "B": p * 1 / 3, "C": (1 - p) * 2 / 3, "D": (1 - p) * 1 / 3} - return _edge_machine(edges, machine_type=_compatible_machine_type(machine_type), name="Coupled Golden Mean Processes", initial_distribution=initial, normalize=False) + return _edge_machine( + edges, + machine_type=_compatible_machine_type(machine_type), + name="Coupled Golden Mean Processes", + initial_distribution=initial, + normalize=False, + ) def UncoupledGMPs(p: float = 0.5, machine_type: Any = EpsilonMachine) -> EpsilonMachine: @@ -526,7 +534,12 @@ def CyclicBranching(num_states: int, num_branchings: int, num_symbols: int = 2) edges.append((x, y, s, 1 / num_symbols)) else: edges.append((x, y, symbol, 1)) - return _edge_machine(edges, machine_type=MealyHMM, name=f"Noisy Period-{num_states} with {num_branchings} branchings", normalize=False) + return _edge_machine( + edges, + machine_type=MealyHMM, + name=f"Noisy Period-{num_states} with {num_branchings} branchings", + normalize=False, + ) def Ehrenfest(p: float = 0.5, N: int = 5, machine_type: Any = EpsilonMachine) -> EpsilonMachine: @@ -549,7 +562,9 @@ def Even(machine_type: Any = EpsilonMachine, bias: float = 0.5) -> EpsilonMachin def RandomEven(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) + return uniform_mealyhmm( + Even(), name="Random Even Process", create_using=_compatible_machine_type(machine_type), prng=rng + ) def EvenRedundant(machine_type: Any = EpsilonMachine, bias: float = 0.5) -> EpsilonMachine: @@ -591,10 +606,7 @@ def Flower( def FourStateAlmostIID(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}; " - f"C A 0 {r}; C D 1 {1 - r}; D A 0 {s}; D D 1 {1 - s};" - ) + 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") @@ -607,7 +619,9 @@ def Girvan_fig6b(machine_type: Any = EpsilonMachine, alpha: float = 0.5, pi: flo ) -def Girvan_fig6c(machine_type: Any = EpsilonMachine, alpha: float = 0.5, pi: float = 0.4, rho: float = 0.3) -> EpsilonMachine: +def Girvan_fig6c( + machine_type: Any = EpsilonMachine, alpha: float = 0.5, pi: float = 0.4, rho: float = 0.3 +) -> EpsilonMachine: return _edge_machine( [ ("A", "A", "1", alpha), @@ -702,7 +716,9 @@ def RkGM(R: int, k: int, p: float = 0.5) -> EpsilonMachine: def RandomGoldenMean(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) + return uniform_mealyhmm( + GoldenMean(), name="Random Golden Mean Process", create_using=_compatible_machine_type(machine_type), prng=rng + ) def GoldenMeanGHMM() -> QuasiStochasticModel: @@ -778,7 +794,9 @@ def Ising(machine_type: Any = EpsilonMachine, J: float = 1.0, B: float = 0.3, T: ) -def Lollipop(N: int, M: int, p: float = 0.5, q: float = 0.5, r: float = 0.1, machine_type: Any = EpsilonMachine) -> EpsilonMachine: +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) hns = [str(ind) for ind in range(N)] sns = [str(ind) for ind in range(N, N + 2 * (M - 1) + 1)] @@ -799,7 +817,9 @@ def Lollipop(N: int, M: int, p: float = 0.5, q: float = 0.5, r: float = 0.1, mac return _edge_machine(edges, machine_type=EpsilonMachine, name="Lollipop", normalize=False) -def LogicMachine(logic: str, bias: float | Sequence[float] = 0.5, noise: float | Sequence[float] = 0.5, minimize: bool = True) -> MealyHMM: +def LogicMachine( + 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) @@ -817,7 +837,9 @@ def markov_skeleton(R: int, k: int | Sequence[Any], join: bool | None = None) -> if any(len(symbol) > 1 for symbol in alphabet): raise ValueError("cannot join symbols with more than one character") if R == 0: - return _edge_machine([("A", "A", symbol, 1.0) for symbol in alphabet], machine_type=MealyHMM, name="Markov skeleton") + return _edge_machine( + [("A", "A", symbol, 1.0) for symbol in alphabet], machine_type=MealyHMM, name="Markov skeleton" + ) states: list[Hashable] = ["".join(word) if join else word for word in _words(alphabet, R)] edges = [] for state in states: @@ -830,7 +852,15 @@ def markov_skeleton(R: int, k: int | Sequence[Any], join: bool | None = None) -> def Misiurewicz(machine_type: Any = MealyHMM) -> MealyHMM: _require_machine_type(machine_type, MealyHMM) return _edge_machine( - [("A", "B", "0", 0.364), ("B", "C", "0", 0.276), ("D", "B", "0", 0.521), ("A", "A", "1", 0.636), ("B", "A", "1", 0.724), ("C", "D", "1", 1), ("D", "C", "1", 0.479)], + [ + ("A", "B", "0", 0.364), + ("B", "C", "0", 0.276), + ("D", "B", "0", 0.521), + ("A", "A", "1", 0.636), + ("B", "A", "1", 0.724), + ("C", "D", "1", 1), + ("D", "C", "1", 0.479), + ], machine_type=MealyHMM, name="Misiurewicz Process (Forward)", normalize=False, @@ -840,7 +870,15 @@ def Misiurewicz(machine_type: Any = MealyHMM) -> MealyHMM: def MisiurewiczSimplified(machine_type: Any = MealyHMM) -> MealyHMM: _require_machine_type(machine_type, MealyHMM) return _edge_machine( - [("A", "B", "0", 0.4), ("B", "C", "0", 0.25), ("D", "B", "0", 0.5), ("A", "A", "1", 0.6), ("B", "A", "1", 0.75), ("C", "D", "1", 1), ("D", "C", "1", 0.5)], + [ + ("A", "B", "0", 0.4), + ("B", "C", "0", 0.25), + ("D", "B", "0", 0.5), + ("A", "A", "1", 0.6), + ("B", "A", "1", 0.75), + ("C", "D", "1", 1), + ("D", "C", "1", 0.5), + ], machine_type=MealyHMM, name="Simplified Misiurewicz Process (Forward)", normalize=False, @@ -850,7 +888,15 @@ def MisiurewiczSimplified(machine_type: Any = MealyHMM) -> MealyHMM: def MisiurewiczUniform(machine_type: Any = MealyHMM) -> MealyHMM: _require_machine_type(machine_type, MealyHMM) return _edge_machine( - [("A", "B", "0", 0.5), ("B", "C", "0", 0.5), ("D", "B", "0", 0.5), ("A", "A", "1", 0.5), ("B", "A", "1", 0.5), ("C", "D", "1", 1), ("D", "C", "1", 0.5)], + [ + ("A", "B", "0", 0.5), + ("B", "C", "0", 0.5), + ("D", "B", "0", 0.5), + ("A", "A", "1", 0.5), + ("B", "A", "1", 0.5), + ("C", "D", "1", 1), + ("D", "C", "1", 0.5), + ], machine_type=MealyHMM, name="Uniform Misiurewicz Process (Forward)", normalize=False, @@ -951,7 +997,9 @@ def EvenOdd(machine_type: Any = EpsilonMachine) -> EpsilonMachine: def ThreEvenOdd(machine_type: Any = EpsilonMachine) -> EpsilonMachine: _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) - 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") + 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: @@ -963,7 +1011,9 @@ def Periodic(word: Sequence[Any], reduce: bool = True) -> MealyHMM: edges = [] for i, symbol in enumerate(base): edges.append((i, (i + 1) % len(base), symbol, 1.0)) - return _edge_machine(edges, machine_type=MealyHMM, name=f"Period-{len(base)} Process ({''.join(map(str, base))})", normalize=False) + return _edge_machine( + edges, machine_type=MealyHMM, name=f"Period-{len(base)} Process ({''.join(map(str, base))})", normalize=False + ) def Period1(machine_type: Any = MealyHMM) -> MealyHMM: @@ -975,7 +1025,11 @@ def Period2(machine_type: Any = MealyHMM) -> MealyHMM: def Period4(machine_type: Any = MealyHMM) -> MealyHMM: - return 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") + return ( + 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: @@ -1036,7 +1090,13 @@ def RIP(p: float = 0.5, q: float = 0.5, reverse: bool = False) -> EpsilonMachine def Rn1C(noise: float = 0.5, bias: float = 0.5) -> EpsilonMachine: return _edge_machine( - [("A", "B", "0", bias), ("B", "A", "0", 1), ("A", "C", "1", 1 - bias), ("C", "A", "0", noise), ("C", "A", "1", 1 - noise)], + [ + ("A", "B", "0", bias), + ("B", "A", "0", 1), + ("A", "C", "1", 1 - bias), + ("C", "A", "0", noise), + ("C", "A", "1", 1 - noise), + ], machine_type=EpsilonMachine, name=f"Random Noisy-1 Copy, p(0|flip is 1) = {noise:.02f}", normalize=False, @@ -1046,7 +1106,13 @@ 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) return _edge_machine( - [("A", "B", "0", bias), ("A", "C", "1", 1 - bias), ("B", "A", "1", 1), ("C", "A", "1", noise), ("C", "A", "0", 1 - noise)], + [ + ("A", "B", "0", bias), + ("A", "C", "1", 1 - bias), + ("B", "A", "1", 1), + ("C", "A", "1", noise), + ("C", "A", "0", 1 - noise), + ], machine_type=EpsilonMachine, name="Rn1N Process", normalize=False, @@ -1109,7 +1175,9 @@ def SNS(machine_type: Any = MealyHMM) -> MealyHMM: ) -def ThreeHundred(machine_type: Any = EpsilonMachine, biases: tuple[float, float, float] = (0.5, 0.5, 0.5)) -> EpsilonMachine: +def ThreeHundred( + machine_type: Any = EpsilonMachine, biases: tuple[float, float, float] = (0.5, 0.5, 0.5) +) -> EpsilonMachine: _require_machine_type(machine_type, EpsilonMachine, RecurrentEpsilonMachine) 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};" @@ -1139,7 +1207,10 @@ def uniform_mealyhmm( if isinstance(topology, Mapping): matrices = {symbol: np.asarray(matrix, dtype=float) for symbol, matrix in topology.items()} else: - matrices = {symbol: np.asarray(matrix, dtype=float) for symbol, matrix in zip(symbols or range(len(topology)), topology, strict=False)} + matrices = { + symbol: np.asarray(matrix, dtype=float) + for symbol, matrix in zip(symbols or range(len(topology)), topology, strict=False) + } first = next(iter(matrices.values())) if nodes is None: nodes = tuple(range(first.shape[0])) @@ -1156,7 +1227,13 @@ def uniform_mealyhmm( return _edge_machine(edges, machine_type=cls, name=name or "Random Mealy HMM", normalize=False) -def uniform_mealymc(order: int, symbols: int | Sequence[Any], name: str | None = None, create_using: type[MealyHMM] | None = None, prng: Any = None) -> MealyHMM: +def uniform_mealymc( + order: int, + symbols: int | Sequence[Any], + name: str | None = None, + create_using: type[MealyHMM] | None = None, + prng: Any = None, +) -> MealyHMM: alphabet = _as_alphabet(symbols) cls = create_using or MealyHMM edges = [] @@ -1165,7 +1242,9 @@ def uniform_mealymc(order: int, symbols: int | Sequence[Any], name: str | None = for prob, symbol in zip(probs, alphabet, strict=True): target = (*state[1:], symbol) if order else () edges.append((state, target, symbol, float(prob))) - return _edge_machine(edges, machine_type=cls, name=(name or "Random Markov Chain") + f" (k={order})", normalize=False) + return _edge_machine( + edges, machine_type=cls, name=(name or "Random Markov Chain") + f" (k={order})", normalize=False + ) def _transducer( @@ -1179,13 +1258,19 @@ def _transducer( inputs = frozenset(input_symbol for _source, _target, input_symbol, _output_symbol, _prob in edge_list) outputs = frozenset(output_symbol for _source, _target, _input_symbol, output_symbol, _prob in edge_list) start = initial if initial is not None else (states[0] if states else None) - machine = MealyMachine(input_alphabet=inputs, output_alphabet=outputs, initial_states=frozenset({start} if start is not None else set())) + machine = MealyMachine( + input_alphabet=inputs, + output_alphabet=outputs, + initial_states=frozenset({start} if start is not None else set()), + ) if name is not None: machine.name = name for state in states: machine.graph.add_state(state) for source, target, input_symbol, output_symbol, prob in edge_list: - machine.graph.add_transition(source, target, **{ATTR_SYMBOL: input_symbol, ATTR_OUTPUT: output_symbol, ATTR_PROB: float(prob)}) + machine.graph.add_transition( + source, target, **{ATTR_SYMBOL: input_symbol, ATTR_OUTPUT: output_symbol, ATTR_PROB: float(prob)} + ) machine.validate() return machine @@ -1197,7 +1282,9 @@ def GMtoEven(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)]) + 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: @@ -1208,7 +1295,10 @@ def BitFlip(create_using: Any = None) -> MealyMachine: def FlipEveryOther(parity: str = "Even", create_using: Any = None) -> MealyMachine: del create_using initial = "A" if parity == "Even" else "B" - return _transducer([("A", "B", "0", "1", 1), ("A", "B", "1", "0", 1), ("B", "A", "0", "0", 1), ("B", "A", "1", "1", 1)], initial=initial) + return _transducer( + [("A", "B", "0", "1", 1), ("A", "B", "1", "0", 1), ("B", "A", "0", "0", 1), ("B", "A", "1", "1", 1)], + initial=initial, + ) def Delay(length: int = 2, symbols: int | Sequence[Any] = 2, create_using: Any = None) -> MealyMachine: @@ -1238,17 +1328,23 @@ def TwoPerm(symbols: int | Sequence[Any] = 2, create_using: Any = None) -> Mealy def BinaryChannel(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)]) + 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: 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)]) + 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: 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)]) + 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: @@ -1323,7 +1419,18 @@ def GME(create_using: Any = None) -> MealyMachine: "ThreEvenOdd", ] nonergodic_generators = ["UncoupledGMPs"] -transducers = ["GMtoEven", "RCT", "BitFlip", "FlipEveryOther", "Delay", "TwoPerm", "BinaryChannel", "SlidingNOR", "Parity", "GME"] +transducers = [ + "GMtoEven", + "RCT", + "BitFlip", + "FlipEveryOther", + "Delay", + "TwoPerm", + "BinaryChannel", + "SlidingNOR", + "Parity", + "GME", +] process_list = [globals()[name] for name in processes] process_list.extend(globals()[name] for name in nonergodic_generators) diff --git a/pensive/generators/base.py b/pensive/generators/base.py index a2fd673..1ef79b1 100644 --- a/pensive/generators/base.py +++ b/pensive/generators/base.py @@ -75,9 +75,7 @@ def reverse(self) -> Self: if isinstance(self, EpsilonMachine): return EpsilonMachine.from_time_reversed(self) return EpsilonMachine.from_hmm(time_reverse_stochastic(self)) - raise NotImplementedError( - "time-reversed generators with edge emissions require EpsilonMachine.from_hmm" - ) + raise NotImplementedError("time-reversed generators with edge emissions require EpsilonMachine.from_hmm") return time_reverse_stochastic(self) diff --git a/pensive/generators/block_convergence.py b/pensive/generators/block_convergence.py index a4ea988..72e09fe 100644 --- a/pensive/generators/block_convergence.py +++ b/pensive/generators/block_convergence.py @@ -356,7 +356,13 @@ def plot( if name == "cm": self._plot_cm(panel, marker=marker, show_grid=show_grid, show_legend=show_legend) else: - self.plot(ax=panel, figure=name, show_asymptotes=show_asymptotes, show_grid=show_grid, show_legend=show_legend) + self.plot( + ax=panel, + figure=name, + show_asymptotes=show_asymptotes, + show_grid=show_grid, + show_legend=show_legend, + ) if title is not None: axes_list[0].figure.suptitle(title) return axes_list @@ -389,7 +395,12 @@ def plot( ax.plot(self.lengths, self.residual_entropy_asymptote, linestyle="--", label=r"$E_R + r_\mu \ell$") ax.plot(self.lengths, self.binding_information_asymptote, linestyle="--", label=r"$E_B + b_\mu \ell$") ax.plot(self.lengths, self.enigmatic_information_asymptote, linestyle="--", label=r"$E_Q + q_\mu \ell$") - ax.plot(self.lengths, self.local_exogenous_information_asymptote, linestyle="--", label=r"$E_W + w_\mu \ell$") + ax.plot( + self.lengths, + self.local_exogenous_information_asymptote, + linestyle="--", + label=r"$E_W + w_\mu \ell$", + ) elif figure == "fig5_rb": ax.plot(self.lengths, self.block_residual_entropy, marker=marker, label=r"$R(\ell)$") ax.plot(self.lengths, self.block_binding_information, marker=marker, label=r"$B(\ell)$") @@ -401,7 +412,12 @@ def plot( ax.plot(self.lengths, self.block_local_exogenous_information, marker=marker, label=r"$W(\ell)$") if show_asymptotes: ax.plot(self.lengths, self.enigmatic_information_asymptote, linestyle="--", label=r"$E_Q + q_\mu \ell$") - ax.plot(self.lengths, self.local_exogenous_information_asymptote, linestyle="--", label=r"$E_W + w_\mu \ell$") + ax.plot( + self.lengths, + self.local_exogenous_information_asymptote, + linestyle="--", + label=r"$E_W + w_\mu \ell$", + ) elif figure == "fig6": ax.plot(self.lengths, self.block_coinformation, marker=marker, label=r"$I(\ell)$") if show_asymptotes: @@ -487,10 +503,7 @@ def block_convergence_diagram( max_caekl_length=max_caekl_length, ) return BlockConvergenceDiagram( - **{ - field: getattr(estimates, field) - for field in BlockConvergenceDiagram.__dataclass_fields__ - } + **{field: getattr(estimates, field) for field in BlockConvergenceDiagram.__dataclass_fields__} ) @@ -522,7 +535,11 @@ def block_convergence_estimates( exact = _exact_anatomy_scalars(machine) if use_exact else None h_mu_exact = exact["entropy_rate"] if exact else None - h_mu = float(entropy_rate if entropy_rate is not None else (_entropy_rate(pi, symbol_matrices) if h_mu_exact is None else h_mu_exact)) + h_mu = float( + entropy_rate + if entropy_rate is not None + else (_entropy_rate(pi, symbol_matrices) if h_mu_exact is None else h_mu_exact) + ) statistical_complexity = _entropy(pi) excess_entropy = _estimated_excess_entropy( diff --git a/pensive/generators/directional_flow.py b/pensive/generators/directional_flow.py index ca0f214..a48b3c9 100644 --- a/pensive/generators/directional_flow.py +++ b/pensive/generators/directional_flow.py @@ -17,7 +17,6 @@ def _require_dit(): return require_dit("directional flow") - def _pair_block_distribution(generator: HiddenMarkovModel, *, history: int) -> Any: """Joint law over flattened ``(x0, y0, x1, y1, ...)`` windows.""" from pensive.generators.hmm_inference import _stationary_emission_tensors diff --git a/pensive/generators/measures.py b/pensive/generators/measures.py index 6d3b3b5..feae4b3 100644 --- a/pensive/generators/measures.py +++ b/pensive/generators/measures.py @@ -20,9 +20,7 @@ def require_dit(feature: str = "entropy measures") -> Any: try: import dit except ImportError as exc: - raise ImportError( - f"dit is required for {feature}; install with `pip install dit`" - ) from exc + raise ImportError(f"dit is required for {feature}; install with `pip install dit`") from exc return dit diff --git a/pensive/generators/stack_hmm.py b/pensive/generators/stack_hmm.py index 4089f6d..e41a012 100644 --- a/pensive/generators/stack_hmm.py +++ b/pensive/generators/stack_hmm.py @@ -341,8 +341,12 @@ def from_sofic_dyck_shift( key = graph.add_transition(transition.source, transition.target, **data) edge_map[ref] = (transition.source, transition.target, key) - matched_edges = frozenset((edge_map[call_ref], edge_map[return_ref]) for call_ref, return_ref in shift.matched_edges) - initial = dict(initial_distribution) if initial_distribution is not None else _uniform_initial_distribution(shift) + matched_edges = frozenset( + (edge_map[call_ref], edge_map[return_ref]) for call_ref, return_ref in shift.matched_edges + ) + initial = ( + dict(initial_distribution) if initial_distribution is not None else _uniform_initial_distribution(shift) + ) return cls( graph=graph, initial_distribution=initial, diff --git a/pensive/generators/stack_inference.py b/pensive/generators/stack_inference.py index e37206b..7c01fa0 100644 --- a/pensive/generators/stack_inference.py +++ b/pensive/generators/stack_inference.py @@ -254,9 +254,7 @@ def _counts_to_stack_hmm( if prob <= 0.0: continue emitting = [ - history - for history in histories - if counts.next_counts.get(history, Counter()).get(symbol, 0) > 0 + history for history in histories if counts.next_counts.get(history, Counter()).get(symbol, 0) > 0 ] if not emitting: continue @@ -374,7 +372,9 @@ def stack_cssr( max_stack_depth=max_stack_depth, ) states = _stack_merge(states, history_to_state, counts, alpha=alpha, test=test) - states = _stack_drop_transient(states, history_to_state, counts, length=max_length, alphabet=alphabet, max_stack_depth=max_stack_depth) + states = _stack_drop_transient( + states, history_to_state, counts, length=max_length, alphabet=alphabet, max_stack_depth=max_stack_depth + ) history_to_state = {history: state_id for state_id, histories in states.items() for history in histories} return _counts_to_stack_hmm( states, @@ -419,11 +419,19 @@ def stack_subtree_merge( 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} + history_to_state = { + history: state_id for state_id, histories_in_state in states.items() for history in histories_in_state + } states = _stack_merge(states, history_to_state, counts, alpha=0.05, test="tv") - history_to_state = {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) - history_to_state = {history: state_id for state_id, histories_in_state in states.items() for history in histories_in_state} + history_to_state = { + 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 + ) + history_to_state = { + history: state_id for state_id, histories_in_state in states.items() for history in histories_in_state + } return _counts_to_stack_hmm( states, counts, diff --git a/pensive/generators/synchronization.py b/pensive/generators/synchronization.py index af8e80f..d5d1947 100644 --- a/pensive/generators/synchronization.py +++ b/pensive/generators/synchronization.py @@ -97,9 +97,7 @@ def _bellman_ford_longest_transient_path(pa: PowerAutomaton) -> float: edges.append((source, target, weight)) reverse_edges.setdefault(target, set()).add(source) - sync_reachable: set[frozenset[Hashable]] = { - node for node in nodes if pa.graph.is_recurrent_pa_state(node) - } + sync_reachable: set[frozenset[Hashable]] = {node for node in nodes if pa.graph.is_recurrent_pa_state(node)} queue = list(sync_reachable) while queue: current = queue.pop(0) @@ -109,7 +107,11 @@ def _bellman_ford_longest_transient_path(pa: PowerAutomaton) -> float: sync_reachable.add(predecessor) queue.append(predecessor) - edges = [(source, target, weight) for source, target, weight in edges if source in sync_reachable and target in sync_reachable] + edges = [ + (source, target, weight) + for source, target, weight in edges + if source in sync_reachable and target in sync_reachable + ] node_list = list(nodes) n = len(node_list) diff --git a/pensive/generators/topological_epsilon_enumeration.py b/pensive/generators/topological_epsilon_enumeration.py index 054980f..846a017 100644 --- a/pensive/generators/topological_epsilon_enumeration.py +++ b/pensive/generators/topological_epsilon_enumeration.py @@ -210,7 +210,9 @@ def _encode_topological_graph_from_root( state_index += 1 if len(index_to_state) != len(states): - raise TopologicalEpsilonEnumerationError("epsilon machine graph must be initially connected from the selected root") + raise TopologicalEpsilonEnumerationError( + "epsilon machine graph must be initially connected from the selected root" + ) validate_idfa_string(transitions, n=len(states), k=len(symbols)) return tuple(transitions) @@ -253,8 +255,7 @@ def is_minimal_idfa(transitions: Sequence[int], *, n: int, k: int) -> bool: groups: dict[tuple[object, ...], set[int]] = {} for state in block: signature = tuple( - None if table[state][symbol] is None else block_index[table[state][symbol]] - for symbol in range(k) + None if table[state][symbol] is None else block_index[table[state][symbol]] for symbol in range(k) ) groups.setdefault(signature, set()).add(state) if len(groups) > 1: diff --git a/pensive/inference/bayesian/comparison.py b/pensive/inference/bayesian/comparison.py index a372416..16c48f2 100644 --- a/pensive/inference/bayesian/comparison.py +++ b/pensive/inference/bayesian/comparison.py @@ -82,13 +82,21 @@ def __init__( self.orders = order_list self.markov_chain_prior = mc_prior self.model_order_prior = mo_prior - self.mc_dict = {order: MarkovChainPosterior(self.alphabet, data, order, prior_type=mc_prior) for order in order_list} + self.mc_dict = { + order: MarkovChainPosterior(self.alphabet, data, order, prior_type=mc_prior) for order in order_list + } class ModelComparisonEM: """Compare candidate unifilar topologies using epsilon-machine evidences.""" - def __init__(self, machines: Iterable[MealyHMM], data: Sequence[Any] | None = None, beta: float = 0.0, state_path: bool = False): + def __init__( + self, + machines: Iterable[MealyHMM], + data: Sequence[Any] | None = None, + beta: float = 0.0, + state_path: bool = False, + ): from pensive.inference.bayesian.epsilon import EpsilonMachinePosterior self.beta = float(beta) diff --git a/pensive/inference/bayesian/counts.py b/pensive/inference/bayesian/counts.py index a02cdce..9e43afd 100644 --- a/pensive/inference/bayesian/counts.py +++ b/pensive/inference/bayesian/counts.py @@ -56,7 +56,6 @@ def posterior_weights( return weights, log_norm - def pretty_symbol(symbol: Any) -> str: """Format a symbol like cmpy's Bayesian inference utilities.""" if isinstance(symbol, str): @@ -93,8 +92,7 @@ def __str__(self) -> str: if not self.counts: return "No counts." formatted = { - f"{pretty_word(context)} -> {pretty_symbol(symbol)}": (context, symbol) - for context, symbol in self.counts + f"{pretty_word(context)} -> {pretty_symbol(symbol)}": (context, symbol) for context, symbol in self.counts } integer_counts = all(float(value).is_integer() for value in self.counts.values()) lines = [] diff --git a/pensive/inference/bayesian/diversity.py b/pensive/inference/bayesian/diversity.py index e05ca9b..79ab84f 100644 --- a/pensive/inference/bayesian/diversity.py +++ b/pensive/inference/bayesian/diversity.py @@ -62,9 +62,7 @@ def _require_dit(): try: from dit.divergences.jensen_shannon_divergence import jensen_shannon_divergence_pmf except ImportError as exc: - raise ImportError( - "dit is required for posterior process diversity; install with `pip install dit`" - ) from exc + raise ImportError("dit is required for posterior process diversity; install with `pip install dit`") from exc return jensen_shannon_divergence_pmf diff --git a/pensive/inference/bayesian/epsilon.py b/pensive/inference/bayesian/epsilon.py index c6b4cee..1004d47 100644 --- a/pensive/inference/bayesian/epsilon.py +++ b/pensive/inference/bayesian/epsilon.py @@ -138,7 +138,9 @@ def set_edge_alpha(self, edge: tuple[Hashable, Any], value: float) -> None: for valid_edge in self.valid_edges: self.alphas[valid_edge[0]] += self.alphas[valid_edge] - def _machine_from_probabilities(self, start_node: Hashable, probabilities: Mapping[tuple[Hashable, Any], float], name: str) -> MealyHMM: + def _machine_from_probabilities( + self, start_node: Hashable, probabilities: Mapping[tuple[Hashable, Any], float], name: str + ) -> MealyHMM: machine = MealyHMM(observation_alphabet=getattr(self.machine, "observation_alphabet", frozenset())) machine.name = name initial_state = self.get_last_node(start_node) if self.data is not None else start_node @@ -170,7 +172,9 @@ def posterior_mean_machine(self, start_node: Hashable) -> MealyHMM | None: if prob is None: raise BayesianInferenceError(f"missing probability for edge {edge!r}") probabilities[edge] = prob - return self._machine_from_probabilities(start_node, probabilities, f"Posterior Mean Machine, Start Node: {start_node}") + return self._machine_from_probabilities( + start_node, probabilities, f"Posterior Mean Machine, Start Node: {start_node}" + ) def generate_sample(self, start_node: Hashable, rng: np.random.Generator | None = None) -> MealyHMM | None: if start_node not in self.valid_startnodes: diff --git a/pensive/inference/bayesian/markov.py b/pensive/inference/bayesian/markov.py index 5002801..509a78e 100644 --- a/pensive/inference/bayesian/markov.py +++ b/pensive/inference/bayesian/markov.py @@ -39,7 +39,9 @@ def __str__(self) -> str: for context in words_iter(self.alphabet, self.order): lines.append(f"alpha({pretty_word(context)} -> *) = {self.get_alpha((*context, '*'))}") for symbol in self.alphabet: - lines.append(f"alpha({pretty_word(context)} -> {pretty_symbol(symbol)}) = {self.get_alpha((*context, symbol))}") + lines.append( + f"alpha({pretty_word(context)} -> {pretty_symbol(symbol)}) = {self.get_alpha((*context, symbol))}" + ) return "\n".join(lines) + "\n" def create_random_prior(self, lower: int, upper: int, rng: np.random.Generator | None = None) -> None: @@ -136,9 +138,7 @@ def log_evidence(self) -> float: if alpha is None: raise BayesianInferenceError("missing prior alpha") cells.append((alpha, self.counts.get_word_count(word))) - evidence += dirichlet_multinomial_log_evidence( - alpha_root, self.counts.get_word_count(root), cells - ) + evidence += dirichlet_multinomial_log_evidence(alpha_root, self.counts.get_word_count(root), cells) return float(evidence) def average_relative_entropy_plus_entropy_rate(self) -> float: @@ -209,7 +209,10 @@ def generate_mealy_hmm(self, method: str = "PME", threshold: float = 0.0, reduce matrix = self.posterior_mean_matrix() elif method == "MLE": matrix = np.array( - [[self.transition_probability_mle(context, symbol)[0] for symbol in self.alphabet] for context in self.contexts], + [ + [self.transition_probability_mle(context, symbol)[0] for symbol in self.alphabet] + for context in self.contexts + ], dtype=float, ) else: @@ -227,7 +230,9 @@ def generate_mealy_hmm(self, method: str = "PME", threshold: float = 0.0, reduce for j, symbol in enumerate(self.alphabet): prob = float(matrix[i, j]) if prob > threshold: - hmm.graph.add_transition(source, self._target_for(context, symbol), **{ATTR_EMISSION: symbol, ATTR_PROB: prob}) + hmm.graph.add_transition( + source, self._target_for(context, symbol), **{ATTR_EMISSION: symbol, ATTR_PROB: prob} + ) hmm.validate() return hmm @@ -256,7 +261,9 @@ def sample_mealy_hmms( for j, symbol in enumerate(self.alphabet): prob = float(sample[i, j]) if prob > threshold: - hmm.graph.add_transition(source, self._target_for(context, symbol), **{ATTR_EMISSION: symbol, ATTR_PROB: prob}) + hmm.graph.add_transition( + source, self._target_for(context, symbol), **{ATTR_EMISSION: symbol, ATTR_PROB: prob} + ) hmm.validate() yield hmm diff --git a/pensive/inference/bayesian/stack_hmm.py b/pensive/inference/bayesian/stack_hmm.py index 799610a..faa7c16 100644 --- a/pensive/inference/bayesian/stack_hmm.py +++ b/pensive/inference/bayesian/stack_hmm.py @@ -140,11 +140,7 @@ def posterior_mean_probabilities(self) -> dict[TransitionRef, float]: mean = (count + alpha) / (row_count + row_alpha) edge_totals[edge] += mean edge_counts[edge] += 1.0 - return { - edge: edge_totals[edge] / edge_counts[edge] - for edge in edge_totals - if edge_counts[edge] > 0 - } + return {edge: edge_totals[edge] / edge_counts[edge] for edge in edge_totals if edge_counts[edge] > 0} def posterior_mean_model(self) -> HiddenMarkovStackModel: from pensive.shifts.sofic_dyck import SoficDyckShift diff --git a/pensive/serialization.py b/pensive/serialization.py index c09c8ae..9ec7b53 100644 --- a/pensive/serialization.py +++ b/pensive/serialization.py @@ -169,10 +169,7 @@ def _stack_alphabets(graph: TransitionGraph) -> dict[str, frozenset[Any]]: def _graph_to_data(graph: TransitionGraph) -> dict[str, Any]: - nodes = [ - {"id": _encode(state), "attrs": _encode(dict(attrs))} - for state, attrs in graph.nx.nodes(data=True) - ] + nodes = [{"id": _encode(state), "attrs": _encode(dict(attrs))} for state, attrs in graph.nx.nodes(data=True)] edges = [ { "source": _encode(source), @@ -241,10 +238,7 @@ def _encode(value: Any) -> Any: if isinstance(value, Mapping): return { _TYPE_KEY: "dict", - "items": [ - {"key": _encode(key), "value": _encode(item_value)} - for key, item_value in value.items() - ], + "items": [{"key": _encode(key), "value": _encode(item_value)} for key, item_value in value.items()], } raise TypeError(f"cannot YAML-serialize value of type {type(value).__qualname__}: {value!r}") diff --git a/pensive/shifts/dyck_enumeration.py b/pensive/shifts/dyck_enumeration.py index 93578fe..d2d4332 100644 --- a/pensive/shifts/dyck_enumeration.py +++ b/pensive/shifts/dyck_enumeration.py @@ -132,9 +132,9 @@ def dyck_graph_string_to_shift(spec: DyckGraphString) -> SoficDyckShift: def _one_state_transition_choices( - symbols_per_kind: Sequence[int], - *, - include_empty: bool, + symbols_per_kind: Sequence[int], + *, + include_empty: bool, ) -> Iterator[tuple[tuple[int, int, int, int], ...]]: """Enumerate transition subsets on a single state looping to itself.""" slots: list[tuple[int, int, int, int]] = [] diff --git a/pensive/shifts/sofic_dyck.py b/pensive/shifts/sofic_dyck.py index 0c220f9..13e730f 100644 --- a/pensive/shifts/sofic_dyck.py +++ b/pensive/shifts/sofic_dyck.py @@ -40,7 +40,9 @@ def __init__( self.return_alphabet = return_alphabet if return_alphabet is not None else frozenset() self.internal_alphabet = internal_alphabet if internal_alphabet is not None else frozenset() inferred_alphabet = self.call_alphabet | self.return_alphabet | self.internal_alphabet - super().__init__(symbol_alphabet=symbol_alphabet if symbol_alphabet is not None else inferred_alphabet, **kwargs) + super().__init__( + symbol_alphabet=symbol_alphabet if symbol_alphabet is not None else inferred_alphabet, **kwargs + ) self.matched_edges = frozenset(matched_edges or frozenset()) def validate(self) -> None: diff --git a/pyproject.toml b/pyproject.toml index 3ad603e..04a8e52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Physics", ] dependencies = [ - "dit>=2.1", + "dit>=2.2", "networkx>=2.6", "numpy>=1.22", "pyyaml>=6.0", @@ -184,11 +184,19 @@ known-first-party = ["pensive"] [tool.ty.environment] python-version = "3.13" +[tool.ty.analysis] +# Optional/conditional dependencies +allowed-unresolved-imports = [ + "pymc", + "pymc.**", +] + [tool.ty.rules] unresolved-import = "error" unresolved-attribute = "warn" call-non-callable = "warn" invalid-argument-type = "warn" +invalid-return-type = "warn" not-subscriptable = "warn" invalid-method-override = "warn" missing-argument = "warn" diff --git a/tests/test_block_convergence.py b/tests/test_block_convergence.py index 109bcb0..7c4dbb3 100644 --- a/tests/test_block_convergence.py +++ b/tests/test_block_convergence.py @@ -2,8 +2,6 @@ from __future__ import annotations -import math - import pytest from pensive.examples import even_process, fair_coin, golden_mean, noisy_random_phase_slip @@ -59,7 +57,7 @@ def test_golden_mean_table_i_scalars(): assert est.r_mu == pytest.approx(0.45915, abs=1e-4) assert est.b_mu == pytest.approx(0.20752, abs=1e-4) assert est.q_mu == pytest.approx(0.04411, abs=1e-4) - assert est.E == pytest.approx(0.25163, abs=1e-4) + assert pytest.approx(0.25163, abs=1e-4) == est.E def test_even_process_table_i_scalars(): @@ -79,7 +77,7 @@ def test_nrps_table_i_scalars(): assert est.block_entropy[1] == pytest.approx(0.97987, abs=1e-4) assert est.h_mu == pytest.approx(0.5, abs=1e-4) assert est.rho_mu == pytest.approx(0.47987, abs=1e-4) - assert est.E == pytest.approx(1.57393, abs=1e-4) + assert pytest.approx(1.57393, abs=1e-4) == est.E assert est.r_mu + est.b_mu == pytest.approx(est.h_mu, abs=1e-4) assert est.b_mu == pytest.approx(0.33333, abs=1e-4) assert est.r_mu == pytest.approx(0.16667, abs=1e-4) diff --git a/tests/test_edge_machine.py b/tests/test_edge_machine.py index 27da5e6..0b87fea 100644 --- a/tests/test_edge_machine.py +++ b/tests/test_edge_machine.py @@ -25,9 +25,7 @@ def _count_labeled_transitions(hmm) -> int: def _transitions_between(hmm, source, target): return [ - transition - for transition in hmm.transitions() - if transition.source == source and transition.target == target + transition for transition in hmm.transitions() if transition.source == source and transition.target == target ] diff --git a/tests/test_stack_inference.py b/tests/test_stack_inference.py index f006ed7..f8bf66b 100644 --- a/tests/test_stack_inference.py +++ b/tests/test_stack_inference.py @@ -36,7 +36,6 @@ def _balanced_dyck_alphabet() -> DyckAlphabet: ) - def _uniform_probabilities(shift): from pensive.shifts.sofic_dyck import transition_ref @@ -142,9 +141,7 @@ def test_model_comparison_stack_hmm_prefers_true_topology(): candidates = [true_model] for topology in iter_sofic_dyck_topologies(call_symbols=("(",), return_symbols=(")",)): - candidates.append( - HiddenMarkovStackModel.from_sofic_dyck_shift(topology, _uniform_probabilities(topology)) - ) + candidates.append(HiddenMarkovStackModel.from_sofic_dyck_shift(topology, _uniform_probabilities(topology))) comparison = ModelComparisonStackHMM(candidates, observations, max_stack_depth=4) assert np.isfinite(comparison.log_evidences()[0]) best = comparison.most_probable_model() diff --git a/tests/test_viz.py b/tests/test_viz.py index d5b0a82..fdc6564 100644 --- a/tests/test_viz.py +++ b/tests/test_viz.py @@ -142,9 +142,7 @@ def test_graphviz_nested_frozenset_state_labels_are_readable(): def test_tikz_nested_frozenset_state_labels_are_readable(): - assert format_state_tikz_node(frozenset({frozenset({0}), frozenset({1, 2})})) == ( - r"\{\{0\}{,} \{1{,} 2\}\}" - ) + assert format_state_tikz_node(frozenset({frozenset({0}), frozenset({1, 2})})) == (r"\{\{0\}{,} \{1{,} 2\}\}") source = model_to_tikz(_nested_frozenset_dfa()) assert "frozenset(" not in source assert r"\{\{0\}{,} \{1{,} 2\}\}" in source diff --git a/uv.lock b/uv.lock index 3023354..3748415 100644 --- a/uv.lock +++ b/uv.lock @@ -505,7 +505,7 @@ wheels = [ [[package]] name = "dit" -version = "2.1" +version = "2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "boltons" }, @@ -518,9 +518,9 @@ dependencies = [ { name = "scipy" }, { name = "xarray" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e7/49/76c41727a2b328c0994d8f92a6289052d94f81ee3f0eee9178884c449d48/dit-2.1.tar.gz", hash = "sha256:e2f3c8267e95c48309ad41703f25d417fb40a6ccb4cbf83f215fa9a9ed99e048", size = 475878, upload-time = "2026-06-24T04:57:49.525Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/e3/17530ca2c0f2bc42f771700ca5ed61fefd0a447f7d593f1221cbcb1c3d1e/dit-2.2.tar.gz", hash = "sha256:957fea2890148c005aca0ed01ebde48ef5aecda8385072dc86c1fb2b0b3dd324", size = 547354, upload-time = "2026-07-14T03:45:02.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/0e/f2a6cafb83504c28b55d0ddc19bb755339694231f394b5f8e843c1e6307d/dit-2.1-py3-none-any.whl", hash = "sha256:2cde420e6ce7d2ab96480254f67396521dc778f19e83c7445fe4d415f0d4898d", size = 474587, upload-time = "2026-06-24T04:57:47.852Z" }, + { url = "https://files.pythonhosted.org/packages/4f/17/0e18c8c994535bc409f7455a5a34fcaa7e19f8f1a43648b4ffe329ef0718/dit-2.2-py3-none-any.whl", hash = "sha256:e8f1929e201d1229ed333e8032f5e39925f97071331e45e9cff81646dcf9ecdf", size = 540525, upload-time = "2026-07-14T03:44:59.975Z" }, ] [[package]] @@ -1444,7 +1444,7 @@ viz = [ [package.metadata] requires-dist = [ { name = "arviz", marker = "extra == 'bayes'" }, - { name = "dit", specifier = ">=2.1" }, + { name = "dit", specifier = ">=2.2" }, { name = "graphviz", marker = "extra == 'dev'", specifier = ">=0.20" }, { name = "graphviz", marker = "extra == 'test'", specifier = ">=0.20" }, { name = "graphviz", marker = "extra == 'viz'", specifier = ">=0.20" },