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. Use ordered_product() if you need automatic deduplication.

Using ordered_product()

The ordered_product() function automatically:

  • Removes duplicate states

  • Flattens nested products

  • Orders states by iter_order and id

[ ]:
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 same num_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 same num_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