Skip to content

emic.types

Core types for epsilon-machines.

types

Core types for epsilon-machine representation.

Public API
  • Symbol (TypeVar)
  • Alphabet (Protocol)
  • ConcreteAlphabet
  • Distribution
  • Transition
  • CausalState
  • EpsilonMachine
  • EpsilonMachineBuilder

Symbol module-attribute

Symbol = TypeVar('Symbol', bound=Hashable)

StateId module-attribute

StateId = str

ProbabilityValue module-attribute

ProbabilityValue = float

Alphabet

Bases: Protocol[Symbol_co]

A finite set of symbols.

This protocol defines the interface for alphabets used in epsilon-machines. Any type implementing this protocol can be used as an alphabet.

symbols property

symbols: frozenset[Symbol_co]

The set of all symbols.

__contains__

__contains__(symbol: object) -> bool

Check if symbol is in alphabet.

Source code in src/emic/types/alphabet.py
def __contains__(self, symbol: object) -> bool:
    """Check if symbol is in alphabet."""
    ...

__iter__

__iter__() -> Iterator[Symbol_co]

Iterate over symbols.

Source code in src/emic/types/alphabet.py
def __iter__(self) -> Iterator[Symbol_co]:
    """Iterate over symbols."""
    ...

__len__

__len__() -> int

Number of symbols.

Source code in src/emic/types/alphabet.py
def __len__(self) -> int:
    """Number of symbols."""
    ...

ConcreteAlphabet dataclass

ConcreteAlphabet(_symbols: frozenset[A])

Bases: Generic[A]

Immutable alphabet implementation.

A concrete implementation of the Alphabet protocol using a frozenset to store symbols. This is the standard alphabet type for most use cases.

Examples:

>>> alpha = ConcreteAlphabet.binary()
>>> 0 in alpha
True
>>> len(alpha)
2
>>> alpha = ConcreteAlphabet.from_symbols('a', 'b', 'c')
>>> list(sorted(alpha))
['a', 'b', 'c']

symbols property

symbols: frozenset[A]

The set of all symbols.

__contains__

__contains__(symbol: object) -> bool

Check if symbol is in alphabet.

Source code in src/emic/types/alphabet.py
def __contains__(self, symbol: object) -> bool:
    """Check if symbol is in alphabet."""
    return symbol in self._symbols

__iter__

__iter__() -> Iterator[A]

Iterate over symbols.

Source code in src/emic/types/alphabet.py
def __iter__(self) -> Iterator[A]:
    """Iterate over symbols."""
    return iter(self._symbols)

__len__

__len__() -> int

Number of symbols.

Source code in src/emic/types/alphabet.py
def __len__(self) -> int:
    """Number of symbols."""
    return len(self._symbols)

binary staticmethod

binary() -> ConcreteAlphabet[int]

Create binary alphabet {0, 1}.

Returns:

Type Description
ConcreteAlphabet[int]

An alphabet containing exactly the integers 0 and 1.

Examples:

>>> alpha = ConcreteAlphabet.binary()
>>> sorted(alpha.symbols)
[0, 1]
Source code in src/emic/types/alphabet.py
@staticmethod
def binary() -> "ConcreteAlphabet[int]":
    """
    Create binary alphabet {0, 1}.

    Returns:
        An alphabet containing exactly the integers 0 and 1.

    Examples:
        >>> alpha = ConcreteAlphabet.binary()
        >>> sorted(alpha.symbols)
        [0, 1]
    """
    return ConcreteAlphabet(frozenset({0, 1}))

from_symbols classmethod

from_symbols(*symbols: A) -> ConcreteAlphabet[A]

Create alphabet from symbols.

Parameters:

Name Type Description Default
*symbols A

Variable number of hashable symbols.

()

Returns:

Type Description
ConcreteAlphabet[A]

An alphabet containing the given symbols.

Examples:

