Core Concepts
This guide explains the fundamental concepts behind StateTracker’s design.
States and States
A State is an object that can take on a finite number of discrete states, numbered from 0 to num_states - 1. The simplest state is a “leaf” state created directly with a specified number of states:
[1]:
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 states, and their current state can be read or set via the state property.
The Manager Context
All states must be created within a Manager context. The Manager tracks all states and their relationships:
[2]:
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:
[3]:
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 states (2 × 3)
print(f"C has {C.num_values} states")
C has 6 states
This creates a directed acyclic graph (DAG) of state dependencies. You can visualize this structure using print_dag():
[4]:
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 State Propagation
StateTracker uses unidirectional state propagation: when you set the state of a derived state, the states of all its parent states are automatically computed and updated.
[5]:
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 state
# Parent states are automatically computed
print(f"A.value = {A.value}") # 1
print(f"B.value = {B.value}") # 2
A.state = 1
B.state = 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 state can be either:
Active: An integer from 0 to
num_states - 1Inactive:
None
Inactive states arise with operations like stack where only one parent is “active” at a time:
[6]:
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 states total
for state in C:
print(f"C={state}, 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 states 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 state assignments. This happens when two different paths would assign different values to the same parent state.
[7]:
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 state 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 state in D:
pass
except Exception as e:
print(f"Error: {type(e).__name__}")
Error: ConflictingStateAssignmentError
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 state 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).
[8]:
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_states 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
[9]:
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)