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.