>>> alpha = ConcreteAlphabet.from_symbols('H', 'T')
>>> 'H' in alpha
True
Source code in src/emic/types/alphabet.py
@classmethod
def from_symbols(cls, *symbols: A) -> "ConcreteAlphabet[A]":
    """
    Create alphabet from symbols.

    Args:
        *symbols: Variable number of hashable symbols.

    Returns:
        An alphabet containing the given symbols.

    Examples:
        >>> alpha = ConcreteAlphabet.from_symbols('H', 'T')
        >>> 'H' in alpha
        True
    """
    return cls(frozenset(symbols))

Distribution dataclass

Distribution(_probs: Mapping[A, ProbabilityValue])

Bases: Generic[A]

An immutable probability distribution over symbols.

Invariants
  • All probabilities are in [0, 1]
  • Probabilities sum to 1 (within tolerance)
  • Only non-zero probabilities are stored

Examples:

>>> dist = Distribution({'a': 0.7, 'b': 0.3})
>>> dist['a']
0.7
>>> dist['c']  # Not in distribution
0.0
>>> dist.entropy()
0.881...

support property

support: frozenset[A]

Symbols with non-zero probability.

probs property

probs: Mapping[A, ProbabilityValue]

The underlying probability mapping (read-only).

__post_init__

__post_init__() -> None

Validate probabilities sum to 1 and are in valid range.

Source code in src/emic/types/probability.py
def __post_init__(self) -> None:
    """Validate probabilities sum to 1 and are in valid range."""
    total = sum(self._probs.values())
    if not (1.0 - _SUM_TOLERANCE <= total <= 1.0 + _SUM_TOLERANCE):
        msg = f"Probabilities must sum to 1, got {total}"
        raise ValueError(msg)
    for symbol, p in self._probs.items():
        if not (0.0 - _PROBABILITY_TOLERANCE <= p <= 1.0 + _PROBABILITY_TOLERANCE):
            msg = f"Probability must be in [0,1], got {p} for symbol {symbol}"
            raise ValueError(msg)

__getitem__

__getitem__(symbol: A) -> ProbabilityValue

Get probability of a symbol (0.0 if not in support).

Source code in src/emic/types/probability.py
def __getitem__(self, symbol: A) -> ProbabilityValue:
    """Get probability of a symbol (0.0 if not in support)."""
    return self._probs.get(symbol, 0.0)

__iter__

__iter__() -> Iterator[A]

Iterate over symbols in the support.

Source code in src/emic/types/probability.py
def __iter__(self) -> Iterator[A]:
    """Iterate over symbols in the support."""
    return iter(self._probs)

__len__

__len__() -> int

Number of symbols in the support.

Source code in src/emic/types/probability.py
def __len__(self) -> int:
    """Number of symbols in the support."""
    return len(self._probs)

entropy

entropy() -> float

Shannon entropy of the distribution in bits.

Returns:

Type Description
float

The entropy H = -Σ p(x) log₂ p(x)

Examples:

>>> Distribution.uniform(frozenset({0, 1})).entropy()
1.0
>>> abs(Distribution.deterministic('a').entropy()) < 1e-10
True
Source code in src/emic/types/probability.py
def entropy(self) -> float:
    """
    Shannon entropy of the distribution in bits.

    Returns:
        The entropy H = -Σ p(x) log₂ p(x)

    Examples:
        >>> Distribution.uniform(frozenset({0, 1})).entropy()
        1.0
        >>> abs(Distribution.deterministic('a').entropy()) < 1e-10
        True
    """
    return -sum(p * math.log2(p) for p in self._probs.values() if p > 0)

uniform classmethod

uniform(symbols: frozenset[A]) -> Self

Create uniform distribution over symbols.

Parameters:

Name Type Description Default
symbols frozenset[A]

Set of symbols to distribute probability over.

required

Returns:

Type Description
Self

A distribution with equal probability for each symbol.

Raises:

Type Description
ValueError

If symbols is empty.

Examples:

>>> dist = Distribution.uniform(frozenset({'a', 'b'}))
>>> dist['a']
0.5
Source code in src/emic/types/probability.py
@classmethod
def uniform(cls, symbols: frozenset[A]) -> Self:
    """
    Create uniform distribution over symbols.

    Args:
        symbols: Set of symbols to distribute probability over.

    Returns:
        A distribution with equal probability for each symbol.

    Raises:
        ValueError: If symbols is empty.

    Examples:
        >>> dist = Distribution.uniform(frozenset({'a', 'b'}))
        >>> dist['a']
        0.5
    """
    if not symbols:
        msg = "Cannot create uniform distribution over empty set"
        raise ValueError(msg)
    n = len(symbols)
    return cls(dict.fromkeys(symbols, 1.0 / n))

