1use 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#[derive(Clone, Debug, Default, PartialEq, Eq)]
41pub struct MeasurementParity {
42 pub records: Vec<usize>,
45 pub value: bool,
47}
48
49impl MeasurementParity {
50 #[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#[derive(Clone, Debug, Default)]
65pub(crate) struct ForcedBranch {
66 pub instruction: usize,
67 pub plan: SymbolicBoolEvaluationPlan,
68}
69
70#[derive(Clone, Copy, Debug)]
74enum SymbolSource {
75 Branch,
77 Derived(usize),
79}
80
81struct ProgramSymbols {
83 sources: HashMap<i32, SymbolSource>,
86 branch_instruction: HashMap<i32, usize>,
88 record_instruction: HashMap<i32, usize>,
90 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 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 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 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 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 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
245pub(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 let mut pivot_rows: BTreeMap<usize, (i32, SymbolicBool)> = BTreeMap::new();
264
265 for constraint in constraints {
266 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 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}