Skip to main content

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}