ket/ir/block.rs
1// SPDX-FileCopyrightText: 2026 Evandro Chagas Ribeiro da Rosa <evandro@quantuloop.com>
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Basic block: a straight-line sequence of quantum gate instructions.
6//!
7//! A [`BasicBlock`] is the primary container for gate sequences in the Libket
8//! IR. It maintains an index of qubit read/write dependencies that is
9//! used by the qubit-mapping pass, and it applies lightweight algebraic
10//! optimizations (gate merging and cancellation) whenever a new instruction is
11//! appended.
12//!
13//! ## Gate simplification
14//!
15//! When a new gate is appended, the block scans backwards through the existing
16//! instruction list for the first instruction that shares any qubit:
17//! - If the two instructions have the same target and controls **and** are
18//! inverses of each other, both are cancelled (removed).
19//! - If they have the same target and controls **and** are on the same rotation
20//! axis, their parameters are added and emitted as a single merged gate.
21//! - If neither simplification applies, the gate is appended unconditionally.
22
23use super::gate::GateInstruction;
24use crate::{
25 error::KetError,
26 ir::gate::{DecomposedGate, GatePropriety, Param, QuantumGate},
27};
28use itertools::Itertools;
29use serde::Serialize;
30use std::collections::{BTreeMap, BTreeSet};
31
32/// Per-qubit dependency record maintained by a [`BasicBlock`].
33///
34/// One entry exists in [`BasicBlock::qubits_op`] for each qubit that appears
35/// as a gate target. The record accumulates the *most general* gate propriety
36/// seen for that qubit (via [`GatePropriety::restrict`]) and the set of
37/// control qubits that have influenced it.
38#[derive(Debug, Clone, Default, Serialize)]
39pub struct QubitOp {
40 /// The most general [`GatePropriety`] of any gate that has targeted this qubit.
41 pub propriety: GatePropriety,
42 /// The set of all control qubit indices for gates targeting this qubit.
43 pub read_qubits: BTreeSet<usize>,
44}
45
46/// A straight-line sequence of quantum gate instructions.
47///
48/// A [`BasicBlock`] is created empty and gates are appended one at a time via
49/// [`BasicBlock::append_gate`] or in bulk via [`BasicBlock::append_block`].
50/// Each append operation attempts lightweight algebraic simplifications:
51/// adjacent inverse gate pairs are cancelled and consecutive same-axis
52/// rotation gates are merged into one.
53///
54/// The block also maintains a dependency index (`qubits_op`) consumed by the
55/// qubit-allocation and circuit-mapping passes.
56#[derive(Debug, Clone, Default)]
57pub struct BasicBlock {
58 /// The ordered list of gate instructions in this block.
59 pub gates: Vec<GateInstruction>,
60 /// Per-qubit dependency records (propriety and read-control sets).
61 pub qubits_op: BTreeMap<usize, QubitOp>,
62 /// Accumulated global phase (in radians), or `None` if no global phase
63 /// has been added to this block.
64 pub global: Option<f64>,
65}
66
67impl BasicBlock {
68 /// Creates a new, empty [`BasicBlock`].
69 #[must_use]
70 pub fn new() -> Self {
71 Self::default()
72 }
73
74 /// Appends a gate instruction to the internal list and updates the
75 /// dependency maps and propriety counters.
76 fn push_gate(&mut self, inst: GateInstruction, check_propiety: bool) {
77 if check_propiety {
78 self.qubits_op
79 .entry(inst.target)
80 .or_default()
81 .propriety
82 .restrict(inst.gate.propriety());
83
84 for c in &inst.control {
85 self.qubits_op
86 .entry(inst.target)
87 .or_default()
88 .read_qubits
89 .insert(*c);
90 }
91 }
92
93 self.gates.push(inst);
94 }
95
96 /// Removes the gate at `index` and updates the dependency maps and
97 /// propriety counters accordingly.
98 fn remove_gate(&mut self, index: usize) {
99 self.gates.remove(index);
100 }
101
102 /// Replaces the gate at `index` with `merged_gate` and updates the
103 /// propriety counters to reflect the change.
104 fn merge_gate(&mut self, index: usize, merged_gate: QuantumGate) {
105 let inst = &self.gates[index];
106 self.qubits_op
107 .entry(inst.target)
108 .or_default()
109 .propriety
110 .restrict(inst.gate.propriety());
111 self.gates[index].gate = merged_gate;
112 }
113
114 /// Attempts to append `new_inst` to the block with algebraic simplification.
115 ///
116 /// The method scans backwards through the existing instructions looking for
117 /// the first instruction that shares any qubit with `new_inst`. If that
118 /// instruction has the same target and controls:
119 /// - and is the inverse of `new_inst`: both are cancelled (removed);
120 /// - and is the same rotation axis: the two are merged into one.
121 ///
122 /// `epsilon` controls the threshold below which a rotation angle is
123 /// considered zero (defaults to `1e-10`).
124 fn append_instruction(
125 &mut self,
126 new_inst: GateInstruction,
127 epsilon: Option<f64>,
128 check_propiety: bool,
129 ) {
130 let epsilon = epsilon.unwrap_or(1e-10);
131
132 if new_inst.gate.is_near_zero(epsilon) {
133 return;
134 }
135
136 for (index, inst) in self.gates.iter().enumerate().rev() {
137 let shares_qubits = inst.target == new_inst.target
138 || inst.control.contains(&new_inst.target)
139 || new_inst.control.contains(&inst.target)
140 || inst.control.iter().any(|c| new_inst.control.contains(c));
141
142 if shares_qubits {
143 let same_target = inst.target == new_inst.target;
144 let same_controls = inst.control == new_inst.control;
145
146 if same_target && same_controls {
147 if inst.gate.inverse() == new_inst.gate {
148 return self.remove_gate(index);
149 } else if let Some(merged_gate) = inst.gate.merge(&new_inst.gate) {
150 if merged_gate.is_near_zero(epsilon) {
151 return self.remove_gate(index);
152 }
153 return self.merge_gate(index, merged_gate);
154 }
155 }
156
157 if !inst.commutes_with(&new_inst) {
158 break;
159 }
160 }
161 }
162
163 self.push_gate(new_inst, check_propiety);
164 }
165
166 /// Appends an uncontrolled gate to the block.
167 pub fn append_gate(&mut self, gate: QuantumGate, target: usize) {
168 self.append_instruction(GateInstruction::new(gate, target), None, true);
169 }
170
171 /// Return a new basic block with the inverse of the circuit.
172 #[must_use]
173 pub fn inverse(&self) -> Self {
174 Self {
175 gates: self
176 .gates
177 .iter()
178 .rev()
179 .map(GateInstruction::inverse)
180 .collect_vec(),
181 global: self.global.map(|g| -g),
182 qubits_op: self.qubits_op.clone(),
183 }
184 }
185
186 /// Returns a copy of this block with `control_qubits` added to every gate.
187 ///
188 /// # Errors
189 /// Propagates any [`KetError`] returned by [`GateInstruction::control`].
190 pub fn control(&self, control_qubits: &[usize]) -> Result<Self, KetError> {
191 let gates: Result<Vec<_>, _> = self
192 .gates
193 .iter()
194 .map(|gate| gate.control(control_qubits))
195 .collect();
196 let gates = gates?;
197
198 let qubits_op = self
199 .qubits_op
200 .iter()
201 .map(|(target, op)| {
202 let mut op = op.to_owned();
203 for c in control_qubits {
204 op.read_qubits.insert(*c);
205 }
206 (*target, op)
207 })
208 .collect();
209
210 let mut result = Self {
211 gates,
212 global: None,
213 qubits_op,
214 };
215
216 if let Some(phase) = &self.global {
217 result.append_instruction(
218 GateInstruction {
219 gate: QuantumGate::Phase(Param::Value(*phase)),
220 target: control_qubits[0],
221 control: control_qubits
222 .iter()
223 .skip(1)
224 .copied()
225 .collect::<BTreeSet<_>>(),
226 control_locked: false,
227 is_approximated: false,
228 decomposed: None,
229 },
230 None,
231 true,
232 );
233 }
234
235 Ok(result)
236 }
237
238 /// Enable approximate decomposition enabled on all gate instructions.
239 pub fn enable_approximated_decomposition(&mut self) {
240 self.gates
241 .iter_mut()
242 .for_each(GateInstruction::enable_approximated_decomposition);
243 }
244
245 /// Lock control sets locked on all gates.
246 pub fn lock_control(&mut self) {
247 self.gates
248 .iter_mut()
249 .for_each(GateInstruction::lock_control);
250 }
251
252 /// Appends all instructions from `block` into `self`, applying algebraic
253 /// simplifications using the given `epsilon` tolerance.
254 ///
255 /// After draining `block.gates`, the global phases are summed and the
256 /// `qubits_op` dependency maps are merged (restricting propriety to the
257 /// more-general of the two).
258 pub fn append_block(&mut self, mut block: Self, epsilon: Option<f64>) {
259 for inst in block.gates {
260 self.append_instruction(inst, epsilon, false);
261 }
262
263 self.global = match (self.global, block.global) {
264 (None, None) => None,
265 (None, Some(g)) | (Some(g), None) => Some(g),
266 (Some(g1), Some(g2)) => Some(g1 + g2),
267 };
268
269 for (&qubit, op) in &mut block.qubits_op {
270 let self_op = self.qubits_op.entry(qubit).or_default();
271 self_op.propriety.restrict(op.propriety);
272 self_op.read_qubits.append(&mut op.read_qubits);
273 }
274 }
275
276 /// Sum a global phase in the block.
277 pub fn add_global_phase(&mut self, phase: f64) {
278 self.global = Some(self.global.unwrap_or_default() + phase);
279 }
280
281 /// Returns the highest qubit index referenced by any gate in this block,
282 /// or `None` if the block is empty.
283 #[must_use]
284 pub fn max_qubit_index(&self) -> Option<usize> {
285 let max_read = self.qubits_op.values().flat_map(|op| &op.read_qubits).max();
286
287 let max_written = self.qubits_op.keys().max();
288
289 match (max_read, max_written) {
290 (None, None) => None,
291 (None, Some(&w)) => Some(w),
292 (Some(&r), None) => Some(r),
293 (Some(&r), Some(&w)) => Some(r.max(w)),
294 }
295 }
296
297 /// Flattens decomposed multi-qubit gates into a single sequence of
298 /// [`DecomposedGate`].
299 ///
300 /// When `parameters` is provided, any symbolic [`Param::Ref`] values are
301 /// resolved to their concrete floating-point equivalents before output.
302 #[must_use]
303 pub fn flat_gates(
304 &self,
305 parameters: Option<&[f64]>,
306 shift: Option<(usize, usize, f64)>, // (param_index, instance_index, shift_amount)
307 ) -> Vec<DecomposedGate> {
308 let mut instance_counts = std::collections::HashMap::new();
309
310 self.gates
311 .iter()
312 .flat_map(|gate| {
313 if let Some(gates) = &gate.decomposed {
314 gates
315 .iter()
316 .map(|g| match g {
317 DecomposedGate::U(qg, target) => DecomposedGate::U(*qg, *target),
318 DecomposedGate::CNOT(c, t) => DecomposedGate::CNOT(*c, *t),
319 })
320 .collect_vec()
321 } else {
322 assert!(gate.control.is_empty());
323 let qg = &gate.gate;
324 let mut final_gate = if let Some(parameters) = parameters {
325 qg.set_parameter(parameters)
326 } else {
327 *qg
328 };
329 if let Some((shift_param, shift_inst, shift_amt)) = shift {
330 let mut p_idx = None;
331 match qg {
332 QuantumGate::RotationX(Param::Ref { index, .. })
333 | QuantumGate::RotationY(Param::Ref { index, .. })
334 | QuantumGate::RotationZ(Param::Ref { index, .. })
335 | QuantumGate::Phase(Param::Ref { index, .. }) => {
336 p_idx = Some(*index);
337 }
338 _ => {}
339 }
340 if let Some(idx) = p_idx {
341 if idx == shift_param {
342 let count = instance_counts.entry(idx).or_insert(0);
343 if *count == shift_inst {
344 final_gate = match final_gate {
345 QuantumGate::RotationX(Param::Value(v)) => {
346 QuantumGate::RotationX(Param::Value(v + shift_amt))
347 }
348 QuantumGate::RotationY(Param::Value(v)) => {
349 QuantumGate::RotationY(Param::Value(v + shift_amt))
350 }
351 QuantumGate::RotationZ(Param::Value(v)) => {
352 QuantumGate::RotationZ(Param::Value(v + shift_amt))
353 }
354 QuantumGate::Phase(Param::Value(v)) => {
355 QuantumGate::Phase(Param::Value(v + shift_amt))
356 }
357 _ => final_gate,
358 };
359 }
360 *count += 1;
361 }
362 }
363 }
364 vec![(final_gate, gate.target).into()]
365 }
366 })
367 .collect_vec()
368 }
369
370 /// Forces the propriety classification of every qubit entry to at most
371 /// [`GatePropriety::Diagonal`], even if the actual gate sequence is more
372 /// general. Used when an externally-verified analysis guarantees the
373 /// circuit is diagonal.
374 pub fn set_as_diagonal(&mut self) {
375 self.qubits_op.values_mut().for_each(|op| {
376 op.propriety.broaden(GatePropriety::Diagonal);
377 });
378 }
379
380 /// Forces the propriety classification of every qubit entry to at most
381 /// [`GatePropriety::Permutation`], even if the actual gate sequence is
382 /// more general. Used when an externally-verified analysis guarantees the
383 /// circuit only permutes basis states.
384 pub fn set_as_permutation(&mut self) {
385 self.qubits_op.values_mut().for_each(|op| {
386 op.propriety.broaden(GatePropriety::Permutation);
387 });
388 }
389}