Manage Simulation Complexity

As a simulation progresses, the number of terms in an Observable can grow exponentially, making the simulation slow or memory-intensive. pyrauli provides powerful tools (Truncator and SchedulingPolicy objects) to manage this complexity automatically during a run() call.

Remove small coefficients

Use a CoefficientTruncator.

This truncator removes any Pauli terms whose coefficient magnitude is below a given threshold.

obs = Observable([PauliTerm("I", 0.99), PauliTerm("Y", 0.01)])
truncator = CoefficientTruncator(0.1)

removed_count = obs.truncate(truncator) # 1

Available truncators

  • NeverTruncator: never truncate observable.

  • CoefficientTruncator: truncate terms with absolute coefficient value below set threshold.

  • WeightTruncator: truncate terms with Pauli weight above set threshold.

  • LambdaTruncator: truncate terms if they match a predicate (Python function or lambda).

  • KeepNTruncator: truncate least significant terms if their number exceed a threshold. (Keep at most N different terms)

  • MultiTruncator: Combine a list of truncators into one.

Using a custom Truncator with your own logic

Use a LambdaTruncator.

This allows you to provide a custom Python function that filters the terms.

obs = Observable([PauliTerm("IXI", 0.5), PauliTerm("IZY", 0.5)])
truncator = LambdaTruncator(lambda pt: 'Y' in repr(pt))

removed_count = obs.truncate(truncator) # 1 (removed IZY)

Note

This truncator removes PauliTerm containing \(Y\). However, this is not very efficient.

Controlling when truncation is applied

Use a SchedulingPolicy.

Scheduling Policies give you fine-grained control over when a Truncator is applied during the simulation. It can query a SimulationState object and other information to make a decision.

The following example uses a LambdaPolicy to apply a truncator only at specific depths.

# 1. Define the custom policy as a Python function
def merge_after_second_split(state: SimulationState, op_type: OperationType, timing: Timing) -> bool:
    """This policy returns True only for a Merge event after 2 splitting gates have been applied."""
    
    # We only care about the 'After' timing for merges
    if timing != Timing.After:
        return False
        
    # We only care about SplittingGate operations
    if op_type != OperationType.SplittingGate:
        return False
        
    # The core logic: check the number of splitting gates already applied.
    if state.nb_splitting_gates_applied == 2:
        return True
        
    return False

# 2. Create the LambdaPolicy from the Python function
custom_merge_policy = LambdaPolicy(merge_after_second_split)

# 3. Create a circuit that uses this policy
qc = Circuit(
    nb_qubits=1,
    truncator=NeverTruncator(),
    merge_policy=custom_merge_policy
)

# 4. Build a circuit with two splitting gates (Rz)
# Each Rz gate will double the number of terms in the observable.
qc.add_operation("Rz", qubit=0, param=0.5)  # 1st splitting gate
qc.add_operation("Rz", qubit=0, param=0.5)  # 2nd splitting gate

# 5. Run the simulation
# The 'X' observable will split into 2 terms at the first Rz, and then 4 terms at the second.
# The merge policy should trigger *only* after the second Rz, merging the 4 terms back down.
obs_in = Observable("X")
obs_out = qc.run(obs_in)

Note

See SimulationState, OperationType, Timing and LambdaPolicy for more information.