Skip to main content

cqlib_core/visualization/circuit/
layout.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 builder.
14//!
15//! This module converts [`Circuit`] operations into backend-agnostic [`VisualCircuit`] IR.
16//!
17//! The builder is responsible for:
18//! - mapping qubits to lanes,
19//! - formatting parameter labels,
20//! - classifying operation drawing styles,
21//! - scheduling operations into non-overlapping columns.
22
23use crate::circuit::gate::{Directive, Instruction, StandardGate};
24use crate::circuit::{
25    Circuit, ClassicalControlOp, ClassicalDataOp, ClassicalExpr, ClassicalExprKind, Operation,
26    Qubit,
27};
28use crate::visualization::VisualizationError;
29use crate::visualization::circuit::ir::{
30    VisualChildren, VisualCircuit, VisualCondition, VisualControlFlowKind, VisualOpStyle,
31    VisualOperation,
32};
33use crate::visualization::circuit::params::{ParameterFormatOptions, ParameterFormatter};
34use std::collections::HashMap;
35
36/// Build-time options for visualization IR.
37///
38/// These options affect IR construction only and are shared by text and figure backends.
39#[derive(Debug, Clone, Copy)]
40pub struct VisualBuildOptions {
41    /// If true, decompose circuit-gates before layout.
42    pub decompose_circuit_gates: bool,
43    /// If true, reserve the full lane span (`min..=max`) for multi-qubit operations.
44    pub reserve_full_span_for_multi_qubit: bool,
45    /// Parameter label formatting options.
46    pub parameter_format: ParameterFormatOptions,
47}
48
49impl Default for VisualBuildOptions {
50    fn default() -> Self {
51        Self {
52            decompose_circuit_gates: false,
53            reserve_full_span_for_multi_qubit: true,
54            parameter_format: ParameterFormatOptions::default(),
55        }
56    }
57}
58
59/// Convert a circuit into backend-agnostic visualization IR.
60///
61/// # Arguments
62///
63/// * `circuit` - Source circuit to be visualized.
64/// * `options` - Builder options controlling decomposition and lane reservation policy.
65///
66/// # Errors
67///
68/// Returns [`VisualizationError`] when operations reference unknown qubits or invalid parameter indices.
69///
70/// # Examples
71///
72/// ```rust
73/// use cqlib_core::circuit::{Circuit, Qubit};
74/// use cqlib_core::visualization::{VisualBuildOptions, build_visual_circuit};
75///
76/// let mut circuit = Circuit::new(2);
77/// circuit.h(Qubit::new(0)).unwrap();
78/// circuit.cx(Qubit::new(0), Qubit::new(1)).unwrap();
79///
80/// let visual = build_visual_circuit(&circuit, &VisualBuildOptions::default()).unwrap();
81/// assert_eq!(visual.num_qubits(), 2);
82/// assert!(!visual.operations.is_empty());
83/// ```
84pub fn build_visual_circuit(
85    circuit: &Circuit,
86    options: &VisualBuildOptions,
87) -> Result<VisualCircuit, VisualizationError> {
88    let owned;
89    let source = if options.decompose_circuit_gates {
90        owned = circuit.decompose()?;
91        &owned
92    } else {
93        circuit
94    };
95
96    build_visual_circuit_from_ops(source, &source.qubits(), source.operations(), options)
97}
98
99fn build_visual_circuit_from_ops(
100    source: &Circuit,
101    qubits: &[Qubit],
102    ops: &[Operation],
103    options: &VisualBuildOptions,
104) -> Result<VisualCircuit, VisualizationError> {
105    let qubit_to_lane: HashMap<Qubit, usize> = qubits
106        .iter()
107        .enumerate()
108        .map(|(idx, q)| (*q, idx))
109        .collect();
110
111    let mut operations = Vec::with_capacity(ops.len());
112    let mut next_free = vec![0usize; qubits.len()];
113    let mut num_columns = 0usize;
114    let parameter_formatter = ParameterFormatter::new(options.parameter_format);
115
116    for op in ops {
117        let lanes = map_lanes(op, &qubit_to_lane)?;
118        let params = op
119            .params
120            .iter()
121            .map(|param| parameter_formatter.format_circuit_param(source, param))
122            .collect::<Result<Vec<_>, _>>()?;
123        let (style, label, span_box) = classify_instruction(&op.instruction)?;
124        let children = build_children_for_instruction(source, qubits, &op.instruction, options)?;
125        let span_cols = estimate_span_cols(&style, children.as_ref());
126        let covered_lanes = compute_covered_lanes(
127            &lanes,
128            &style,
129            qubits.len(),
130            options.reserve_full_span_for_multi_qubit,
131        );
132
133        let column = compute_column(&covered_lanes, &next_free);
134        for lane in &covered_lanes {
135            next_free[*lane] = column + span_cols;
136        }
137        num_columns = num_columns.max(column + span_cols);
138
139        operations.push(VisualOperation {
140            column,
141            lanes,
142            covered_lanes,
143            label,
144            params,
145            style,
146            span_box,
147            children,
148            span_cols,
149        });
150    }
151
152    Ok(VisualCircuit {
153        qubits: qubits.to_vec(),
154        operations,
155        num_columns,
156    })
157}
158
159/// Build nested visual children for structured control-flow instructions.
160///
161/// Child circuits keep the parent's qubit lane list so branch/body drawings stay aligned
162/// with the surrounding circuit, even when a child only touches a subset of qubits.
163fn build_children_for_instruction(
164    source: &Circuit,
165    parent_qubits: &[Qubit],
166    instruction: &Instruction,
167    options: &VisualBuildOptions,
168) -> Result<Option<VisualChildren>, VisualizationError> {
169    match instruction {
170        Instruction::ClassicalControl(flow) => {
171            build_children_for_flow(source, parent_qubits, flow, options)
172        }
173        _ => Ok(None),
174    }
175}
176
177/// Convert a classical-control operation into visual child IR.
178///
179/// The source circuit is reused for parameter lookup, while operations come from the
180/// control-flow body being rendered.
181fn build_children_for_flow(
182    source: &Circuit,
183    parent_qubits: &[Qubit],
184    flow: &ClassicalControlOp,
185    options: &VisualBuildOptions,
186) -> Result<Option<VisualChildren>, VisualizationError> {
187    match flow {
188        ClassicalControlOp::If(gate) => {
189            let then_circuit = build_visual_circuit_from_ops(
190                source,
191                parent_qubits,
192                gate.then_body().operations(),
193                options,
194            )?;
195            let else_circuit = if let Some(body) = gate.else_body() {
196                Some(Box::new(build_visual_circuit_from_ops(
197                    source,
198                    parent_qubits,
199                    body.operations(),
200                    options,
201                )?))
202            } else {
203                None
204            };
205            Ok(Some(VisualChildren::IfElse {
206                then_circuit: Box::new(then_circuit),
207                else_circuit,
208            }))
209        }
210        ClassicalControlOp::While(gate) => {
211            let body_circuit = build_visual_circuit_from_ops(
212                source,
213                parent_qubits,
214                gate.body().operations(),
215                options,
216            )?;
217            Ok(Some(VisualChildren::While {
218                body_circuit: Box::new(body_circuit),
219            }))
220        }
221        ClassicalControlOp::For(gate) => {
222            let body_circuit = build_visual_circuit_from_ops(
223                source,
224                parent_qubits,
225                gate.body().operations(),
226                options,
227            )?;
228            Ok(Some(VisualChildren::For {
229                body_circuit: Box::new(body_circuit),
230            }))
231        }
232        ClassicalControlOp::Switch(gate) => {
233            let mut case_circuits = Vec::with_capacity(gate.cases().len());
234            for case in gate.cases() {
235                case_circuits.push((
236                    case.value().to_string(),
237                    Box::new(build_visual_circuit_from_ops(
238                        source,
239                        parent_qubits,
240                        case.body().operations(),
241                        options,
242                    )?),
243                ));
244            }
245            let default_circuit = if let Some(body) = gate.default() {
246                Some(Box::new(build_visual_circuit_from_ops(
247                    source,
248                    parent_qubits,
249                    body.operations(),
250                    options,
251                )?))
252            } else {
253                None
254            };
255            Ok(Some(VisualChildren::Switch {
256                case_circuits,
257                default_circuit,
258            }))
259        }
260        ClassicalControlOp::Break | ClassicalControlOp::Continue => Ok(None),
261    }
262}
263
264/// Estimate how many logical columns a visual operation must reserve.
265///
266/// Ordinary operations occupy one column. Control-flow blocks reserve enough columns for
267/// visible branch/body children plus small margins and separators so later operations do
268/// not overlap the expanded drawing.
269fn estimate_span_cols(style: &VisualOpStyle, children: Option<&VisualChildren>) -> usize {
270    if !matches!(style, VisualOpStyle::ControlFlow { .. }) {
271        return 1;
272    }
273    const MARGIN: usize = 2;
274    const SEP: usize = 2;
275
276    match children {
277        Some(VisualChildren::IfElse {
278            then_circuit,
279            else_circuit,
280        }) => {
281            let then_cols = then_circuit.num_columns.max(1);
282            let else_cols = else_circuit
283                .as_ref()
284                .map(|c| c.num_columns.max(1))
285                .unwrap_or(1);
286            MARGIN + then_cols + SEP + else_cols + MARGIN
287        }
288        Some(VisualChildren::While { body_circuit }) => {
289            let body_cols = body_circuit.num_columns.max(1);
290            MARGIN + body_cols + MARGIN
291        }
292        Some(VisualChildren::For { body_circuit }) => {
293            let body_cols = body_circuit.num_columns.max(1);
294            MARGIN + body_cols + MARGIN
295        }
296        Some(VisualChildren::Switch {
297            case_circuits,
298            default_circuit,
299        }) => {
300            let case_cols = case_circuits
301                .iter()
302                .map(|(_, circuit)| circuit.num_columns.max(1))
303                .sum::<usize>();
304            let default_cols = default_circuit
305                .as_ref()
306                .map(|circuit| circuit.num_columns.max(1))
307                .unwrap_or(0);
308            let branch_count = case_circuits.len() + usize::from(default_circuit.is_some());
309            let separators = branch_count.saturating_sub(1) * SEP;
310            MARGIN + case_cols + default_cols + separators + MARGIN
311        }
312        None => 3,
313    }
314}
315
316/// Map operation qubits to visual lane indices.
317///
318/// The returned order matches the operation operand order, which is important for controls,
319/// targets, and measurement labels.
320fn map_lanes(
321    op: &Operation,
322    qubit_to_lane: &HashMap<Qubit, usize>,
323) -> Result<Vec<usize>, VisualizationError> {
324    let mut lanes = Vec::with_capacity(op.qubits.len());
325    for qubit in &op.qubits {
326        let lane = qubit_to_lane
327            .get(qubit)
328            .ok_or(VisualizationError::UnknownQubit(qubit.id()))?;
329        lanes.push(*lane);
330    }
331    Ok(lanes)
332}
333
334/// Classify a circuit instruction into draw style, label, and span-box policy.
335///
336/// The classification is intentionally rendering-agnostic; text and figure backends consume
337/// the same [`VisualOpStyle`] and labels.
338fn classify_instruction(
339    instruction: &Instruction,
340) -> Result<(VisualOpStyle, String, bool), VisualizationError> {
341    match instruction {
342        Instruction::Standard(gate) => {
343            if *gate == StandardGate::SWAP {
344                return Ok((VisualOpStyle::Swap, "SWAP".to_string(), false));
345            }
346            if *gate == StandardGate::CZ {
347                return Ok((VisualOpStyle::Cz, "CZ".to_string(), false));
348            }
349            let num_ctrls = gate.num_ctrl_qubits();
350            if num_ctrls > 0 {
351                Ok((
352                    VisualOpStyle::Controlled {
353                        num_controls: num_ctrls,
354                    },
355                    controlled_target_label_for_standard(*gate),
356                    false,
357                ))
358            } else {
359                Ok((VisualOpStyle::Gate, standard_gate_label(*gate), false))
360            }
361        }
362        Instruction::McGate(gate) => {
363            if gate.num_ctrl_qubits() == 1 && *gate.base_gate() == StandardGate::Z {
364                Ok((VisualOpStyle::Cz, "CZ".to_string(), false))
365            } else {
366                Ok((
367                    VisualOpStyle::Controlled {
368                        num_controls: gate.num_ctrl_qubits(),
369                    },
370                    standard_gate_label(*gate.base_gate()),
371                    false,
372                ))
373            }
374        }
375        Instruction::UnitaryGate(gate) => {
376            let label = fallback_if_empty(gate.label(), "Unitary");
377            Ok((VisualOpStyle::Gate, label, true))
378        }
379        Instruction::CircuitGate(gate) => {
380            let label = fallback_if_empty(gate.name(), "Gate");
381            Ok((VisualOpStyle::Gate, label, true))
382        }
383        Instruction::Directive(Directive::Barrier) => {
384            Ok((VisualOpStyle::Barrier, "B".to_string(), false))
385        }
386        Instruction::Directive(Directive::Measure) => {
387            Ok((VisualOpStyle::Measure, "M".to_string(), false))
388        }
389        Instruction::Directive(Directive::Reset) => {
390            Ok((VisualOpStyle::Reset, "R".to_string(), false))
391        }
392        Instruction::Delay => Ok((VisualOpStyle::Delay, "D".to_string(), false)),
393        Instruction::ClassicalControl(flow) => {
394            let (style, label) = classify_control_flow(flow);
395            Ok((style, label, false))
396        }
397        Instruction::ClassicalData(ClassicalDataOp::MeasureBit { .. })
398        | Instruction::ClassicalData(ClassicalDataOp::MeasureBits { .. }) => {
399            Ok((VisualOpStyle::Measure, "M".to_string(), false))
400        }
401        Instruction::ClassicalData(ClassicalDataOp::Store { .. }) => {
402            Ok((VisualOpStyle::Gate, "STORE".to_string(), true))
403        }
404    }
405}
406
407/// Use a stable fallback label for unnamed module-like gates.
408fn fallback_if_empty(label: &str, default_label: &str) -> String {
409    let trimmed = label.trim();
410    if trimmed.is_empty() {
411        default_label.to_string()
412    } else {
413        trimmed.to_string()
414    }
415}
416
417/// Build a compact label and style marker for structured classical control flow.
418fn classify_control_flow(flow: &ClassicalControlOp) -> (VisualOpStyle, String) {
419    match flow {
420        ClassicalControlOp::If(gate) => {
421            let condition = VisualCondition {
422                label: classical_expr_label(gate.condition()),
423            };
424            let has_false_branch = gate.else_body().is_some();
425            (
426                VisualOpStyle::ControlFlow {
427                    kind: VisualControlFlowKind::IfElseBlock {
428                        has_false_branch,
429                        condition: condition.clone(),
430                    },
431                },
432                format!("IF {}", condition.label),
433            )
434        }
435        ClassicalControlOp::While(gate) => {
436            let condition = VisualCondition {
437                label: classical_expr_label(gate.condition()),
438            };
439            (
440                VisualOpStyle::ControlFlow {
441                    kind: VisualControlFlowKind::WhileBlock {
442                        condition: condition.clone(),
443                    },
444                },
445                format!("WH {}", condition.label),
446            )
447        }
448        ClassicalControlOp::For(gate) => {
449            let range = VisualCondition {
450                label: format!(
451                    "range({},{})",
452                    classical_expr_label(gate.start()),
453                    classical_expr_label(gate.stop()),
454                ),
455            };
456            (
457                VisualOpStyle::ControlFlow {
458                    kind: VisualControlFlowKind::ForBlock {
459                        range: range.clone(),
460                    },
461                },
462                format!("FOR {}", range.label),
463            )
464        }
465        ClassicalControlOp::Switch(gate) => {
466            let target = VisualCondition {
467                label: classical_expr_label(gate.target()),
468            };
469            (
470                VisualOpStyle::ControlFlow {
471                    kind: VisualControlFlowKind::SwitchBlock {
472                        target: target.clone(),
473                    },
474                },
475                format!("SW {}", target.label),
476            )
477        }
478        ClassicalControlOp::Break => (
479            VisualOpStyle::ControlFlow {
480                kind: VisualControlFlowKind::Break,
481            },
482            "Break".to_string(),
483        ),
484        ClassicalControlOp::Continue => (
485            VisualOpStyle::ControlFlow {
486                kind: VisualControlFlowKind::Continue,
487            },
488            "Continue".to_string(),
489        ),
490    }
491}
492
493/// Format classical expressions for compact display in control-flow labels.
494///
495/// Unsupported expression shapes fall back to `Debug` so visualization remains available
496/// while preserving enough detail for inspection.
497fn classical_expr_label(expr: &ClassicalExpr) -> String {
498    match expr.kind() {
499        ClassicalExprKind::BoolLiteral(value) => value.to_string(),
500        ClassicalExprKind::BitLiteral(value) => format!("bit({value})"),
501        ClassicalExprKind::UIntLiteral { value, .. } => value.to_string(),
502        ClassicalExprKind::BitVecLiteral { value, .. } => format!("bits({value})"),
503        _ => format!("{expr:?}"),
504    }
505}
506
507/// Choose the target label for controlled standard gates.
508fn controlled_target_label_for_standard(gate: StandardGate) -> String {
509    match gate {
510        StandardGate::CX | StandardGate::CCX => "X".to_string(),
511        StandardGate::CY => "Y".to_string(),
512        StandardGate::CRX => "RX".to_string(),
513        StandardGate::CRY => "RY".to_string(),
514        StandardGate::CRZ => "RZ".to_string(),
515        _ => strip_control_prefix(&standard_gate_label(gate)),
516    }
517}
518
519/// Determine which lanes must be reserved for column scheduling.
520///
521/// Multi-qubit gates usually reserve their full vertical span so wires and boxes do not
522/// collide with unrelated operations placed between their operands. Control-flow blocks
523/// reserve every lane because their child region spans the full circuit display.
524fn compute_covered_lanes(
525    lanes: &[usize],
526    style: &VisualOpStyle,
527    num_qubits: usize,
528    reserve_full_span: bool,
529) -> Vec<usize> {
530    if num_qubits == 0 {
531        return Vec::new();
532    }
533
534    if matches!(style, VisualOpStyle::Barrier) {
535        if lanes.is_empty() {
536            return (0..num_qubits).collect();
537        }
538        if reserve_full_span && lanes.len() > 1 {
539            let min_lane = lanes.iter().copied().min().unwrap_or(0);
540            let max_lane = lanes.iter().copied().max().unwrap_or(min_lane);
541            return (min_lane..=max_lane).collect();
542        }
543        return lanes.to_vec();
544    }
545
546    if matches!(style, VisualOpStyle::ControlFlow { .. }) {
547        return (0..num_qubits).collect();
548    }
549
550    if lanes.is_empty() {
551        return vec![0];
552    }
553
554    if reserve_full_span && lanes.len() > 1 {
555        let min_lane = lanes.iter().copied().min().unwrap_or(0);
556        let max_lane = lanes.iter().copied().max().unwrap_or(min_lane);
557        return (min_lane..=max_lane).collect();
558    }
559
560    lanes.to_vec()
561}
562
563/// Find the earliest column that is free on every covered lane.
564fn compute_column(covered_lanes: &[usize], next_free: &[usize]) -> usize {
565    let mut column = 0usize;
566    for lane in covered_lanes {
567        column = column.max(next_free[*lane]);
568    }
569    column
570}
571
572/// Public-facing gate label used by visualization backends.
573fn standard_gate_label(gate: StandardGate) -> String {
574    match gate {
575        StandardGate::SDG => "SD".to_string(),
576        StandardGate::TDG => "TD".to_string(),
577        StandardGate::Phase | StandardGate::GPhase => "P".to_string(),
578        _ => gate.to_string(),
579    }
580}
581
582/// Remove leading control markers from labels such as `CRX` when drawing the target box.
583fn strip_control_prefix(label: &str) -> String {
584    let mut out = label.to_string();
585    while out.starts_with('C') && out.len() > 1 {
586        out.remove(0);
587    }
588    out
589}