Skip to main content

ticit/sampler/
pinning.rs

1//! Pinned measurement parities: fixing what the *noiseless* circuit measures.
2//!
3//! # Why this is a branch-level problem
4//!
5//! A caller wants to say "the XOR of these measurement records is 1 in the
6//! noiseless circuit" — the logical outcome that selects a compiled branch of
7//! an adaptive program. A record's value is its intrinsic measurement branch
8//! XOR a frame that, in the noiseless circuit, contains only exogenous noise
9//! symbols (all zero). So the constraint expands into a linear equation over
10//! the *branch* symbols, and that is where it must be enforced.
11//!
12//! Enforcing the recorded parity directly would be wrong for anything but the
13//! reference sample: it would suppress exactly the noise-induced flips a
14//! decoder is meant to correct, and the pinned parity would come out
15//! error-free by construction.
16//!
17//! # Solving
18//!
19//! Every constraint becomes one row over branch symbols. Rows are reduced
20//! against each other by Gaussian elimination over GF(2), each row taking as
21//! its pivot the branch drawn *last* — so when the pivot instruction runs,
22//! every other term in its row already holds a value and the pin is a plain
23//! XOR of assigned symbols. A row that reduces to no branches at all is
24//! deterministic: it either already holds, or the constraint is impossible and
25//! compilation fails.
26
27use std::collections::{BTreeMap, HashMap, HashSet};
28
29use crate::errors::{Result, TicitError};
30use crate::factored::{FactoredInstruction, FactoredInstructionProgram};
31use crate::symbolic::{SymbolicBool, SymbolicBoolEvaluationPlan, symbolic_bool, xor_bool};
32
33/// A parity the noiseless circuit must produce over a set of measurement
34/// records.
35///
36/// Used through [`SamplerOptions::pin_measurements`], where the records are the
37/// zero-based indices a sampling call returns.
38///
39/// [`SamplerOptions::pin_measurements`]: crate::SamplerOptions::pin_measurements
40#[derive(Clone, Debug, Default, PartialEq, Eq)]
41pub struct MeasurementParity {
42    /// Zero-based measurement records whose XOR is constrained. A single index
43    /// pins one record; repeated indices cancel, as XOR implies.
44    pub records: Vec<usize>,
45    /// The value that XOR must take in the noiseless circuit.
46    pub value: bool,
47}
48
49impl MeasurementParity {
50    /// A constraint on the XOR of `records`.
51    #[must_use]
52    pub fn new(records: impl Into<Vec<usize>>, value: bool) -> Self {
53        Self {
54            records: records.into(),
55            value,
56        }
57    }
58}
59
60/// One pinned measurement branch.
61///
62/// At `instruction`, the branch symbol takes the value of `plan` — an XOR over
63/// branch symbols assigned by earlier instructions — instead of being drawn.
64#[derive(Clone, Debug, Default)]
65pub(crate) struct ForcedBranch {
66    pub instruction: usize,
67    pub plan: SymbolicBoolEvaluationPlan,
68}
69
70/// How a symbol gets its value during execution. Symbols that appear here are
71/// exactly those an instruction assigns; every other symbol is exogenous noise
72/// and is therefore zero in the noiseless circuit.
73#[derive(Clone, Copy, Debug)]
74enum SymbolSource {
75    /// A measurement branch drawn at this instruction — a free variable.
76    Branch,
77    /// A record condition assigned from this instruction's outcome expression.
78    Derived(usize),
79}
80
81/// Instruction-level index of what assigns each symbol and each record.
82struct ProgramSymbols {
83    /// Only measurement branches and record conditions land here; the table is
84    /// sized by the measurement count, not by the (far larger) symbol count.
85    sources: HashMap<i32, SymbolSource>,
86    /// Instruction index that draws each branch symbol, used to order pivots.
87    branch_instruction: HashMap<i32, usize>,
88    /// Instruction index writing each one-based record.
89    record_instruction: HashMap<i32, usize>,
90    /// Noiseless expansion of a symbol into branch symbols, memoized.
91    expansions: HashMap<i32, SymbolicBool>,
92}
93
94impl ProgramSymbols {
95    fn new(program: &FactoredInstructionProgram) -> Self {
96        let mut sources = HashMap::new();
97        let mut branch_instruction = HashMap::new();
98        let mut record_instruction = HashMap::new();
99        for (index, instruction) in program.instructions.iter().enumerate() {
100            let branch = match instruction {
101                FactoredInstruction::MeasurePrecomputedActivePauli(inst) => Some(inst.branch),
102                FactoredInstruction::IntroduceDormantMeasurementBranch(inst) => Some(inst.branch),
103                _ => None,
104            };
105            // An expectation probe rides the measurement opcodes but samples
106            // nothing, so its branch symbol is never assigned.
107            let probes = instruction.exp_val().is_some();
108            if let Some(branch) = branch
109                && !probes
110            {
111                sources.insert(branch, SymbolSource::Branch);
112                branch_instruction.insert(branch, index);
113            }
114            if let Some(condition) = instruction.record_condition()
115                && !probes
116            {
117                sources
118                    .entry(condition)
119                    .or_insert(SymbolSource::Derived(index));
120            }
121            if let Some(record) = instruction.record()
122                && !probes
123            {
124                record_instruction.insert(record, index);
125            }
126        }
127        Self {
128            sources,
129            branch_instruction,
130            record_instruction,
131            expansions: HashMap::new(),
132        }
133    }
134
135    /// The record's noiseless value as an XOR over branch symbols.
136    fn record_expansion(
137        &mut self,
138        program: &FactoredInstructionProgram,
139        record: usize,
140    ) -> Result<SymbolicBool> {
141        let one_based = i32::try_from(record + 1)
142            .map_err(|_| TicitError::new("pinned measurement record index is out of range"))?;
143        let Some(&instruction) = self.record_instruction.get(&one_based) else {
144            return Err(TicitError::new(format!(
145                "pinned measurement record {record} is not written by this circuit"
146            )));
147        };
148        let outcome = program.instructions[instruction]
149            .outcome()
150            .ok_or_else(|| TicitError::new("measurement instruction has no outcome expression"))?
151            .clone();
152        self.expand(program, &outcome)
153    }
154
155    /// Expands `expr` with every exogenous symbol held at zero.
156    ///
157    /// Iterative rather than recursive: a record condition can depend on an
158    /// arbitrarily long chain of earlier ones, and the chain length is set by
159    /// the circuit, not by anything this code controls.
160    fn expand(
161        &mut self,
162        program: &FactoredInstructionProgram,
163        expr: &SymbolicBool,
164    ) -> Result<SymbolicBool> {
165        for &condition in &expr.conditions {
166            self.expand_symbol(program, condition)?;
167        }
168        let mut out = SymbolicBool::from(expr.constant);
169        for &condition in &expr.conditions {
170            let expansion = self
171                .expansions
172                .get(&condition)
173                .expect("every condition was just expanded");
174            out = xor_bool(&out, expansion);
175        }
176        Ok(out)
177    }
178
179    fn expand_symbol(&mut self, program: &FactoredInstructionProgram, symbol: i32) -> Result<()> {
180        let mut stack = vec![symbol];
181        let mut in_progress: HashSet<i32> = HashSet::new();
182        while let Some(&top) = stack.last() {
183            if self.expansions.contains_key(&top) {
184                in_progress.remove(&top);
185                stack.pop();
186                continue;
187            }
188            match self.sources.get(&top).copied() {
189                // Exogenous noise is zero in the noiseless circuit.
190                None => {
191                    self.expansions.insert(top, SymbolicBool::from(false));
192                }
193                Some(SymbolSource::Branch) => {
194                    self.expansions.insert(top, symbolic_bool(top));
195                }
196                Some(SymbolSource::Derived(instruction)) => {
197                    in_progress.insert(top);
198                    let outcome = program.instructions[instruction].outcome().ok_or_else(|| {
199                        TicitError::new("record condition has no outcome expression")
200                    })?;
201                    let mut pending = 0usize;
202                    for &condition in &outcome.conditions {
203                        if self.expansions.contains_key(&condition) {
204                            continue;
205                        }
206                        if in_progress.contains(&condition) {
207                            return Err(TicitError::new("measurement record expression is cyclic"));
208                        }
209                        stack.push(condition);
210                        pending += 1;
211                    }
212                    if pending > 0 {
213                        continue;
214                    }
215                    let mut value = SymbolicBool::from(outcome.constant);
216                    for &condition in &outcome.conditions {
217                        let expansion = self
218                            .expansions
219                            .get(&condition)
220                            .expect("all conditions resolved above");
221                        value = xor_bool(&value, expansion);
222                    }
223                    self.expansions.insert(top, value);
224                }
225            }
226            in_progress.remove(&top);
227            stack.pop();
228        }
229        Ok(())
230    }
231
232    /// The row term drawn last, which is the only one safe to pin.
233    fn last_drawn(&self, conditions: &[i32]) -> Option<(usize, i32)> {
234        conditions
235            .iter()
236            .filter_map(|&symbol| {
237                self.branch_instruction
238                    .get(&symbol)
239                    .map(|&instruction| (instruction, symbol))
240            })
241            .max()
242    }
243}
244
245/// Compiles measurement-parity constraints into the branch pins that satisfy
246/// them.
247///
248/// # Errors
249///
250/// Returns an error if a record is not written by the circuit, or if a
251/// constraint's parity is already determined to the opposite value — the
252/// caller asked for something the circuit cannot produce.
253pub(crate) fn plan_pinned_measurements(
254    program: &FactoredInstructionProgram,
255    constraints: &[MeasurementParity],
256) -> Result<Vec<ForcedBranch>> {
257    if constraints.is_empty() {
258        return Ok(Vec::new());
259    }
260    let mut symbols = ProgramSymbols::new(program);
261    // Keyed by the pivot's instruction index, which is also the elimination
262    // order: reducing a row always removes its highest-index term.
263    let mut pivot_rows: BTreeMap<usize, (i32, SymbolicBool)> = BTreeMap::new();
264
265    for constraint in constraints {
266        // The row is `value XOR (noiseless parity)`, so a satisfied constraint
267        // is the row evaluating to zero.
268        let mut row = SymbolicBool::from(constraint.value);
269        for &record in &constraint.records {
270            let expansion = symbols.record_expansion(program, record)?;
271            row = xor_bool(&row, &expansion);
272        }
273        loop {
274            let Some((instruction, pivot)) = symbols.last_drawn(&row.conditions) else {
275                if row.constant {
276                    return Err(TicitError::new(format!(
277                        "pinned measurement parity over {:?} is deterministic and cannot be {}",
278                        constraint.records,
279                        u8::from(constraint.value),
280                    )));
281                }
282                break;
283            };
284            match pivot_rows.get(&instruction) {
285                Some((_, existing)) => row = xor_bool(&row, existing),
286                None => {
287                    pivot_rows.insert(instruction, (pivot, row));
288                    break;
289                }
290            }
291        }
292    }
293
294    let mut forced = Vec::with_capacity(pivot_rows.len());
295    for (instruction, (pivot, row)) in pivot_rows {
296        // `row == 0` means `pivot == everything else in the row`.
297        let assignment = xor_bool(&row, &symbolic_bool(pivot));
298        forced.push(ForcedBranch {
299            instruction,
300            plan: SymbolicBoolEvaluationPlan::new(&assignment),
301        });
302    }
303    Ok(forced)
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::circuit::{parse_ticit_text, plan_ticit_factored_program};
310
311    fn planned(text: &str) -> FactoredInstructionProgram {
312        let parsed = parse_ticit_text(text).expect("test circuit parses");
313        plan_ticit_factored_program(&parsed).expect("test circuit plans")
314    }
315
316    #[test]
317    fn a_free_coin_is_pinned_at_its_own_instruction() {
318        let program = planned("H 0\nM 0\n");
319        let forced = plan_pinned_measurements(&program, &[MeasurementParity::new([0], true)])
320            .expect("a fair coin can be pinned");
321        assert_eq!(forced.len(), 1);
322        assert!(forced[0].plan.conditions.is_empty());
323        assert!(forced[0].plan.constant);
324    }
325
326    #[test]
327    fn a_deterministic_record_rejects_the_wrong_value() {
328        let program = planned("M 0\n");
329        let error = plan_pinned_measurements(&program, &[MeasurementParity::new([0], true)])
330            .expect_err("|0> always measures 0");
331        assert!(error.to_string().contains("deterministic"));
332    }
333
334    #[test]
335    fn a_deterministic_record_accepts_the_right_value() {
336        let program = planned("M 0\n");
337        let forced = plan_pinned_measurements(&program, &[MeasurementParity::new([0], false)])
338            .expect("|0> always measures 0");
339        assert!(forced.is_empty());
340    }
341
342    #[test]
343    fn a_parity_pins_only_its_last_free_coin() {
344        let program = planned("H 0\nH 1\nM 0\nM 1\n");
345        let forced = plan_pinned_measurements(&program, &[MeasurementParity::new([0, 1], true)])
346            .expect("two fair coins can meet a parity");
347        assert_eq!(forced.len(), 1, "only the last draw is pinned");
348        assert_eq!(forced[0].plan.conditions.len(), 1, "it follows the first");
349    }
350
351    #[test]
352    fn independent_constraints_take_distinct_pivots() {
353        let program = planned("H 0\nH 1\nM 0\nM 1\n");
354        let forced = plan_pinned_measurements(
355            &program,
356            &[
357                MeasurementParity::new([0], true),
358                MeasurementParity::new([0, 1], false),
359            ],
360        )
361        .expect("independent constraints solve");
362        assert_eq!(forced.len(), 2);
363        assert_ne!(forced[0].instruction, forced[1].instruction);
364    }
365
366    #[test]
367    fn contradictory_constraints_are_rejected() {
368        let program = planned("H 0\nM 0\nM 0\n");
369        let error = plan_pinned_measurements(
370            &program,
371            &[
372                MeasurementParity::new([0], true),
373                MeasurementParity::new([1], false),
374            ],
375        )
376        .expect_err("the second measurement repeats the first");
377        assert!(error.to_string().contains("deterministic"));
378    }
379
380    #[test]
381    fn an_unwritten_record_is_rejected() {
382        let program = planned("H 0\nM 0\n");
383        let error = plan_pinned_measurements(&program, &[MeasurementParity::new([7], true)])
384            .expect_err("record 7 does not exist");
385        assert!(error.to_string().contains("not written"));
386    }
387}