deterministic classmethod

deterministic(symbol: A) -> Self

Create distribution with all mass on one symbol.

Parameters:

Name Type Description Default
symbol A

The symbol to assign probability 1.0.

required

Returns:

Type Description
Self

A distribution where only the given symbol has non-zero probability.

Examples:

>>> dist = Distribution.deterministic('x')
>>> dist['x']
1.0
>>> dist['y']
0.0
Source code in src/emic/types/probability.py
@classmethod
def deterministic(cls, symbol: A) -> Self:
    """
    Create distribution with all mass on one symbol.

    Args:
        symbol: The symbol to assign probability 1.0.

    Returns:
        A distribution where only the given symbol has non-zero probability.

    Examples:
        >>> dist = Distribution.deterministic('x')
        >>> dist['x']
        1.0
        >>> dist['y']
        0.0
    """
    return cls({symbol: 1.0})

Transition dataclass

Transition(
    symbol: A,
    probability: ProbabilityValue,
    target: StateId,
)

Bases: Generic[A]

A labeled transition from one state to another.

Represents: "On symbol symbol, go to target with probability probability"

Attributes:

Name Type Description
symbol A

The emitted symbol for this transition.

probability ProbabilityValue

The probability of taking this transition (must be in (0, 1]).

target StateId

The ID of the target state.

Examples:

>>> t = Transition(symbol='a', probability=0.5, target='S1')
>>> t.symbol
'a'
>>> t.probability
0.5

__post_init__

__post_init__() -> None

Validate that probability is in valid range.

Source code in src/emic/types/states.py
def __post_init__(self) -> None:
    """Validate that probability is in valid range."""
    if not (0.0 < self.probability <= 1.0 + _PROBABILITY_TOLERANCE):
        msg = f"Probability must be in (0,1], got {self.probability}"
        raise ValueError(msg)

CausalState dataclass

CausalState(
    id: StateId, transitions: frozenset[Transition[A]]
)

Bases: Generic[A]

A causal state in an epsilon-machine.

A causal state is an equivalence class of histories that induce the same conditional distribution over futures.

Attributes:

Name Type Description
id StateId

Unique identifier for this state.

transitions frozenset[Transition[A]]

Set of outgoing transitions from this state.

Examples:

>>> t1 = Transition(symbol=0, probability=0.5, target='A')
>>> t2 = Transition(symbol=1, probability=0.5, target='B')
>>> state = CausalState(id='A', transitions=frozenset({t1, t2}))
>>> state.alphabet
frozenset({0, 1})

alphabet property

alphabet: frozenset[A]

Symbols that have transitions from this state.

__post_init__

__post_init__() -> None

Validate transition probabilities for each symbol sum to <= 1.

Source code in src/emic/types/states.py
def __post_init__(self) -> None:
    """Validate transition probabilities for each symbol sum to <= 1."""
    by_symbol: dict[A, float] = {}
    for t in self.transitions:
        by_symbol[t.symbol] = by_symbol.get(t.symbol, 0.0) + t.probability
    for symbol, total in by_symbol.items():
        if total > 1.0 + _PROBABILITY_TOLERANCE:
            msg = f"Transitions for symbol {symbol} sum to {total} > 1"
            raise ValueError(msg)

transition_distribution

transition_distribution(
    symbol: A,
) -> Distribution[StateId]

Get the distribution over next states given a symbol.

Parameters:

Name Type Description Default
symbol A

The input symbol.

required

Returns:

Type Description
Distribution[StateId]

A distribution over target state IDs.

Raises:

Type Description
KeyError

If symbol has no transitions from this state.

Examples:

