The DEM builder and the simulator disagree about idle noise for the same experiment, and the workflow that would reconcile them is not expressible.
The divergence
DetectorErrorModel.builder().with_idle_after_2q(d) strips runtime-emitted idles first. The default is deliberate — python/quantum-pecos/src/pecos/qec/dem.py:311:
# Inserting a uniform idle convention on top of runtime-emitted idles
# would double-count idle noise, so insertion implies stripping first.
strip_traced_idles = idle_after_2q_duration is not None
general_noise().with_idle_after_2q(d) has no such protection, because a noise model decorates a gate stream and cannot remove gates from it. Both of these fire:
crates/pecos-engines/src/noise/general.rs:575 — applies idle faults to every real GateType::Idle in the stream.
crates/pecos-engines/src/noise/general.rs:1278 — separately inserts idle faults after every two-qubit gate.
So if the Selene runtime plugin emitted its own Idle gates during lowering, the simulator applies idle noise twice where the DEM applies it once. Configuring the same experiment for both surfaces silently gives different physics.
The workflow that would fix it, and where it stops
Three of four hops already work:
from pecos.tracing import trace_program_to_tick_circuit
tc = trace_program_to_tick_circuit(prog, 2) # Guppy -> TickCircuit via the QIS trace
tc.remove_identity() # strip runtime-emitted idles
tc.insert_idle_after_two_qubit_gates(1.0) # apply the uniform convention once
The fourth does not:
sim(tc)
TypeError: program must be a Qasm, Qis, Hugr, or PhirJson instance
What exists already
exp/pecos-neo can do this in Rust today — impl SimNeoInput for pecos_quantum::TickCircuit at exp/pecos-neo/src/tool/simulation.rs:633, and &TickCircuit at :640. DagCircuit and CommandQueue are likewise accepted. But sim_neo is not exported to Python from pecos, so the capability is unreachable from where users write this.
Proposal
Let sim() accept a TickCircuit (and plausibly DagCircuit), likely via a TickCircuitEngine alongside the existing Qasm/Qis/Hugr/PhirJson inputs. The circuit is what every other input lowers to, so this adds an entry point rather than a concept.
This is preferable to giving the engines noise model a strip-then-insert flag: circuit transformation is a circuit-layer concern, and having a noise model mutate the gate stream is the design that was rejected for exp/pecos-neo in PR #420 (see pecos-docs/design/neo-noise-event-emission.md).
Acceptance
sim(tick_circuit) runs, with the same noise-model and backend configuration as other inputs.
- A test showing that stripping before insertion produces different noise from not stripping — i.e. the double-count is real and the pipeline avoids it.
- A test showing the DEM and the simulator agree for the same experiment when both strip once and insert once.
Found while documenting with_runtime in PR #420. Not fixed there: that PR does not touch sim()s accepted inputs.
The DEM builder and the simulator disagree about idle noise for the same experiment, and the workflow that would reconcile them is not expressible.
The divergence
DetectorErrorModel.builder().with_idle_after_2q(d)strips runtime-emitted idles first. The default is deliberate —python/quantum-pecos/src/pecos/qec/dem.py:311:general_noise().with_idle_after_2q(d)has no such protection, because a noise model decorates a gate stream and cannot remove gates from it. Both of these fire:crates/pecos-engines/src/noise/general.rs:575— applies idle faults to every realGateType::Idlein the stream.crates/pecos-engines/src/noise/general.rs:1278— separately inserts idle faults after every two-qubit gate.So if the Selene runtime plugin emitted its own
Idlegates during lowering, the simulator applies idle noise twice where the DEM applies it once. Configuring the same experiment for both surfaces silently gives different physics.The workflow that would fix it, and where it stops
Three of four hops already work:
The fourth does not:
What exists already
exp/pecos-neocan do this in Rust today —impl SimNeoInput for pecos_quantum::TickCircuitatexp/pecos-neo/src/tool/simulation.rs:633, and&TickCircuitat:640.DagCircuitandCommandQueueare likewise accepted. Butsim_neois not exported to Python frompecos, so the capability is unreachable from where users write this.Proposal
Let
sim()accept aTickCircuit(and plausiblyDagCircuit), likely via aTickCircuitEnginealongside the existing Qasm/Qis/Hugr/PhirJson inputs. The circuit is what every other input lowers to, so this adds an entry point rather than a concept.This is preferable to giving the engines noise model a strip-then-insert flag: circuit transformation is a circuit-layer concern, and having a noise model mutate the gate stream is the design that was rejected for
exp/pecos-neoin PR #420 (seepecos-docs/design/neo-noise-event-emission.md).Acceptance
sim(tick_circuit)runs, with the same noise-model and backend configuration as other inputs.Found while documenting
with_runtimein PR #420. Not fixed there: that PR does not touchsim()s accepted inputs.