Skip to main content

cqlib_core/compile/transform/layout/
analysis.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 2025-2026
4//
5// This code is licensed under the Apache License, Version 2.0. You may
6// obtain a copy of this license in the LICENSE.txt file in the root directory
7// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8//
9// Any modifications or derivative works of this code must retain this
10// copyright notice, and modified files need to carry a notice indicating
11// that they have been altered from the originals.
12
13//! Circuit interaction analysis used by layout methods.
14//!
15//! The analyzer extracts the information that initial-layout algorithms need:
16//! logical qubits in circuit order and a deterministic weighted graph of
17//! two-qubit interactions. Single-qubit operations, classical data operations,
18//! directives, and delays do not constrain layout and are ignored.
19//!
20//! Structured classical-control operations are scanned recursively. This is a
21//! static compiler analysis: a gate in a branch or loop body contributes once
22//! to the layout model, regardless of whether that path is taken at runtime.
23
24use crate::circuit::{Circuit, ClassicalControlOp, Instruction, Operation, Qubit, StandardGate};
25use crate::compile::CompilerError;
26use crate::device::LogicalQubit;
27use std::collections::BTreeMap;
28
29/// Layout-relevant summary of a circuit.
30///
31/// The analysis intentionally contains no physical-device information. It can
32/// be reused across multiple target devices or layout algorithms.
33#[derive(Debug, Clone, PartialEq)]
34pub struct CircuitLayoutAnalysis {
35    /// Logical qubits in the same order as the source circuit.
36    pub logical_qubits: Vec<LogicalQubit>,
37    /// Weighted two-qubit interaction graph extracted from operations.
38    pub interactions: InteractionGraph,
39}
40
41/// One weighted logical interaction between two logical qubits.
42///
43/// Endpoints are stored in sorted order so interaction lookup and algorithm
44/// tie-breaks remain deterministic. The two directed weights preserve the
45/// observed operation direction for later coupling-direction scoring.
46#[derive(Debug, Clone, PartialEq)]
47pub struct Interaction {
48    /// Lower-sorted logical qubit endpoint.
49    pub left: LogicalQubit,
50    /// Higher-sorted logical qubit endpoint.
51    pub right: LogicalQubit,
52    /// Total interaction weight accumulated for this unordered pair.
53    pub weight: f64,
54    /// Weight observed in operation order `left -> right`.
55    pub directed_weight_left_to_right: f64,
56    /// Weight observed in operation order `right -> left`.
57    pub directed_weight_right_to_left: f64,
58    /// First operation order at which this unordered interaction was seen.
59    ///
60    /// Algorithms use this as a stable tie-breaker after interaction weight.
61    pub first_seen_order: usize,
62}
63
64/// Weighted logical interaction graph.
65///
66/// The graph stores one edge per unordered logical-qubit pair. Edges are kept
67/// sorted by endpoint, which makes downstream algorithms reproducible without
68/// relying on hash-map iteration order.
69#[derive(Debug, Clone, Default, PartialEq)]
70pub struct InteractionGraph {
71    interactions: Vec<Interaction>,
72    gate_contributions: Vec<Vec<GateInteraction>>,
73    temporal_weights: Vec<[f64; 3]>,
74}
75
76/// Gate-specific weights for one unordered logical-qubit interaction.
77///
78/// This remains compiler-internal so the public [`Interaction`] summary keeps
79/// its existing shape and direction-weight contract.
80#[derive(Debug, Clone, Copy, PartialEq)]
81pub(super) struct GateInteraction {
82    pub(super) gate: StandardGate,
83    pub(super) left_to_right_weight: f64,
84    pub(super) right_to_left_weight: f64,
85}
86
87impl InteractionGraph {
88    /// Creates an empty interaction graph.
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    /// Returns all interactions in deterministic endpoint order.
94    pub fn interactions(&self) -> &[Interaction] {
95        &self.interactions
96    }
97
98    /// Returns gate-specific contributions for the interaction at `index`.
99    pub(super) fn gate_contributions(&self, index: usize) -> &[GateInteraction] {
100        debug_assert_eq!(self.interactions.len(), self.gate_contributions.len());
101        self.gate_contributions
102            .get(index)
103            .expect("interaction and gate-contribution slots must remain aligned")
104            .as_slice()
105    }
106
107    pub(super) fn temporal_weights(&self, index: usize) -> [f64; 3] {
108        debug_assert_eq!(self.interactions.len(), self.temporal_weights.len());
109        self.temporal_weights
110            .get(index)
111            .copied()
112            .unwrap_or([0.0; 3])
113    }
114
115    #[cfg(test)]
116    pub(super) fn contribution_slot_count(&self) -> usize {
117        self.gate_contributions.len()
118    }
119
120    /// Returns whether no two-qubit interactions were observed.
121    pub fn is_empty(&self) -> bool {
122        self.interactions.is_empty()
123    }
124
125    /// Returns the number of logical interaction edges.
126    pub fn len(&self) -> usize {
127        self.interactions.len()
128    }
129
130    /// Returns per-logical-qubit activity weight from incident interactions.
131    ///
132    /// Activity is used to place busier logical qubits earlier, and to scale
133    /// readout-error scoring when calibration data is available.
134    pub fn logical_activity(&self) -> BTreeMap<LogicalQubit, f64> {
135        let mut activity = BTreeMap::new();
136        for interaction in &self.interactions {
137            *activity.entry(interaction.left).or_insert(0.0) += interaction.weight;
138            *activity.entry(interaction.right).or_insert(0.0) += interaction.weight;
139        }
140        activity
141    }
142
143    /// Adds one observed two-qubit operation to the deterministic graph.
144    ///
145    /// Endpoints are canonicalized into sorted order while preserving the
146    /// operation direction in the directed weight fields.
147    fn add_operation_interaction(
148        &mut self,
149        first: LogicalQubit,
150        second: LogicalQubit,
151        gate: Option<StandardGate>,
152        weight: f64,
153        order: usize,
154        temporal_bucket: usize,
155    ) {
156        let (left, right, left_to_right) = if first <= second {
157            (first, second, true)
158        } else {
159            (second, first, false)
160        };
161
162        match self
163            .interactions
164            .binary_search_by_key(&(left, right), |i| (i.left, i.right))
165        {
166            Ok(index) => {
167                let interaction = &mut self.interactions[index];
168                interaction.weight += weight;
169                if left_to_right {
170                    interaction.directed_weight_left_to_right += weight;
171                } else {
172                    interaction.directed_weight_right_to_left += weight;
173                }
174                interaction.first_seen_order = interaction.first_seen_order.min(order);
175                if let Some(gate) = gate {
176                    add_gate_contribution(
177                        &mut self.gate_contributions[index],
178                        gate,
179                        left_to_right,
180                        weight,
181                    );
182                }
183                self.temporal_weights[index][temporal_bucket] += weight;
184            }
185            Err(index) => {
186                self.interactions.insert(
187                    index,
188                    Interaction {
189                        left,
190                        right,
191                        weight,
192                        directed_weight_left_to_right: if left_to_right { weight } else { 0.0 },
193                        directed_weight_right_to_left: if left_to_right { 0.0 } else { weight },
194                        first_seen_order: order,
195                    },
196                );
197                let mut contributions = Vec::new();
198                if let Some(gate) = gate {
199                    add_gate_contribution(&mut contributions, gate, left_to_right, weight);
200                }
201                self.gate_contributions.insert(index, contributions);
202                let mut temporal = [0.0; 3];
203                temporal[temporal_bucket] = weight;
204                self.temporal_weights.insert(index, temporal);
205            }
206        }
207        debug_assert_eq!(self.interactions.len(), self.gate_contributions.len());
208        debug_assert_eq!(self.interactions.len(), self.temporal_weights.len());
209    }
210}
211
212fn add_gate_contribution(
213    contributions: &mut Vec<GateInteraction>,
214    gate: StandardGate,
215    left_to_right: bool,
216    weight: f64,
217) {
218    if let Some(contribution) = contributions.iter_mut().find(|item| item.gate == gate) {
219        if left_to_right {
220            contribution.left_to_right_weight += weight;
221        } else {
222            contribution.right_to_left_weight += weight;
223        }
224        return;
225    }
226
227    contributions.push(GateInteraction {
228        gate,
229        left_to_right_weight: if left_to_right { weight } else { 0.0 },
230        right_to_left_weight: if left_to_right { 0.0 } else { weight },
231    });
232}
233
234/// Extracts layout-relevant interaction data from a circuit.
235///
236/// Scans the full operation tree. Structured classical-control bodies are
237/// traversed recursively so mixed quantum/classical circuits still expose the
238/// two-qubit interactions that drive initial-layout quality.
239///
240/// # Errors
241///
242/// Returns [`CompilerError::InvalidInput`] when a unitary operation acts on more
243/// than two qubits. Such operations must be decomposed before layout so the
244/// interaction graph represents hardware-supported two-qubit constraints.
245pub fn analyze_circuit_for_layout(
246    circuit: &Circuit,
247) -> Result<CircuitLayoutAnalysis, CompilerError> {
248    let operation_count = count_layout_order_operations(circuit.operations());
249    let mut analyzer = InteractionAnalyzer::new(circuit.qubits(), operation_count);
250    analyzer.scan_operations(circuit.operations())?;
251    Ok(analyzer.finish())
252}
253
254fn count_layout_order_operations(operations: &[Operation]) -> usize {
255    operations
256        .iter()
257        .map(|operation| match &operation.instruction {
258            Instruction::ClassicalControl(control) => control
259                .bodies()
260                .map(|body| count_layout_order_operations(body.operations()))
261                .sum(),
262            Instruction::ClassicalData(_) | Instruction::Directive(_) | Instruction::Delay => 0,
263            _ => 1,
264        })
265        .sum()
266}
267
268struct InteractionAnalyzer {
269    logical_qubits: Vec<LogicalQubit>,
270    interactions: InteractionGraph,
271    /// Monotonic order for quantum operations that affect layout tie-breaks.
272    operation_order: usize,
273    total_operation_count: usize,
274}
275
276impl InteractionAnalyzer {
277    /// Creates an analyzer seeded with the circuit's logical qubit order.
278    fn new(qubits: Vec<Qubit>, total_operation_count: usize) -> Self {
279        Self {
280            logical_qubits: qubits.into_iter().map(LogicalQubit::from_qubit).collect(),
281            interactions: InteractionGraph::new(),
282            operation_order: 0,
283            total_operation_count,
284        }
285    }
286
287    /// Scans one operation and records layout-relevant quantum interactions.
288    ///
289    /// Structured classical-control operations delegate to
290    /// [`InteractionAnalyzer::scan_control_flow`]. Non-control operations with
291    /// arity greater than two are rejected because layout requires explicit
292    /// two-qubit decomposition.
293    fn scan_operation(&mut self, operation: &Operation) -> Result<(), CompilerError> {
294        match &operation.instruction {
295            Instruction::ClassicalControl(control) => self.scan_control_flow(control),
296            Instruction::ClassicalData(_) | Instruction::Directive(_) | Instruction::Delay => {
297                Ok(())
298            }
299            _ => {
300                // Layout only needs two-qubit constraints. Larger unitary
301                // operations must be lowered first so their hardware
302                // interaction structure is explicit.
303                match operation.qubits.len() {
304                    0 | 1 => {}
305                    2 => {
306                        let first = LogicalQubit::from_qubit(operation.qubits[0]);
307                        let second = LogicalQubit::from_qubit(operation.qubits[1]);
308                        self.interactions.add_operation_interaction(
309                            first,
310                            second,
311                            match &operation.instruction {
312                                Instruction::Standard(gate) if gate.num_qubits() == 2 => {
313                                    Some(*gate)
314                                }
315                                _ => None,
316                            },
317                            1.0,
318                            self.operation_order,
319                            (self.operation_order.saturating_mul(3)
320                                / self.total_operation_count.max(1))
321                            .min(2),
322                        );
323                    }
324                    arity => {
325                        return Err(CompilerError::InvalidInput(format!(
326                            "layout requires unitary operations with more than two qubits to be decomposed before layout; found {}-qubit operation {}",
327                            arity, operation.instruction
328                        )));
329                    }
330                }
331                self.operation_order += 1;
332                Ok(())
333            }
334        }
335    }
336
337    /// Scans a sequence of operations in source order.
338    fn scan_operations(&mut self, operations: &[Operation]) -> Result<(), CompilerError> {
339        for operation in operations {
340            self.scan_operation(operation)?;
341        }
342        Ok(())
343    }
344
345    /// Recursively scans every body of one classical-control operation.
346    ///
347    /// This is structural analysis: each branch, loop body, or switch case is
348    /// included once, without runtime probability or loop-count weighting.
349    fn scan_control_flow(&mut self, control: &ClassicalControlOp) -> Result<(), CompilerError> {
350        // Structural scan: visit each body once without execution weighting.
351        for body in control.bodies() {
352            self.scan_operations(body.operations())?;
353        }
354        Ok(())
355    }
356
357    /// Consumes the analyzer and returns the immutable layout analysis.
358    fn finish(self) -> CircuitLayoutAnalysis {
359        CircuitLayoutAnalysis {
360            logical_qubits: self.logical_qubits,
361            interactions: self.interactions,
362        }
363    }
364}