>>> t = Transition(symbol=0, probability=1.0, target='B')
>>> state = CausalState(id='A', transitions=frozenset({t}))
>>> dist = state.transition_distribution(0)
>>> dist['B']
1.0
Source code in src/emic/types/states.py
def transition_distribution(self, symbol: A) -> Distribution[StateId]:
    """
    Get the distribution over next states given a symbol.

    Args:
        symbol: The input symbol.

    Returns:
        A distribution over target state IDs.

    Raises:
        KeyError: If symbol has no transitions from this state.

    Examples:
        >>> t = Transition(symbol=0, probability=1.0, target='B')
        >>> state = CausalState(id='A', transitions=frozenset({t}))
        >>> dist = state.transition_distribution(0)
        >>> dist['B']
        1.0
    """
    relevant = [t for t in self.transitions if t.symbol == symbol]
    if not relevant:
        msg = f"No transition for symbol {symbol} from state {self.id}"
        raise KeyError(msg)
    return Distribution({t.target: t.probability for t in relevant})

emission_distribution

emission_distribution() -> Distribution[A]

Get the distribution over emitted symbols from this state.

Note: This assumes the machine is "edge-emitting" (symbols on transitions).

Returns:

Type Description
Distribution[A]

A distribution over symbols that can be emitted from this state.

Examples:

>>> t1 = Transition(symbol=0, probability=0.5, target='A')
>>> t2 = Transition(symbol=1, probability=0.5, target='B')
>>> state = CausalState(id='A', transitions=frozenset({t1, t2}))
>>> dist = state.emission_distribution()
>>> dist[0]
0.5
Source code in src/emic/types/states.py
def emission_distribution(self) -> Distribution[A]:
    """
    Get the distribution over emitted symbols from this state.

    Note: This assumes the machine is "edge-emitting" (symbols on transitions).

    Returns:
        A distribution over symbols that can be emitted from this state.

    Examples:
        >>> t1 = Transition(symbol=0, probability=0.5, target='A')
        >>> t2 = Transition(symbol=1, probability=0.5, target='B')
        >>> state = CausalState(id='A', transitions=frozenset({t1, t2}))
        >>> dist = state.emission_distribution()
        >>> dist[0]
        0.5
    """
    probs: dict[A, float] = {}
    for t in self.transitions:
        probs[t.symbol] = probs.get(t.symbol, 0.0) + t.probability
    return Distribution(probs)

next_states

next_states(symbol: A) -> frozenset[StateId]

Get possible next states given a symbol.

Parameters:

Name Type Description Default
symbol A

The input symbol.

required

Returns:

Type Description
frozenset[StateId]

Set of state IDs that can be reached with this symbol.

Examples:

>>> t = Transition(symbol=0, probability=1.0, target='B')
>>> state = CausalState(id='A', transitions=frozenset({t}))
>>> state.next_states(0)
frozenset({'B'})
Source code in src/emic/types/states.py
def next_states(self, symbol: A) -> frozenset[StateId]:
    """
    Get possible next states given a symbol.

    Args:
        symbol: The input symbol.

    Returns:
        Set of state IDs that can be reached with this symbol.

    Examples:
        >>> t = Transition(symbol=0, probability=1.0, target='B')
        >>> state = CausalState(id='A', transitions=frozenset({t}))
        >>> state.next_states(0)
        frozenset({'B'})
    """
    return frozenset(t.target for t in self.transitions if t.symbol == symbol)

EpsilonMachine dataclass

EpsilonMachine(
    alphabet: frozenset[A],
    states: frozenset[CausalState[A]],
    start_state: StateId,
    stationary_distribution: Distribution[StateId],
)

Bases: Generic[A]

An epsilon-machine (ε-machine) over alphabet A.

An ε-machine is the minimal, optimal predictor of a stationary stochastic process. It consists of: - A finite set of causal states - Labeled transitions between states - A stationary distribution over states

Properties: - Unifilarity: Each (state, symbol) pair has at most one outgoing transition - Minimality: No two states have the same conditional future distribution

This class represents the result of inference - the discovered structure.

Attributes:

Name Type Description
alphabet frozenset[A]

The set of symbols used by this machine.

states frozenset[CausalState[A]]

The set of causal states.

start_state StateId

The ID of the initial state.

