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 - 1Inactive:
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)