Skip to main content

prism_q/sim/
noise_builder.rs

1//! Rule-based construction of a [`NoiseModel`] against a circuit.
2//!
3//! Rules are declared once and compiled against an instruction stream by
4//! [`NoiseBuilder::build`], which emits the same per-instruction event vector
5//! the manual `after_gate` path builds. Nothing here runs per shot.
6
7use smallvec::smallvec;
8
9use crate::circuit::{Circuit, Instruction, SmallVec};
10use crate::error::{PrismError, Result};
11use crate::gates::Gate;
12use crate::sim::noise::{NoiseChannel, NoiseEvent, NoiseModel, ReadoutError};
13
14/// Which gates a rule fires after.
15///
16/// An unset field imposes no restriction, so [`GateFilter::all`] matches every
17/// gate on every qubit.
18#[derive(Debug, Clone, Default)]
19pub struct GateFilter {
20    arity: Option<usize>,
21    name: Option<String>,
22    qubits: Option<Vec<usize>>,
23    targets: Option<Vec<usize>>,
24}
25
26impl GateFilter {
27    pub fn all() -> Self {
28        Self::default()
29    }
30
31    /// Restrict to gates with exactly `arity` targets.
32    pub fn arity(mut self, arity: usize) -> Self {
33        self.arity = Some(arity);
34        self
35    }
36
37    /// Restrict to gates whose [`Gate::name`] equals `name`, such as `"cx"`.
38    ///
39    /// A name no gate in the circuit carries is not an error: the rule emits
40    /// nothing. Fusion renames gates (`"fused"`, `"fused_2q"`, `"multi_fused"`),
41    /// so build the model against the circuit the caller holds rather than a
42    /// fused stream.
43    pub fn named(mut self, name: impl Into<String>) -> Self {
44        self.name = Some(name.into());
45        self
46    }
47
48    /// Restrict to the listed qubits, as an unordered set. Every rule kind
49    /// honours it: one acting per target emits events only on the targets in
50    /// the set, and one acting on a gate's whole target list, or on the
51    /// spectators of a target, fires only for targets the set admits.
52    pub fn on_qubits(mut self, qubits: impl IntoIterator<Item = usize>) -> Self {
53        let mut listed: Vec<usize> = qubits.into_iter().collect();
54        listed.sort_unstable();
55        listed.dedup();
56        self.qubits = Some(listed);
57        self
58    }
59
60    /// Restrict to gates whose target list equals `targets` in order.
61    ///
62    /// Directed, unlike [`GateFilter::on_qubits`]: `on_targets([0, 1])` matches
63    /// `cx(0, 1)` and not `cx(1, 0)`, which is what a calibration table keyed by
64    /// coupling-map edge needs.
65    pub fn on_targets(mut self, targets: impl IntoIterator<Item = usize>) -> Self {
66        self.targets = Some(targets.into_iter().collect());
67        self
68    }
69
70    fn matches(&self, gate: &Gate, targets: &[usize]) -> bool {
71        if self.arity.is_some_and(|arity| arity != targets.len()) {
72            return false;
73        }
74        if self.name.as_deref().is_some_and(|name| name != gate.name()) {
75            return false;
76        }
77        if self
78            .targets
79            .as_deref()
80            .is_some_and(|listed| listed != targets)
81        {
82            return false;
83        }
84        true
85    }
86
87    fn allows(&self, qubit: usize) -> bool {
88        self.qubits
89            .as_ref()
90            .is_none_or(|listed| listed.binary_search(&qubit).is_ok())
91    }
92}
93
94enum Rule {
95    PerTarget {
96        filter: GateFilter,
97        channel: NoiseChannel,
98    },
99    Joint {
100        filter: GateFilter,
101        channel: NoiseChannel,
102    },
103    Crosstalk {
104        filter: GateFilter,
105        coupling: Vec<(usize, usize)>,
106        channel: NoiseChannel,
107    },
108    OverRotation {
109        filter: GateFilter,
110        relative: f64,
111    },
112    Idle {
113        channel: NoiseChannel,
114    },
115    AfterReset {
116        channel: NoiseChannel,
117    },
118    BeforeMeasure {
119        channel: NoiseChannel,
120    },
121}
122
123/// Declarative noise-model construction.
124///
125/// Each method appends a rule; [`NoiseBuilder::build`] walks a circuit once and
126/// evaluates the rules in registration order at every instruction, so the event
127/// order inside a slot is the order the rules were declared. Rules match
128/// [`Instruction::Gate`] only: a [`Instruction::Conditional`] carries noise that
129/// would fire whether or not its gate ran, so it is left alone.
130///
131/// # Examples
132///
133/// ```
134/// use prism_q::{Circuit, Gate, GateFilter, NoiseBuilder, NoiseChannel};
135///
136/// let mut circuit = Circuit::new(3, 3);
137/// circuit.add_gate(Gate::H, &[0]);
138/// circuit.add_gate(Gate::Cx, &[0, 1]);
139///
140/// let noise = NoiseBuilder::new()
141///     .after_gates(GateFilter::all().arity(1), NoiseChannel::Depolarizing { p: 1e-4 })
142///     .after_gates(GateFilter::all().named("cx"), NoiseChannel::Depolarizing { p: 2e-3 })
143///     .on_idle_qubits(NoiseChannel::PhaseDamping { gamma: 1e-5 })
144///     .readout_error(2, 0.02, 0.03)
145///     .build(&circuit)?;
146///
147/// assert_eq!(noise.after_gate.len(), circuit.instructions.len());
148/// # Ok::<(), prism_q::PrismError>(())
149/// ```
150#[derive(Default)]
151pub struct NoiseBuilder {
152    rules: Vec<Rule>,
153    readout: Vec<(usize, ReadoutError)>,
154    uniform_readout: Option<ReadoutError>,
155}
156
157impl NoiseBuilder {
158    pub fn new() -> Self {
159        Self::default()
160    }
161
162    /// One single-qubit channel on each matching target of each matching gate.
163    pub fn after_gates(mut self, filter: GateFilter, channel: NoiseChannel) -> Self {
164        self.rules.push(Rule::PerTarget { filter, channel });
165        self
166    }
167
168    /// One channel on a matching gate's whole target list, for the correlated
169    /// two-qubit channels ([`NoiseChannel::TwoQubitDepolarizing`],
170    /// [`NoiseChannel::Kraus2q`]). The rule fires only on gates whose target
171    /// count equals the channel's own arity.
172    pub fn after_gates_joint(mut self, filter: GateFilter, channel: NoiseChannel) -> Self {
173        self.rules.push(Rule::Joint { filter, channel });
174        self
175    }
176
177    /// One channel per spectator qubit coupled to a target of a matching gate.
178    ///
179    /// `coupling` is an undirected edge list. A single-qubit channel lands once
180    /// on each distinct spectator; a two-qubit channel lands on each
181    /// `(target, spectator)` pair with the target as its first qubit. Qubits
182    /// among the gate's own targets are not spectators of it, and a target the
183    /// filter's qubit set excludes contributes no spectators.
184    pub fn crosstalk(
185        mut self,
186        filter: GateFilter,
187        coupling: impl IntoIterator<Item = (usize, usize)>,
188        channel: NoiseChannel,
189    ) -> Self {
190        self.rules.push(Rule::Crosstalk {
191            filter,
192            coupling: coupling.into_iter().collect(),
193            channel,
194        });
195        self
196    }
197
198    /// A coherent rotation of `relative * theta` about a matching rotation
199    /// gate's own axis, appended after it: the gate turns `theta` into
200    /// `theta * (1 + relative)`.
201    ///
202    /// Fires on `rx`, `ry`, `rz`, and `p` only. The excess is emitted as a
203    /// one-qubit Kraus set, so the multi-qubit rotations (`rzz`, `pauli_rot`)
204    /// do not fit even though they carry a single angle. Other gates matching
205    /// `filter` are skipped.
206    pub fn over_rotation(mut self, filter: GateFilter, relative: f64) -> Self {
207        self.rules.push(Rule::OverRotation { filter, relative });
208        self
209    }
210
211    /// One single-qubit channel on every qubit no instruction of a layer
212    /// touches, fired at the end of the layer.
213    ///
214    /// Layers come from the greedy assignment [`Circuit::depth`] reports, so
215    /// the layer count a caller can print is the one the idle budget is charged
216    /// against. A layer's events ride its highest-indexed instruction. Greedy
217    /// assignment can place a later instruction in an earlier layer, and on
218    /// such a circuit a layer's idle events fire after the events of the layer
219    /// that follows it.
220    ///
221    /// The event count grows as layers times idle qubits, which on a wide
222    /// shallow circuit is most of the register on every layer. Every engine
223    /// walks that stream per shot.
224    pub fn on_idle_qubits(mut self, channel: NoiseChannel) -> Self {
225        self.rules.push(Rule::Idle { channel });
226        self
227    }
228
229    /// One single-qubit channel on the reset qubit, after each reset.
230    pub fn after_resets(mut self, channel: NoiseChannel) -> Self {
231        self.rules.push(Rule::AfterReset { channel });
232        self
233    }
234
235    /// One single-qubit channel on the measured qubit, immediately before each
236    /// measurement.
237    ///
238    /// Distinct from [`NoiseModel::readout`], which flips reported bits once at
239    /// the end of a shot: this damages the state the measurement then projects,
240    /// so a mid-circuit outcome feeding a classical conditional is the faulty
241    /// one. A purely classical fault that leaves the state intact is not
242    /// expressible this way.
243    ///
244    /// The channel is carried by the slot of the preceding instruction, so a
245    /// measurement at instruction 0 has nowhere to put it and
246    /// [`NoiseBuilder::build`] rejects the circuit; prepend a barrier to make
247    /// room. Sharing that slot with the preceding gate's own error means
248    /// declaration order decides which fires first, so declare this rule after
249    /// the gate rules.
250    pub fn before_measurements(mut self, channel: NoiseChannel) -> Self {
251        self.rules.push(Rule::BeforeMeasure { channel });
252        self
253    }
254
255    /// Readout error on one classical bit. Overrides any uniform rate whatever
256    /// order the two are declared in.
257    pub fn readout_error(mut self, bit: usize, p01: f64, p10: f64) -> Self {
258        self.readout.push((bit, ReadoutError { p01, p10 }));
259        self
260    }
261
262    /// Readout error on every classical bit of the register the model is built
263    /// for, including bits no measurement writes.
264    pub fn uniform_readout_error(mut self, p01: f64, p10: f64) -> Self {
265        self.uniform_readout = Some(ReadoutError { p01, p10 });
266        self
267    }
268
269    /// Compile the rules against `circuit`.
270    ///
271    /// # Errors
272    ///
273    /// Reports a per-bit readout rate outside the classical register, a
274    /// measurement at instruction 0 under a
275    /// [`before_measurements`](NoiseBuilder::before_measurements) rule, and
276    /// everything [`NoiseModel::validate_for`] rejects.
277    pub fn build(&self, circuit: &Circuit) -> Result<NoiseModel> {
278        let mut after_gate: Vec<Vec<NoiseEvent>> = vec![Vec::new(); circuit.instructions.len()];
279
280        let idle = self
281            .rules
282            .iter()
283            .any(|rule| matches!(rule, Rule::Idle { .. }))
284            .then(|| idle_qubits_by_layer(circuit));
285        let pre_measure = self
286            .rules
287            .iter()
288            .any(|rule| matches!(rule, Rule::BeforeMeasure { .. }))
289            .then(|| pre_measure_qubits(circuit))
290            .transpose()?;
291
292        for (idx, instr) in circuit.instructions.iter().enumerate() {
293            for rule in &self.rules {
294                emit(rule, idx, instr, &idle, &pre_measure, &mut after_gate);
295            }
296        }
297
298        let mut readout = vec![self.uniform_readout.clone(); circuit.num_classical_bits];
299        for (bit, error) in &self.readout {
300            if *bit >= readout.len() {
301                return Err(PrismError::InvalidParameter {
302                    message: format!(
303                        "readout error on classical bit {bit} is outside the {}-bit register",
304                        readout.len()
305                    ),
306                });
307            }
308            readout[*bit] = Some(error.clone());
309        }
310
311        let model = NoiseModel {
312            after_gate,
313            readout,
314        };
315        model.validate_for(circuit)?;
316        Ok(model)
317    }
318}
319
320fn emit(
321    rule: &Rule,
322    idx: usize,
323    instr: &Instruction,
324    idle: &Option<Vec<Vec<usize>>>,
325    pre_measure: &Option<Vec<Option<usize>>>,
326    after_gate: &mut [Vec<NoiseEvent>],
327) {
328    let slot = &mut after_gate[idx];
329    match rule {
330        Rule::PerTarget { filter, channel } => {
331            let Instruction::Gate { gate, targets } = instr else {
332                return;
333            };
334            if !filter.matches(gate, targets) {
335                return;
336            }
337            for &qubit in targets.iter().filter(|&&q| filter.allows(q)) {
338                slot.push(NoiseEvent {
339                    channel: channel.clone(),
340                    qubits: smallvec![qubit],
341                });
342            }
343        }
344        Rule::Joint { filter, channel } => {
345            let Instruction::Gate { gate, targets } = instr else {
346                return;
347            };
348            if targets.len() != channel.num_qubits()
349                || !filter.matches(gate, targets)
350                || !targets.iter().all(|&q| filter.allows(q))
351            {
352                return;
353            }
354            slot.push(NoiseEvent {
355                channel: channel.clone(),
356                qubits: targets.iter().copied().collect(),
357            });
358        }
359        Rule::Crosstalk {
360            filter,
361            coupling,
362            channel,
363        } => {
364            let Instruction::Gate { gate, targets } = instr else {
365                return;
366            };
367            if !filter.matches(gate, targets) {
368                return;
369            }
370            emit_crosstalk(filter, coupling, channel, targets, slot);
371        }
372        Rule::OverRotation { filter, relative } => {
373            let Instruction::Gate { gate, targets } = instr else {
374                return;
375            };
376            if !filter.matches(gate, targets) {
377                return;
378            }
379            let Some(channel) = over_rotation_channel(gate, *relative) else {
380                return;
381            };
382            for &qubit in targets.iter().filter(|&&q| filter.allows(q)) {
383                slot.push(NoiseEvent {
384                    channel: channel.clone(),
385                    qubits: smallvec![qubit],
386                });
387            }
388        }
389        Rule::Idle { channel } => {
390            let Some(idle) = idle else { return };
391            for &qubit in &idle[idx] {
392                slot.push(NoiseEvent {
393                    channel: channel.clone(),
394                    qubits: smallvec![qubit],
395                });
396            }
397        }
398        Rule::AfterReset { channel } => {
399            if let Instruction::Reset { qubit } = instr {
400                slot.push(NoiseEvent {
401                    channel: channel.clone(),
402                    qubits: smallvec![*qubit],
403                });
404            }
405        }
406        Rule::BeforeMeasure { channel } => {
407            let Some(pre_measure) = pre_measure else {
408                return;
409            };
410            if let Some(qubit) = pre_measure[idx] {
411                slot.push(NoiseEvent {
412                    channel: channel.clone(),
413                    qubits: smallvec![qubit],
414                });
415            }
416        }
417    }
418}
419
420fn emit_crosstalk(
421    filter: &GateFilter,
422    coupling: &[(usize, usize)],
423    channel: &NoiseChannel,
424    targets: &[usize],
425    slot: &mut Vec<NoiseEvent>,
426) {
427    let mut seen: Vec<usize> = Vec::new();
428    for &target in targets.iter().filter(|&&q| filter.allows(q)) {
429        let mut spectators: Vec<usize> = coupling
430            .iter()
431            .filter_map(|&(a, b)| match (a == target, b == target) {
432                (true, false) => Some(b),
433                (false, true) => Some(a),
434                _ => None,
435            })
436            .filter(|spectator| !targets.contains(spectator))
437            .collect();
438        spectators.sort_unstable();
439        spectators.dedup();
440
441        for spectator in spectators {
442            let qubits: SmallVec<[usize; 2]> = if channel.num_qubits() == 2 {
443                smallvec![target, spectator]
444            } else {
445                if seen.contains(&spectator) {
446                    continue;
447                }
448                seen.push(spectator);
449                smallvec![spectator]
450            };
451            slot.push(NoiseEvent {
452                channel: channel.clone(),
453                qubits,
454            });
455        }
456    }
457}
458
459/// The unitary an over-rotation of `relative` appends after `gate`, as a
460/// one-operator Kraus set. `None` for a gate carrying no single rotation angle.
461fn over_rotation_channel(gate: &Gate, relative: f64) -> Option<NoiseChannel> {
462    let excess = match gate {
463        Gate::Rx(theta) => Gate::Rx(relative * theta),
464        Gate::Ry(theta) => Gate::Ry(relative * theta),
465        Gate::Rz(theta) => Gate::Rz(relative * theta),
466        Gate::P(theta) => Gate::P(relative * theta),
467        _ => return None,
468    };
469    Some(NoiseChannel::Custom {
470        kraus: vec![excess.matrix_2x2()],
471    })
472}
473
474/// Qubits an instruction occupies. Empty for a barrier, which schedules
475/// rather than acts; [`idle_qubits_by_layer`] handles that case itself.
476fn instruction_qubits(instr: &Instruction) -> SmallVec<[usize; 4]> {
477    match instr {
478        Instruction::Gate { targets, .. } | Instruction::Conditional { targets, .. } => {
479            targets.clone()
480        }
481        Instruction::Measure { qubit, .. } | Instruction::Reset { qubit } => smallvec![*qubit],
482        Instruction::Barrier { qubits } => qubits.clone(),
483        Instruction::Region(region) => region.qubits().iter().copied().collect(),
484    }
485}
486
487/// For the highest-indexed instruction of each layer, the qubits that layer
488/// left idle; empty everywhere else.
489///
490/// Layer assignment is the greedy rule `Circuit::depth` reports: an instruction
491/// takes the earliest layer where every qubit it touches is free, and a barrier
492/// synchronizes its qubits to that layer without occupying it.
493fn idle_qubits_by_layer(circuit: &Circuit) -> Vec<Vec<usize>> {
494    let num_qubits = circuit.num_qubits;
495    let mut qubit_depth = vec![0usize; num_qubits];
496    let mut layers: Vec<(Vec<bool>, usize)> = Vec::new();
497
498    for (idx, instr) in circuit.instructions.iter().enumerate() {
499        let qubits = instruction_qubits(instr);
500        if qubits.is_empty() {
501            continue;
502        }
503        let layer = qubits.iter().map(|&q| qubit_depth[q]).max().unwrap_or(0);
504        if matches!(instr, Instruction::Barrier { .. }) {
505            for &qubit in &qubits {
506                qubit_depth[qubit] = layer;
507            }
508            continue;
509        }
510        while layers.len() <= layer {
511            layers.push((vec![false; num_qubits], 0));
512        }
513        let (touched, last) = &mut layers[layer];
514        for &qubit in &qubits {
515            touched[qubit] = true;
516            qubit_depth[qubit] = layer + 1;
517        }
518        *last = (*last).max(idx);
519    }
520
521    let mut idle = vec![Vec::new(); circuit.instructions.len()];
522    for (touched, last) in layers {
523        idle[last] = touched
524            .iter()
525            .enumerate()
526            .filter_map(|(qubit, &used)| (!used).then_some(qubit))
527            .collect();
528    }
529    idle
530}
531
532/// The qubit measured by instruction `idx + 1`, indexed by `idx`, so a
533/// pre-measurement channel rides the preceding instruction's slot.
534fn pre_measure_qubits(circuit: &Circuit) -> Result<Vec<Option<usize>>> {
535    if let Some(Instruction::Measure { .. }) = circuit.instructions.first() {
536        return Err(PrismError::InvalidParameter {
537            message: "pre-measurement noise needs a preceding instruction to attach to, and \
538                      instruction 0 is a measurement; prepend a barrier"
539                .into(),
540        });
541    }
542    let mut out = vec![None; circuit.instructions.len()];
543    for (idx, instr) in circuit.instructions.iter().enumerate().skip(1) {
544        if let Instruction::Measure { qubit, .. } = instr {
545            out[idx - 1] = Some(*qubit);
546        }
547    }
548    Ok(out)
549}