Skip to main content

cqlib_core/visualization/circuit/
ir.rs

1// This code is part of Cqlib.
2//
3// (C) Copyright China Telecom Quantum Group 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//! Visualization IR data model and transformations.
14//!
15//! This module defines backend-agnostic intermediate representation (IR) types used by
16//! visualization backends such as text and figure drawers. It also contains small IR
17//! transformations shared by those backends.
18//!
19//! # Design Goals
20//!
21//! - Decouple circuit semantics from rendering implementation.
22//! - Preserve lane/column layout decisions for reuse across backends.
23//! - Carry enough metadata (style/children/span) for control-flow-aware drawing.
24
25use crate::circuit::Qubit;
26use std::collections::BTreeSet;
27
28/// Draw style used by visualization backends.
29///
30/// Each variant determines how one [`VisualOperation`] should be rendered.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum VisualOpStyle {
33    /// Generic gate-like box.
34    Gate,
35    /// Controlled operation where the first `num_controls` operands are controls.
36    Controlled {
37        /// Number of control qubits at the start of the operand list.
38        num_controls: usize,
39    },
40    /// Controlled-Z marker rendered as two dots connected vertically.
41    Cz,
42    /// Swap marker across two qubits.
43    Swap,
44    /// Barrier marker.
45    Barrier,
46    /// Measurement marker.
47    Measure,
48    /// Reset marker.
49    Reset,
50    /// Delay marker.
51    Delay,
52    /// Control-flow marker (if/while).
53    ControlFlow {
54        /// Specific control-flow marker family rendered by backends.
55        kind: VisualControlFlowKind,
56    },
57}
58
59/// Control-flow operation family used by visualization backends.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub enum VisualControlFlowKind {
62    /// Source IfElse block before flattening.
63    IfElseBlock {
64        /// Whether the source block includes a false branch.
65        has_false_branch: bool,
66        /// Display metadata for the branch condition.
67        condition: VisualCondition,
68    },
69    /// Source While-loop block before flattening.
70    WhileBlock {
71        /// Display metadata for the loop condition.
72        condition: VisualCondition,
73    },
74    /// Source For-loop block before flattening.
75    ForBlock {
76        /// Display metadata for the loop range expression.
77        range: VisualCondition,
78    },
79    /// Source Switch block before flattening.
80    SwitchBlock {
81        /// Display metadata for the switch target expression.
82        target: VisualCondition,
83    },
84    /// Structured break marker.
85    Break,
86    /// Structured continue marker.
87    Continue,
88    /// Flattened marker: `If ...`.
89    IfStart,
90    /// Flattened marker: `Else-...`.
91    ElseStart,
92    /// Flattened marker: `While ...`.
93    WhileStart,
94    /// Flattened marker: `For ...`.
95    ForStart,
96    /// Flattened marker: `Switch ...`.
97    SwitchStart,
98    /// Flattened marker: `Case ...`.
99    CaseStart,
100    /// Flattened marker: `Default-...`.
101    DefaultStart,
102    /// Flattened marker: `End-...`.
103    End,
104}
105
106/// Structured condition metadata used by control-flow visualization.
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct VisualCondition {
109    /// Display label for the classical expression controlling the branch.
110    pub label: String,
111}
112
113/// Backend-agnostic operation prepared for rendering.
114///
115/// This is the atomic IR node consumed by drawers.
116#[derive(Debug, Clone)]
117pub struct VisualOperation {
118    /// Time column after layering.
119    pub column: usize,
120    /// Operand lanes (in original operand order).
121    pub lanes: Vec<usize>,
122    /// Lanes that reserve this column to avoid overlap.
123    pub covered_lanes: Vec<usize>,
124    /// Primary display label.
125    pub label: String,
126    /// Parameter labels, already formatted.
127    pub params: Vec<String>,
128    /// Rendering style.
129    pub style: VisualOpStyle,
130    /// If true, render this operation as a span box on multi-qubit lanes.
131    pub span_box: bool,
132    /// Optional child circuits for control-flow operations.
133    pub children: Option<VisualChildren>,
134    /// Number of logical columns reserved by this operation.
135    pub span_cols: usize,
136}
137
138/// Backend-agnostic circuit after layout.
139///
140/// # Examples
141///
142/// ```rust
143/// use cqlib_core::circuit::Qubit;
144/// use cqlib_core::visualization::{VisualCircuit, VisualOpStyle, VisualOperation};
145///
146/// let visual = VisualCircuit {
147///     qubits: vec![Qubit::new(0)],
148///     operations: vec![VisualOperation {
149///         column: 0,
150///         lanes: vec![0],
151///         covered_lanes: vec![0],
152///         label: "H".to_string(),
153///         params: vec![],
154///         style: VisualOpStyle::Gate,
155///         span_box: false,
156///         children: None,
157///         span_cols: 1,
158///     }],
159///     num_columns: 1,
160/// };
161/// assert_eq!(visual.num_qubits(), 1);
162/// ```
163#[derive(Debug, Clone)]
164pub struct VisualCircuit {
165    /// Logical qubits in lane order.
166    pub qubits: Vec<Qubit>,
167    /// Layered operations.
168    pub operations: Vec<VisualOperation>,
169    /// Number of occupied columns.
170    pub num_columns: usize,
171}
172
173impl VisualCircuit {
174    /// Number of qubit lanes.
175    pub fn num_qubits(&self) -> usize {
176        self.qubits.len()
177    }
178}
179
180/// Optional child circuits attached to control-flow operations.
181///
182/// These child circuits keep lane alignment with the parent visualization context.
183#[derive(Debug, Clone)]
184pub enum VisualChildren {
185    /// IfElse branch children.
186    IfElse {
187        /// Child circuit executed when the branch condition is true.
188        then_circuit: Box<VisualCircuit>,
189        /// Optional child circuit executed when the branch condition is false.
190        else_circuit: Option<Box<VisualCircuit>>,
191    },
192    /// While-loop body child.
193    While {
194        /// Child circuit executed inside the loop body.
195        body_circuit: Box<VisualCircuit>,
196    },
197    /// For-loop body child.
198    For {
199        /// Child circuit executed inside the loop body.
200        body_circuit: Box<VisualCircuit>,
201    },
202    /// Switch case/default children.
203    Switch {
204        /// Case label and child circuit pairs in source order.
205        case_circuits: Vec<(String, Box<VisualCircuit>)>,
206        /// Optional child circuit executed for the default case.
207        default_circuit: Option<Box<VisualCircuit>>,
208    },
209}
210
211/// Expand control-flow operations into timeline markers and body operations.
212///
213/// This mirrors text rendering semantics: `If/Else/End` and `While/End` markers
214/// become explicit operations in the resulting visual circuit.
215pub(crate) fn flatten_control_flow_visual(visual: &VisualCircuit) -> VisualCircuit {
216    let mut flat_ops = Vec::new();
217    let mut next_cf_id = 0usize;
218    let root_scope_lanes: Vec<usize> = (0..visual.num_qubits()).collect();
219    flatten_ops_recursive(visual, &mut next_cf_id, &mut flat_ops, &root_scope_lanes);
220
221    let mut next_free = vec![0usize; visual.num_qubits()];
222    let mut num_columns = 0usize;
223    for op in &mut flat_ops {
224        let schedule_lanes = scheduling_covered_lanes(op, visual.num_qubits());
225        let column = schedule_lanes
226            .iter()
227            .filter_map(|lane| next_free.get(*lane).copied())
228            .max()
229            .unwrap_or(0);
230        op.column = column;
231        let span = op.span_cols.max(1);
232        for lane in schedule_lanes {
233            if let Some(slot) = next_free.get_mut(lane) {
234                *slot = column + span;
235            }
236        }
237        num_columns = num_columns.max(column + span);
238    }
239
240    VisualCircuit {
241        qubits: visual.qubits.clone(),
242        operations: flat_ops,
243        num_columns,
244    }
245}
246
247/// Reverse displayed qubit order for a pre-built visual circuit.
248pub(crate) fn reverse_visual_lanes(mut visual: VisualCircuit) -> VisualCircuit {
249    let n = visual.num_qubits();
250    if n == 0 {
251        return visual;
252    }
253    visual.qubits.reverse();
254    for op in &mut visual.operations {
255        for lane in &mut op.lanes {
256            *lane = n - 1 - *lane;
257        }
258        for lane in &mut op.covered_lanes {
259            *lane = n - 1 - *lane;
260        }
261    }
262    visual
263}
264
265fn flatten_ops_recursive(
266    visual: &VisualCircuit,
267    next_cf_id: &mut usize,
268    out: &mut Vec<VisualOperation>,
269    scope_lanes: &[usize],
270) {
271    for op in &visual.operations {
272        match &op.style {
273            VisualOpStyle::ControlFlow {
274                kind: VisualControlFlowKind::IfElseBlock { condition, .. },
275            } => {
276                let cf_id = *next_cf_id;
277                *next_cf_id += 1;
278                let covered = control_flow_marker_lanes(op, visual.num_qubits(), scope_lanes);
279                let if_label = format!("If-{cf_id} {}", condition.label);
280                out.push(make_control_marker(
281                    if_label,
282                    VisualControlFlowKind::IfStart,
283                    covered.clone(),
284                ));
285
286                if let Some(VisualChildren::IfElse {
287                    then_circuit,
288                    else_circuit,
289                }) = op.children.as_ref()
290                {
291                    flatten_ops_recursive(then_circuit, next_cf_id, out, &covered);
292                    if let Some(else_body) = else_circuit {
293                        out.push(make_control_marker(
294                            format!("Else-{cf_id}"),
295                            VisualControlFlowKind::ElseStart,
296                            covered.clone(),
297                        ));
298                        flatten_ops_recursive(else_body, next_cf_id, out, &covered);
299                    }
300                }
301
302                out.push(make_control_marker(
303                    format!("End-{cf_id}"),
304                    VisualControlFlowKind::End,
305                    covered,
306                ));
307            }
308            VisualOpStyle::ControlFlow {
309                kind: VisualControlFlowKind::WhileBlock { condition },
310            } => {
311                let cf_id = *next_cf_id;
312                *next_cf_id += 1;
313                let covered = control_flow_marker_lanes(op, visual.num_qubits(), scope_lanes);
314                let while_label = format!("While-{cf_id} {}", condition.label);
315                out.push(make_control_marker(
316                    while_label,
317                    VisualControlFlowKind::WhileStart,
318                    covered.clone(),
319                ));
320                if let Some(VisualChildren::While { body_circuit }) = op.children.as_ref() {
321                    flatten_ops_recursive(body_circuit, next_cf_id, out, &covered);
322                }
323                out.push(make_control_marker(
324                    format!("End-{cf_id}"),
325                    VisualControlFlowKind::End,
326                    covered,
327                ));
328            }
329            VisualOpStyle::ControlFlow {
330                kind: VisualControlFlowKind::ForBlock { range },
331            } => {
332                let cf_id = *next_cf_id;
333                *next_cf_id += 1;
334                let covered = control_flow_marker_lanes(op, visual.num_qubits(), scope_lanes);
335                out.push(make_control_marker(
336                    format!("For-{cf_id} {}", range.label),
337                    VisualControlFlowKind::ForStart,
338                    covered.clone(),
339                ));
340                if let Some(VisualChildren::For { body_circuit }) = op.children.as_ref() {
341                    flatten_ops_recursive(body_circuit, next_cf_id, out, &covered);
342                }
343                out.push(make_control_marker(
344                    format!("End-{cf_id}"),
345                    VisualControlFlowKind::End,
346                    covered,
347                ));
348            }
349            VisualOpStyle::ControlFlow {
350                kind: VisualControlFlowKind::SwitchBlock { target },
351            } => {
352                let cf_id = *next_cf_id;
353                *next_cf_id += 1;
354                let covered = control_flow_marker_lanes(op, visual.num_qubits(), scope_lanes);
355                out.push(make_control_marker(
356                    format!("Switch-{cf_id} {}", target.label),
357                    VisualControlFlowKind::SwitchStart,
358                    covered.clone(),
359                ));
360                if let Some(VisualChildren::Switch {
361                    case_circuits,
362                    default_circuit,
363                }) = op.children.as_ref()
364                {
365                    for (case_label, case_circuit) in case_circuits {
366                        out.push(make_control_marker(
367                            format!("Case-{cf_id} {case_label}"),
368                            VisualControlFlowKind::CaseStart,
369                            covered.clone(),
370                        ));
371                        flatten_ops_recursive(case_circuit, next_cf_id, out, &covered);
372                    }
373                    if let Some(default_body) = default_circuit {
374                        out.push(make_control_marker(
375                            format!("Default-{cf_id}"),
376                            VisualControlFlowKind::DefaultStart,
377                            covered.clone(),
378                        ));
379                        flatten_ops_recursive(default_body, next_cf_id, out, &covered);
380                    }
381                }
382                out.push(make_control_marker(
383                    format!("End-{cf_id}"),
384                    VisualControlFlowKind::End,
385                    covered,
386                ));
387            }
388            _ => {
389                let mut clone = op.clone();
390                clone.children = None;
391                clone.span_cols = 1;
392                clone.column = 0;
393                if matches!(clone.style, VisualOpStyle::ControlFlow { .. }) {
394                    let covered = control_flow_marker_lanes(op, visual.num_qubits(), scope_lanes);
395                    clone.lanes = covered.clone();
396                    clone.covered_lanes = covered;
397                } else if clone.lanes.is_empty() {
398                    clone.lanes = effective_covered_lanes(op, visual.num_qubits());
399                }
400                out.push(clone);
401            }
402        }
403    }
404}
405
406fn make_control_marker(
407    label: String,
408    kind: VisualControlFlowKind,
409    covered_lanes: Vec<usize>,
410) -> VisualOperation {
411    VisualOperation {
412        column: 0,
413        lanes: covered_lanes.clone(),
414        covered_lanes,
415        label,
416        params: Vec::new(),
417        style: VisualOpStyle::ControlFlow { kind },
418        span_box: false,
419        children: None,
420        span_cols: 1,
421    }
422}
423
424fn effective_covered_lanes(op: &VisualOperation, num_qubits: usize) -> Vec<usize> {
425    if !op.covered_lanes.is_empty() {
426        return op.covered_lanes.clone();
427    }
428    if !op.lanes.is_empty() {
429        return op.lanes.clone();
430    }
431    (0..num_qubits).collect()
432}
433
434fn scheduling_covered_lanes(op: &VisualOperation, num_qubits: usize) -> Vec<usize> {
435    if matches!(op.style, VisualOpStyle::ControlFlow { .. }) {
436        return (0..num_qubits).collect();
437    }
438    effective_covered_lanes(op, num_qubits)
439}
440
441fn control_flow_marker_lanes(
442    op: &VisualOperation,
443    num_qubits: usize,
444    scope_lanes: &[usize],
445) -> Vec<usize> {
446    let lanes = op
447        .children
448        .as_ref()
449        .and_then(collect_children_used_lanes)
450        .or_else(|| non_empty_lanes(&op.lanes))
451        .or_else(|| non_empty_lanes(scope_lanes))
452        .unwrap_or_else(|| effective_covered_lanes(op, num_qubits));
453
454    normalize_lane_span(&lanes)
455}
456
457fn collect_children_used_lanes(children: &VisualChildren) -> Option<Vec<usize>> {
458    let mut lanes = BTreeSet::new();
459    match children {
460        VisualChildren::IfElse {
461            then_circuit,
462            else_circuit,
463        } => {
464            collect_circuit_used_lanes(then_circuit, &mut lanes);
465            if let Some(else_circuit) = else_circuit {
466                collect_circuit_used_lanes(else_circuit, &mut lanes);
467            }
468        }
469        VisualChildren::While { body_circuit } | VisualChildren::For { body_circuit } => {
470            collect_circuit_used_lanes(body_circuit, &mut lanes);
471        }
472        VisualChildren::Switch {
473            case_circuits,
474            default_circuit,
475        } => {
476            for (_, case_circuit) in case_circuits {
477                collect_circuit_used_lanes(case_circuit, &mut lanes);
478            }
479            if let Some(default_circuit) = default_circuit {
480                collect_circuit_used_lanes(default_circuit, &mut lanes);
481            }
482        }
483    }
484
485    lanes_to_vec(lanes)
486}
487
488fn collect_circuit_used_lanes(visual: &VisualCircuit, lanes: &mut BTreeSet<usize>) {
489    for op in &visual.operations {
490        if let Some(children) = op.children.as_ref()
491            && let Some(child_lanes) = collect_children_used_lanes(children)
492        {
493            lanes.extend(child_lanes);
494            continue;
495        }
496
497        if !op.lanes.is_empty() {
498            lanes.extend(op.lanes.iter().copied());
499        } else if matches!(op.style, VisualOpStyle::Barrier) {
500            lanes.extend(effective_covered_lanes(op, visual.num_qubits()));
501        }
502    }
503}
504
505fn non_empty_lanes(lanes: &[usize]) -> Option<Vec<usize>> {
506    if lanes.is_empty() {
507        None
508    } else {
509        Some(lanes.to_vec())
510    }
511}
512
513fn lanes_to_vec(lanes: BTreeSet<usize>) -> Option<Vec<usize>> {
514    if lanes.is_empty() {
515        None
516    } else {
517        Some(lanes.into_iter().collect())
518    }
519}
520
521fn normalize_lane_span(lanes: &[usize]) -> Vec<usize> {
522    let Some(min_lane) = lanes.iter().copied().min() else {
523        return Vec::new();
524    };
525    let max_lane = lanes.iter().copied().max().unwrap_or(min_lane);
526    (min_lane..=max_lane).collect()
527}