Core Concepts

This guide explains the fundamental concepts behind StateTracker’s design.

States

A State is an object that can take on a finite number of discrete values, numbered from 0 to num_values - 1. The simplest state is a “leaf” state created directly with a specified number of values:

from statetracker import Manager, State

with Manager():
    A = State(num_values=5, name="A")
    print(list(A))
[0, 1, 2, 3, 4]

States can be iterated to cycle through all their values, and their current value can be read or set via the value property.

The Manager Context

All states must be created within a Manager context. The Manager tracks all states and their relationships:

with Manager() as mgr:
    A = State(num_values=3, name="A")
    B = State(num_values=2, name="B")

    # Manager tracks all states
    print(mgr.get_all_names())
['A', 'B']

Creating states outside a Manager context raises a RuntimeError.

State Composition

The power of StateTracker comes from composing states using operations. When you combine states, you create a new “derived” state that depends on its “parent” states:

from statetracker import product

with Manager():
    A = State(num_values=2, name="A")
    B = State(num_values=3, name="B")

    # C is derived from A and B via product
    C = product([A, B], name="C")  # 6 values (2 x 3)

    print(f"C has {C.num_values} values")
C has 6 values

This creates a directed acyclic graph (DAG) of state dependencies. You can visualize this structure using print_dag():

with Manager():
    A = State(num_values=2, name="A")
    B = State(num_values=3, name="B")
    C = product([A, B], name="C")

    C.print_dag()
C (counter, io=0, n=6)
+-- [op=Product]
    +-- A (counter, io=0, n=2)
    +-- B (counter, io=0, n=3)

Unidirectional Value Propagation

StateTracker uses unidirectional value propagation: when you set the value of a derived state, the values of all its parent states are automatically computed and updated.

with Manager():
    A = State(num_values=2, name="A")
    B = State(num_values=3, name="B")
    C = product([A, B])

    C.value = 5  # Set the derived state's value

    # Parent values are automatically computed
    print(f"A.value = {A.value}")  # 1
    print(f"B.value = {B.value}")  # 2
A.value = 1
B.value = 2

This is the key insight: you iterate over the derived state, and the parent states automatically track along.

Active vs Inactive States

A state’s value can be either:

  • Active: An integer from 0 to num_values - 1

  • Inactive: None

Inactive states arise with operations like stack where only one parent is “active” at a time:

from statetracker import stack

with Manager():
    A = State(num_values=2, name="A")
    B = State(num_values=3, name="B")
    C = stack([A, B])  # 5 values total

    for value in C:
        print(f"C={value}, A={A.value}, B={B.value}")
C=0, A=0, B=None
C=1, A=1, B=None
C=2, A=None, B=0
C=3, A=None, B=1
C=4, A=None, B=2

Use is_active to check if a state is active. You can also use print_states() to conveniently display all values at once, or print_states(include_inactive=False) to show only active states.

Conflict Detection

When a state appears in multiple branches of the DAG, StateTracker detects conflicting value assignments. This happens when two different paths would assign different values to the same parent state.

with Manager():
    A = State(num_values=3, name="A")
    B = A[0:2]  # B=0 -> A=0, B=1 -> A=1
    C = A[1:3]  # C=0 -> A=1, C=1 -> A=2

    # Using product tries to activate BOTH B and C simultaneously
    D = product([B, C])

    # D value 0 means B=0 (A=0) and C=0 (A=1)
    # But A can't be both 0 and 1 at the same time!
    try:
        for value in D:
            pass
    except Exception as e:
        print(f"Error: {type(e).__name__}")
Error: ConflictingValueAssignmentError

Note that stack does NOT cause conflicts because it only activates one parent at a time. The conflict arises when an operation like product tries to activate multiple states that share a common ancestor with incompatible value requirements.

Iteration Order and ordered_product()

Sometimes you want certain states to have priority over others when they appear together in Cartesian products. The ordered_product() function reads an iter_order property from each state to determine ordering. States with lower iter_order values iterate “faster” (change more frequently in the inner loop).

from statetracker import ordered_product

with Manager():
    A = State(num_values=2, name="A", iter_order=0)  # Fast
    B = State(num_values=2, name="B", iter_order=1)  # Slow

    C = ordered_product([A, B])

    for _ in C:
        print(f"A={A.value}, B={B.value}")
A=0, B=0
A=1, B=0
A=0, B=1
A=1, B=1

The default iter_order is 0 for leaf states. Derived states inherit the minimum iter_order of their parents.

Note that the regular product() function does not use iter_order — it preserves the exact order of states you pass to it.

State Identity

Each state has a unique id assigned by the Manager. This is used for:

  • Deduplication in ordered_product()

  • Tie-breaking when states have the same iter_order

States are compared by identity (is), not value, so two states with the same num_values are still distinct objects.

Copying States

States support two types of copying:

  • copy(): Creates a new state with the same parents (shallow copy)

  • deepcopy(): Recursively creates a new state with copied ancestors

with Manager():
    A = State(num_values=3, name="A")
    B = A[1:3]

    # Shallow copy: C shares parent A with B
    C = B.copy(name="C")

    # Deep copy: D has its own copy of the parent
    D = B.deepcopy(name="D")

    print("B's DAG:")
    B.print_dag()
    print("\nC's DAG (shallow copy - shares A):")
    C.print_dag()
    print("\nD's DAG (deep copy - has own parent):")
    D.print_dag()
B's DAG:
Counter[1] (counter, io=0, n=2)
+-- [op=Slice]
    +-- A (counter, io=0, n=3)

C's DAG (shallow copy - shares A):
C (counter, io=0, n=2)
+-- [op=Slice]
    +-- A (counter, io=0, n=3)

D's DAG (deep copy - has own parent):
D (counter, io=0, n=2)
+-- [op=Slice]
    +-- Counter[3] (counter, io=0, n=3)