stationary_distribution Distribution[StateId]

The steady-state distribution over states.

Examples:

>>> from emic.types import EpsilonMachineBuilder
>>> machine = (
...     EpsilonMachineBuilder[int]()
...     .add_transition("A", 0, "A", 0.5)
...     .add_transition("A", 1, "B", 0.5)
...     .add_transition("B", 0, "A", 1.0)
...     .with_start_state("A")
...     .build()
... )
>>> len(machine)
2
>>> machine.is_unifilar()
True

state_ids property

state_ids: frozenset[StateId]

Set of all state IDs.

__post_init__

__post_init__() -> None

Validate machine invariants.

Source code in src/emic/types/machine.py
def __post_init__(self) -> None:
    """Validate machine invariants."""
    # Validate start state exists
    state_ids = frozenset(s.id for s in self.states)
    if self.start_state not in state_ids:
        msg = f"Start state {self.start_state} not in states"
        raise ValueError(msg)

    # Validate stationary distribution is over states
    for state_id in self.stationary_distribution.support:
        if state_id not in state_ids:
            msg = f"Stationary distribution contains unknown state {state_id}"
            raise ValueError(msg)

    # Validate unifilarity
    for state in self.states:
        seen: dict[A, StateId] = {}
        for t in state.transitions:
            if t.symbol in seen and seen[t.symbol] != t.target:
                msg = (
                    f"State {state.id} violates unifilarity: "
                    f"symbol {t.symbol} goes to both {seen[t.symbol]} and {t.target}"
                )
                raise ValueError(msg)
            seen[t.symbol] = t.target

__len__

__len__() -> int

Number of causal states.

Source code in src/emic/types/machine.py
def __len__(self) -> int:
    """Number of causal states."""
    return len(self.states)

get_state

get_state(state_id: StateId) -> CausalState[A]

Get a state by ID.

Parameters:

Name Type Description Default
state_id StateId

The ID of the state to retrieve.

required

Returns:

Type Description
CausalState[A]

The CausalState with the given ID.

Raises:

Type Description
KeyError

If state not found.

Examples:

>>> from emic.types import EpsilonMachineBuilder
>>> machine = (
...     EpsilonMachineBuilder[int]()
...     .add_transition("A", 0, "A", 1.0)
...     .with_start_state("A")
...     .build()
... )
>>> state = machine.get_state("A")
>>> state.id
'A'
Source code in src/emic/types/machine.py
def get_state(self, state_id: StateId) -> CausalState[A]:
    """
    Get a state by ID.

    Args:
        state_id: The ID of the state to retrieve.

    Returns:
        The CausalState with the given ID.

    Raises:
        KeyError: If state not found.

    Examples:
        >>> from emic.types import EpsilonMachineBuilder
        >>> machine = (
        ...     EpsilonMachineBuilder[int]()
        ...     .add_transition("A", 0, "A", 1.0)
        ...     .with_start_state("A")
        ...     .build()
        ... )
        >>> state = machine.get_state("A")
        >>> state.id
        'A'
    """
    for state in self.states:
        if state.id == state_id:
            return state
    msg = f"State {state_id} not found"
    raise KeyError(msg)

transition_matrix

transition_matrix(
    symbol: A,
) -> dict[StateId, Distribution[StateId]]

Get the transition matrix for a given symbol.

Returns a mapping from source state to distribution over target states. Only states that have transitions for the given symbol are included.

Parameters:

Name Type Description Default
symbol A

The symbol to get transitions for.

required

Returns:

Type Description
dict[StateId, Distribution[StateId]]

A dict mapping state IDs to distributions over next states.

Examples:

>>> from emic.types import EpsilonMachineBuilder
>>> machine = (
...     EpsilonMachineBuilder[int]()
...     .add_transition("A", 0, "B", 1.0)
...     .add_transition("B", 0, "A", 1.0)
...     .with_start_state("A")
...     .build()
... )
>>> matrix = machine.transition_matrix(0)
>>> matrix["A"]["B"]
1.0
Source code in src/emic/types/machine.py
def transition_matrix(self, symbol: A) -> dict[StateId, Distribution[StateId]]:
    """
    Get the transition matrix for a given symbol.

    Returns a mapping from source state to distribution over target states.
    Only states that have transitions for the given symbol are included.

    Args:
        symbol: The symbol to get transitions for.

    Returns:
        A dict mapping state IDs to distributions over next states.

    Examples:
        >>> from emic.types import EpsilonMachineBuilder
        >>> machine = (
        ...     EpsilonMachineBuilder[int]()
        ...     .add_transition("A", 0, "B", 1.0)
        ...     .add_transition("B", 0, "A", 1.0)
        ...     .with_start_state("A")
        ...     .build()
        ... )
        >>> matrix = machine.transition_matrix(0)
        >>> matrix["A"]["B"]
        1.0
    """
    result: dict[StateId, Distribution[StateId]] = {}
    for state in self.states:
        if symbol in state.alphabet:
            result[state.id] = state.transition_distribution(symbol)
    return result

is_unifilar

is_unifilar() -> bool

Check if machine is unifilar (deterministic given state and symbol).

A machine is unifilar if, for each state, each symbol leads to at most one target state. This is a fundamental property of epsilon-machines.

Returns:

Type Description
bool

True if the machine is unifilar, False otherwise.

Examples:

>>> from emic.types import EpsilonMachineBuilder
>>> machine = (
...     EpsilonMachineBuilder[int]()
...     .add_transition("A", 0, "A", 1.0)
...     .with_start_state("A")
...     .build()
... )
>>> machine.is_unifilar()
True
Source code in src/emic/types/machine.py
def is_unifilar(self) -> bool:
    """
    Check if machine is unifilar (deterministic given state and symbol).

    A machine is unifilar if, for each state, each symbol leads to at most
    one target state. This is a fundamental property of epsilon-machines.

    Returns:
        True if the machine is unifilar, False otherwise.

    Examples:
        >>> from emic.types import EpsilonMachineBuilder
        >>> machine = (
        ...     EpsilonMachineBuilder[int]()
        ...     .add_transition("A", 0, "A", 1.0)
        ...     .with_start_state("A")
        ...     .build()
        ... )
        >>> machine.is_unifilar()
        True
    """
    for state in self.states:
        symbols_seen: set[A] = set()
        for t in state.transitions:
            if t.symbol in symbols_seen:
                return False
            symbols_seen.add(t.symbol)
    return True

is_ergodic

is_ergodic() -> bool

Check if machine is ergodic (single recurrent class).

Returns:

Type Description
bool

True if the machine is ergodic, False otherwise.

Raises:

Type Description
NotImplementedError

This method is not yet implemented.

Source code in src/emic/types/machine.py
def is_ergodic(self) -> bool:
    """
    Check if machine is ergodic (single recurrent class).

    Returns:
        True if the machine is ergodic, False otherwise.

    Raises:
        NotImplementedError: This method is not yet implemented.
    """
    # TODO: Implement graph connectivity check
    raise NotImplementedError

EpsilonMachineBuilder

EpsilonMachineBuilder()

Bases: Generic[A]

Mutable builder for constructing EpsilonMachine instances.

Since EpsilonMachine is immutable, this builder provides an ergonomic way to construct machines incrementally.

Examples:

>>> machine = (
...     EpsilonMachineBuilder[int]()
...     .with_alphabet({0, 1})
...     .add_state("A")
...     .add_state("B")
...     .add_transition("A", 0, "A", 0.5)
...     .add_transition("A", 1, "B", 0.5)
...     .add_transition("B", 0, "A", 1.0)
...     .with_start_state("A")
...     .build()
... )
>>> len(machine)
2

Initialize an empty builder.

Source code in src/emic/types/machine.py
def __init__(self) -> None:
    """Initialize an empty builder."""
    self._alphabet: set[A] = set()
    self._states: dict[StateId, list[Transition[A]]] = {}
    self._start_state: StateId | None = None
    self._stationary: dict[StateId, float] | None = None

with_alphabet

with_alphabet(symbols: set[A]) -> Self

Set the alphabet for the machine.

Parameters:

Name Type Description Default
symbols set[A]

