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.
Quantifying Truncation Error
Using a truncator can significantly speed up simulations, but it introduces
a numerical error by discarding parts of the observable. pyrauli tracks
an estimate of this error, allowing you to balance performance and accuracy.
The expectation_value() method returns a tuple containing
both the expectation value and the accumulated truncation error.
The following example uses a CoefficientTruncator and
inspects the resulting error.
# Use a truncator that will remove terms with small coefficients
truncator = CoefficientTruncator(0.1)
circuit = Circuit(2, truncator=truncator)
circuit.h(0)
circuit.h(1)
circuit.cx(0, 1)
circuit.rz(1, 0.01) # This gate splits the observable
circuit.h(0)
circuit.h(1)
observable = Observable("ZZ")
# The second value returned is the estimated error
ev, err = circuit.expectation_value(observable)
print(circuit.run(observable))
print(f"Expectation value: {ev:.4f}")
print(f"Estimated truncation error: {err:.4f}")
# For comparison, run without truncation
circuit.set_truncator(NeverTruncator())
exact_ev, _ = circuit.expectation_value(observable)
print(f"Exact expectation value: {exact_ev:.4f}")
print(f"Actual error: {abs(exact_ev - ev):.4f}")
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.