Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions pensive/automata/transducer_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 2 additions & 5 deletions pensive/automata/transducers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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()
Expand Down
173 changes: 140 additions & 33 deletions pensive/examples/processes.py

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions pensive/generators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
33 changes: 25 additions & 8 deletions pensive/generators/block_convergence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)$")
Expand All @@ -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:
Expand Down Expand Up @@ -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__}
)


Expand Down Expand Up @@ -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(
Expand Down
1 change: 0 additions & 1 deletion pensive/generators/directional_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions pensive/generators/measures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
8 changes: 6 additions & 2 deletions pensive/generators/stack_hmm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 16 additions & 8 deletions pensive/generators/stack_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 6 additions & 4 deletions pensive/generators/synchronization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions pensive/generators/topological_epsilon_enumeration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 10 additions & 2 deletions pensive/inference/bayesian/comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions pensive/inference/bayesian/counts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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 = []
Expand Down
4 changes: 1 addition & 3 deletions pensive/inference/bayesian/diversity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
8 changes: 6 additions & 2 deletions pensive/inference/bayesian/epsilon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
21 changes: 14 additions & 7 deletions pensive/inference/bayesian/markov.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
6 changes: 1 addition & 5 deletions pensive/inference/bayesian/stack_hmm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading