Operations Guide
This guide provides detailed explanations and examples for each state operation available in StateTracker.
Product (Cartesian Product)
The product operation creates a state whose states enumerate all combinations of parent state states—the Cartesian product.
Using product()
Use the product() function to create a Cartesian product:
[ ]:
from statetracker import Manager, State, product
with Manager():
A = State(num_values=2, name="A")
B = State(num_values=3, name="B")
C = product([A, B]) # 6 states (2 × 3)
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
A=0, B=2
A=1, B=2
The first state (A) varies fastest, cycling through all its states before the second state (B) advances.
You can combine more than two states:
[ ]:
with Manager():
A = State(num_values=2, name="A")
B = State(num_values=2, name="B")
C = State(num_values=2, name="C")
D = product([A, B, C], name="D") # 8 states
print(f"D has {D.num_values} states")
D has 8 states
Note:
product()does not allow duplicate states. Useordered_product()if you need automatic deduplication.
Using ordered_product()
The ordered_product() function automatically:
Removes duplicate states
Flattens nested products
Orders states by
iter_orderandid
[ ]:
from statetracker import ordered_product
with Manager():
A = State(num_values=2, name="A")
B = State(num_values=3, name="B")
# Duplicates are automatically removed
C = ordered_product([A, B, A]) # Same as ordered_product([A, B])
print(f"C has {C.num_values} states")
C has 6 states
Control the ordering with set_product_order_mode():
[ ]:
from statetracker import set_product_order_mode
set_product_order_mode("first_state_fastest") # Default behavior
# set_product_order_mode('first_state_slowest') # Reverse ordering
Stack (Disjoint Union)
The stack operation creates a state that iterates through each parent state sequentially—a disjoint union. 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 states (2 + 3)
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
Notice that when A is active, B is None (inactive), and vice versa.
Synchronize
The sync operation creates a state that keeps multiple parent states in lockstep—they all have the same state value.
[ ]:
from statetracker import sync
with Manager():
A = State(num_values=4, name="A")
B = State(num_values=4, name="B")
C = sync([A, B]) # 4 states
for _ in C:
print(f"A={A.value}, B={B.value}")
A=0, B=0
A=1, B=1
A=2, B=2
A=3, B=3
Warning: All states passed to
sync()must have the samenum_states.
Slice
The slice operation selects a subset of states from a parent state, similar to Python list slicing.
Using Index Notation
The most convenient way is Python’s slice syntax:
[ ]:
with Manager():
A = State(num_values=10, name="A")
B = A[2:5] # States 2, 3, 4
C = A[::2] # Even states: 0, 2, 4, 6, 8
D = A[::-1] # Reversed: 9, 8, 7, ..., 0
E = A[3] # Single state: 3
print(f"B (A[2:5]) has {B.num_values} states")
print(f"C (A[::2]) has {C.num_values} states")
print(f"D (A[::-1]) has {D.num_values} states")
print(f"E (A[3]) has {E.num_values} states")
B (A[2:5]) has 3 states
C (A[::2]) has 5 states
D (A[::-1]) has 10 states
E (A[3]) has 1 states
Using slice()
The slice() function provides the same functionality:
[ ]:
from statetracker import slice
with Manager():
A = State(num_values=10, name="A")
B = slice(A, start=2, stop=5) # States 2, 3, 4
C = slice(A, step=2) # Even states
print(f"B has {B.num_values} states")
print(f"C has {C.num_values} states")
B has 3 states
C has 5 states
Repeat
The repeat operation creates a state that cycles through the parent’s states multiple times.
[ ]:
from statetracker import repeat
with Manager():
A = State(num_values=3, name="A")
B = repeat(A, times=2) # 6 states
for state in B:
print(f"B={state}, A={A.value}")
B=0, A=0
B=1, A=1
B=2, A=2
B=3, A=0
B=4, A=1
B=5, A=2
Shuffle
The shuffle operation creates a state with randomly permuted states. The shuffle is deterministic when a seed is provided.
[ ]:
from statetracker import shuffle
with Manager():
A = State(num_values=5, name="A")
# Random permutation with seed for reproducibility
B = shuffle(A, seed=42)
for state in B:
print(f"B={state}, A={A.value}")
B=0, A=3
B=1, A=1
B=2, A=2
B=3, A=4
B=4, A=0
You can also provide an explicit permutation:
[ ]:
with Manager():
A = State(num_values=4, name="A")
# Explicit permutation: reverse order
B = shuffle(A, permutation=[3, 2, 1, 0])
for state in B:
print(f"B={state}, A={A.value}")
B=0, A=3
B=1, A=2
B=2, A=1
B=3, A=0
Sample
The sample operation creates a state with sampled states from the parent. This is useful for creating random subsets.
[ ]:
from statetracker import sample
with Manager():
A = State(num_values=100, name="A")
# Sample 10 states with replacement
B = sample(A, num_values=10, seed=42)
# Sample 10 states without replacement
C = sample(A, num_values=10, seed=42, with_replacement=False)
print(f"B (with replacement) has {B.num_values} states")
print(f"C (without replacement) has {C.num_values} states")
B (with replacement) has 10 states
C (without replacement) has 10 states
You can also provide explicit sampled states:
[ ]:
with Manager():
A = State(num_values=10, name="A")
# Explicit states to sample
B = sample(A, sampled_states=[0, 2, 4, 6, 8])
for state in B:
print(f"B={state}, A={A.value}")
B=0, A=0
B=1, A=2
B=2, A=4
B=3, A=6
B=4, A=8
Split
The split operation divides a state into multiple sub-states.
Equal Split
Split into N roughly equal parts:
[ ]:
from statetracker import split
with Manager():
A = State(num_values=10, name="A")
# Split into 3 parts: sizes 4, 3, 3
B, C, D = split(A, 3)
print(f"B: {B.num_values} states")
print(f"C: {C.num_values} states")
print(f"D: {D.num_values} states")
B: 4 states
C: 3 states
D: 3 states
Proportional Split
Split according to proportions:
[ ]:
with Manager():
A = State(num_values=100, name="A")
# Split 80/20
train, test = split(A, [0.8, 0.2], names=["train", "test"])
print(f"train: {train.num_values} states")
print(f"test: {test.num_values} states")
train: 80 states
test: 20 states
Interleave
The interleave operation creates a state that alternates between parent states’ states in a round-robin fashion.
[ ]:
from statetracker import interleave
with Manager():
A = State(num_values=3, name="A")
B = State(num_values=3, name="B")
C = interleave([A, B]) # 6 states
for state in C:
print(f"C={state}, A={A.value}, B={B.value}")
C=0, A=0, B=None
C=1, A=None, B=0
C=2, A=1, B=None
C=3, A=None, B=1
C=4, A=2, B=None
C=5, A=None, B=2
The states alternate: A’s state 0, B’s state 0, A’s state 1, B’s state 1, etc.
Warning: All states passed to
interleave()must have the samenum_states.
SyncedTo
The synced_to operation creates a state that mirrors its parent exactly. This is useful for creating an alias or checkpoint in the state DAG.
[ ]:
from statetracker import synced_to
with Manager():
A = State(num_values=5, name="A")
B = synced_to(A, name="B") # B mirrors A exactly
for state in B:
print(f"B={state}, A={A.value}")
B=0, A=0
B=1, A=1
B=2, A=2
B=3, A=3
B=4, A=4
Combining Operations
Operations can be freely combined to create complex iteration patterns:
[ ]:
with Manager():
# Create base states for different conditions
control = State(num_values=10, name="control")
treatment = State(num_values=10, name="treatment")
# Stack them (20 states total)
all_samples = stack([control, treatment])
# Shuffle for randomization
randomized = shuffle(all_samples, seed=42)
# Split into train/test
train, test = split(randomized, [0.8, 0.2])
print(f"Train set: {train.num_values} samples")
print(f"Test set: {test.num_values} samples")
Train set: 16 samples
Test set: 4 samples