cqlib_core/compile/transform/layout/
analysis.rs1use crate::circuit::{Circuit, ClassicalControlOp, Instruction, Operation, Qubit, StandardGate};
25use crate::compile::CompilerError;
26use crate::device::LogicalQubit;
27use std::collections::BTreeMap;
28
29#[derive(Debug, Clone, PartialEq)]
34pub struct CircuitLayoutAnalysis {
35 pub logical_qubits: Vec<LogicalQubit>,
37 pub interactions: InteractionGraph,
39}
40
41#[derive(Debug, Clone, PartialEq)]
47pub struct Interaction {
48 pub left: LogicalQubit,
50 pub right: LogicalQubit,
52 pub weight: f64,
54 pub directed_weight_left_to_right: f64,
56 pub directed_weight_right_to_left: f64,
58 pub first_seen_order: usize,
62}
63
64#[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#[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 pub fn new() -> Self {
90 Self::default()
91 }
92
93 pub fn interactions(&self) -> &[Interaction] {
95 &self.interactions
96 }
97
98 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 pub fn is_empty(&self) -> bool {
122 self.interactions.is_empty()
123 }
124
125 pub fn len(&self) -> usize {
127 self.interactions.len()
128 }
129
130 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 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
234pub 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 operation_order: usize,
273 total_operation_count: usize,
274}
275
276impl InteractionAnalyzer {
277 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 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 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 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 fn scan_control_flow(&mut self, control: &ClassicalControlOp) -> Result<(), CompilerError> {
350 for body in control.bodies() {
352 self.scan_operations(body.operations())?;
353 }
354 Ok(())
355 }
356
357 fn finish(self) -> CircuitLayoutAnalysis {
359 CircuitLayoutAnalysis {
360 logical_qubits: self.logical_qubits,
361 interactions: self.interactions,
362 }
363 }
364}