The set of symbols to use.

required

Returns:

Type Description
Self

Self for method chaining.

Source code in src/emic/types/machine.py
def with_alphabet(self, symbols: set[A]) -> Self:
    """
    Set the alphabet for the machine.

    Args:
        symbols: The set of symbols to use.

    Returns:
        Self for method chaining.
    """
    self._alphabet = symbols
    return self

add_state

add_state(state_id: StateId) -> Self

Add a state to the machine.

Parameters:

Name Type Description Default
state_id StateId

The ID of the state to add.

required

Returns:

Type Description
Self

Self for method chaining.

Source code in src/emic/types/machine.py
def add_state(self, state_id: StateId) -> Self:
    """
    Add a state to the machine.

    Args:
        state_id: The ID of the state to add.

    Returns:
        Self for method chaining.
    """
    if state_id not in self._states:
        self._states[state_id] = []
    return self

add_transition

add_transition(
    source: StateId,
    symbol: A,
    target: StateId,
    probability: float,
) -> Self

Add a transition to the machine.

This will also add the source and target states if they don't exist, and add the symbol to the alphabet.

Parameters:

Name Type Description Default
source StateId

The source state ID.

required
symbol A

The symbol emitted on this transition.

required
target StateId

The target state ID.

required
probability float

The probability of this transition.

required

Returns:

Type Description
Self

Self for method chaining.

Source code in src/emic/types/machine.py
def add_transition(
    self,
    source: StateId,
    symbol: A,
    target: StateId,
    probability: float,
) -> Self:
    """
    Add a transition to the machine.

    This will also add the source and target states if they don't exist,
    and add the symbol to the alphabet.

    Args:
        source: The source state ID.
        symbol: The symbol emitted on this transition.
        target: The target state ID.
        probability: The probability of this transition.

    Returns:
        Self for method chaining.
    """
    self.add_state(source)
    self.add_state(target)
    self._alphabet.add(symbol)
    self._states[source].append(Transition(symbol, probability, target))
    return self

with_start_state

with_start_state(state_id: StateId) -> Self

Set the start state for the machine.

Parameters:

Name Type Description Default
state_id StateId

The ID of the start state.

required

Returns:

Type Description
Self

Self for method chaining.

Source code in src/emic/types/machine.py
def with_start_state(self, state_id: StateId) -> Self:
    """
    Set the start state for the machine.

    Args:
        state_id: The ID of the start state.

    Returns:
        Self for method chaining.
    """
    self._start_state = state_id
    return self

with_stationary_distribution

with_stationary_distribution(
    dist: dict[StateId, float],
) -> Self

Set the stationary distribution for the machine.

Parameters:

Name Type Description Default
dist dict[StateId, float]

A mapping from state IDs to probabilities.

required

Returns:

Type Description
Self

Self for method chaining.

Source code in src/emic/types/machine.py
def with_stationary_distribution(
    self,
    dist: dict[StateId, float],
) -> Self:
    """
    Set the stationary distribution for the machine.

    Args:
        dist: A mapping from state IDs to probabilities.

    Returns:
        Self for method chaining.
    """
    self._stationary = dist
    return self

build

build() -> EpsilonMachine[A]

Build the EpsilonMachine.

Returns:

Type Description
EpsilonMachine[A]

The constructed EpsilonMachine.

Raises:

Type Description
ValueError

If start state is not set.

Source code in src/emic/types/machine.py
def build(self) -> EpsilonMachine[A]:
    """
    Build the EpsilonMachine.

    Returns:
        The constructed EpsilonMachine.

    Raises:
        ValueError: If start state is not set.
    """
    if self._start_state is None:
        msg = "Start state not set"
        raise ValueError(msg)

    states = frozenset(
        CausalState(id=sid, transitions=frozenset(trans)) for sid, trans in self._states.items()
    )

    # Compute stationary distribution if not provided
    if self._stationary is None:
        self._stationary = self._compute_stationary_distribution()

    return EpsilonMachine(
        alphabet=frozenset(self._alphabet),
        states=states,
        start_state=self._start_state,
        stationary_distribution=Distribution(self._stationary),
    )