Skip to main content

prism_q/circuit/
draw.rs

1use std::fmt;
2
3use crate::circuit::{Circuit, ClassicalCondition, Instruction, SmallVec};
4use crate::gates::Gate;
5
6const SUMMARY_QUBIT_THRESHOLD: usize = 64;
7const SUMMARY_MOMENT_THRESHOLD: usize = 500;
8const DEFAULT_FOLD_WIDTH: usize = 120;
9
10pub struct TextOptions {
11    pub fold_width: usize,
12    pub show_idle_wires: bool,
13    pub show_barriers: bool,
14    pub max_qubits: Option<usize>,
15    pub max_moments: Option<usize>,
16}
17
18impl Default for TextOptions {
19    fn default() -> Self {
20        Self {
21            fold_width: DEFAULT_FOLD_WIDTH,
22            show_idle_wires: true,
23            show_barriers: true,
24            max_qubits: None,
25            max_moments: None,
26        }
27    }
28}
29
30pub(super) struct PlacedOp {
31    pub(super) label: String,
32    pub(super) qubits: SmallVec<[usize; 4]>,
33    pub(super) kind: OpKind,
34    pub(super) gate: Option<Gate>,
35}
36
37pub(super) enum OpKind {
38    Single,
39    Controlled { controls: Vec<usize>, target: usize },
40    TwoQubit,
41    Swap,
42    Barrier,
43    Measure { cbit: usize },
44    Reset,
45    Conditional { cbit_label: String },
46    MultiFused,
47}
48
49fn classify_op(gate: &Gate, targets: &[usize]) -> (String, OpKind) {
50    let label = gate.to_string();
51    let kind = match gate {
52        Gate::Cx => OpKind::Controlled {
53            controls: vec![targets[0]],
54            target: targets[1],
55        },
56        Gate::Cu(_) => OpKind::Controlled {
57            controls: vec![targets[0]],
58            target: targets[1],
59        },
60        Gate::Mcu(data) => {
61            let nc = data.num_controls as usize;
62            OpKind::Controlled {
63                controls: targets[..nc].to_vec(),
64                target: targets[nc],
65            }
66        }
67        Gate::Cz => OpKind::Controlled {
68            controls: vec![targets[0]],
69            target: targets[1],
70        },
71        Gate::Swap => OpKind::Swap,
72        Gate::Rzz(_)
73        | Gate::Fused2q(_)
74        | Gate::BatchRzz(_)
75        | Gate::DiagonalBatch(_)
76        | Gate::Multi2q(_) => OpKind::TwoQubit,
77        Gate::BatchPhase(_) => OpKind::Controlled {
78            controls: vec![targets[0]],
79            target: *targets.last().unwrap(),
80        },
81        Gate::MultiFused(_) => OpKind::MultiFused,
82        _ => OpKind::Single,
83    };
84    (label, kind)
85}
86
87pub(super) fn assign_moments(circuit: &Circuit) -> Vec<Vec<PlacedOp>> {
88    let n = circuit.num_qubits;
89    let mut qubit_depth = vec![0usize; n];
90    let mut moments: Vec<Vec<PlacedOp>> = Vec::new();
91
92    for inst in &circuit.instructions {
93        match inst {
94            Instruction::Gate { gate, targets } => {
95                let max_d = targets.iter().map(|&q| qubit_depth[q]).max().unwrap_or(0);
96                let (label, kind) = classify_op(gate, targets);
97                let op = PlacedOp {
98                    label,
99                    qubits: SmallVec::from_slice(targets),
100                    kind,
101                    gate: Some(gate.clone()),
102                };
103                if max_d >= moments.len() {
104                    moments.resize_with(max_d + 1, Vec::new);
105                }
106                moments[max_d].push(op);
107                for &q in targets.iter() {
108                    qubit_depth[q] = max_d + 1;
109                }
110            }
111            Instruction::Measure {
112                qubit,
113                classical_bit,
114            } => {
115                let d = qubit_depth[*qubit];
116                let op = PlacedOp {
117                    label: "M".into(),
118                    qubits: smallvec::smallvec![*qubit],
119                    kind: OpKind::Measure {
120                        cbit: *classical_bit,
121                    },
122                    gate: None,
123                };
124                if d >= moments.len() {
125                    moments.resize_with(d + 1, Vec::new);
126                }
127                moments[d].push(op);
128                qubit_depth[*qubit] = d + 1;
129            }
130            Instruction::Reset { qubit } => {
131                let d = qubit_depth[*qubit];
132                let op = PlacedOp {
133                    label: "|0⟩".into(),
134                    qubits: smallvec::smallvec![*qubit],
135                    kind: OpKind::Reset,
136                    gate: None,
137                };
138                if d >= moments.len() {
139                    moments.resize_with(d + 1, Vec::new);
140                }
141                moments[d].push(op);
142                qubit_depth[*qubit] = d + 1;
143            }
144            Instruction::Barrier { qubits } => {
145                let max_d = qubits.iter().map(|&q| qubit_depth[q]).max().unwrap_or(0);
146                let op = PlacedOp {
147                    label: String::new(),
148                    qubits: SmallVec::from_slice(qubits),
149                    kind: OpKind::Barrier,
150                    gate: None,
151                };
152                if max_d >= moments.len() {
153                    moments.resize_with(max_d + 1, Vec::new);
154                }
155                moments[max_d].push(op);
156                for &q in qubits.iter() {
157                    qubit_depth[q] = max_d;
158                }
159            }
160            Instruction::Conditional {
161                condition,
162                gate,
163                targets,
164            } => {
165                let max_d = targets.iter().map(|&q| qubit_depth[q]).max().unwrap_or(0);
166                let cbit_label = match condition {
167                    ClassicalCondition::BitIsOne(b) => format!("c[{}]", b),
168                    ClassicalCondition::BitIsZero(b) => format!("!c[{}]", b),
169                    ClassicalCondition::RegisterEquals {
170                        offset,
171                        size,
172                        value,
173                    } => {
174                        format!("c[{}..{}]=={}", offset, offset + size, value)
175                    }
176                    ClassicalCondition::RegisterNotEquals {
177                        offset,
178                        size,
179                        value,
180                    } => {
181                        format!("c[{}..{}]!={}", offset, offset + size, value)
182                    }
183                };
184                let op = PlacedOp {
185                    label: gate.to_string(),
186                    qubits: SmallVec::from_slice(targets),
187                    kind: OpKind::Conditional { cbit_label },
188                    gate: Some(gate.clone()),
189                };
190                if max_d >= moments.len() {
191                    moments.resize_with(max_d + 1, Vec::new);
192                }
193                moments[max_d].push(op);
194                for &q in targets.iter() {
195                    qubit_depth[q] = max_d + 1;
196                }
197            }
198        }
199    }
200    moments
201}
202
203const WIRE: char = '\u{2500}'; // ─
204const VERT: char = '\u{2502}'; // │
205const CTRL: char = '@';
206const SWAP_X: char = '\u{00D7}'; // ×
207
208fn qubit_label_width(n: usize) -> usize {
209    if n <= 1 {
210        return 4;
211    }
212    let digits = ((n - 1) as f64).log10().floor() as usize + 1;
213    3 + digits // "q[" + digits + "]"
214}
215
216struct GridCell {
217    content: String,
218    is_vert_connector: bool,
219}
220
221impl GridCell {
222    fn wire(width: usize) -> Self {
223        Self {
224            content: WIRE.to_string().repeat(width),
225            is_vert_connector: false,
226        }
227    }
228
229    fn gate(label: &str, width: usize) -> Self {
230        let pad_total = width.saturating_sub(label.len());
231        let pad_left = pad_total / 2;
232        let pad_right = pad_total - pad_left;
233        let content = format!(
234            "{}{}{}",
235            WIRE.to_string().repeat(pad_left),
236            label,
237            WIRE.to_string().repeat(pad_right),
238        );
239        Self {
240            content,
241            is_vert_connector: false,
242        }
243    }
244
245    fn control(width: usize) -> Self {
246        Self::gate(&CTRL.to_string(), width)
247    }
248
249    fn swap_marker(width: usize) -> Self {
250        Self::gate(&SWAP_X.to_string(), width)
251    }
252
253    fn vert_connector(width: usize) -> Self {
254        let pad_left = width.saturating_sub(1) / 2;
255        let pad_right = width - pad_left - 1;
256        let content = format!("{}{}{}", " ".repeat(pad_left), VERT, " ".repeat(pad_right),);
257        Self {
258            content,
259            is_vert_connector: true,
260        }
261    }
262
263    fn barrier(width: usize) -> Self {
264        Self {
265            content: "\u{250A}".to_string().repeat(width), // ┊
266            is_vert_connector: false,
267        }
268    }
269}
270
271#[allow(clippy::needless_range_loop)]
272fn render_moments(moments: &[Vec<PlacedOp>], num_qubits: usize, opts: &TextOptions) -> Vec<String> {
273    if moments.is_empty() || num_qubits == 0 {
274        return Vec::new();
275    }
276
277    let max_moments = opts.max_moments.unwrap_or(moments.len()).min(moments.len());
278    let moments = &moments[..max_moments];
279
280    let label_w = qubit_label_width(num_qubits);
281
282    let mut col_widths: Vec<usize> = Vec::with_capacity(moments.len());
283    for moment in moments {
284        let mut max_label = 1usize;
285        for op in moment {
286            if matches!(op.kind, OpKind::Barrier) {
287                continue;
288            }
289            max_label = max_label.max(op.label.len());
290        }
291        col_widths.push(max_label + 2);
292    }
293
294    let mut active_qubits: Vec<bool> = vec![false; num_qubits];
295    if !opts.show_idle_wires {
296        for moment in moments {
297            for op in moment {
298                for &q in &op.qubits {
299                    if q < num_qubits {
300                        active_qubits[q] = true;
301                    }
302                }
303            }
304        }
305    } else {
306        active_qubits.fill(true);
307    }
308
309    let visible_qubits: Vec<usize> = (0..num_qubits).filter(|&q| active_qubits[q]).collect();
310    if visible_qubits.is_empty() {
311        return vec!["(empty circuit)".to_string()];
312    }
313
314    let max_vis_qubits = opts.max_qubits.unwrap_or(visible_qubits.len());
315    let show_qubits = &visible_qubits[..max_vis_qubits.min(visible_qubits.len())];
316    let elided = visible_qubits.len().saturating_sub(show_qubits.len());
317
318    let qubit_to_row: Vec<Option<usize>> = {
319        let mut map = vec![None; num_qubits];
320        for (row, &q) in show_qubits.iter().enumerate() {
321            map[q] = Some(row);
322        }
323        map
324    };
325
326    let num_rows = show_qubits.len() * 2 - 1;
327
328    let mut grid: Vec<Vec<GridCell>> = Vec::with_capacity(num_rows);
329    for r in 0..num_rows {
330        let mut row = Vec::with_capacity(moments.len());
331        for &w in &col_widths {
332            if r % 2 == 0 {
333                row.push(GridCell::wire(w));
334            } else {
335                row.push(GridCell {
336                    content: " ".repeat(w),
337                    is_vert_connector: false,
338                });
339            }
340        }
341        grid.push(row);
342    }
343
344    for (m_idx, moment) in moments.iter().enumerate() {
345        let w = col_widths[m_idx];
346        for op in moment {
347            match &op.kind {
348                OpKind::Single | OpKind::MultiFused => {
349                    for &q in &op.qubits {
350                        if let Some(row) = qubit_to_row.get(q).copied().flatten() {
351                            grid[row * 2][m_idx] = GridCell::gate(&op.label, w);
352                        }
353                    }
354                }
355                OpKind::Controlled { controls, target } => {
356                    for &c in controls {
357                        if let Some(row) = qubit_to_row.get(c).copied().flatten() {
358                            grid[row * 2][m_idx] = GridCell::control(w);
359                        }
360                    }
361                    if let Some(row) = qubit_to_row.get(*target).copied().flatten() {
362                        let tgt_label = match op.label.as_str() {
363                            "CX" => "X",
364                            "CZ" => "Z",
365                            "CU" => "U",
366                            other => {
367                                if other.starts_with("MCU") {
368                                    "U"
369                                } else {
370                                    &op.label
371                                }
372                            }
373                        };
374                        grid[row * 2][m_idx] = GridCell::gate(tgt_label, w);
375                    }
376                    let all_rows: Vec<usize> = controls
377                        .iter()
378                        .chain(std::iter::once(target))
379                        .filter_map(|&q| qubit_to_row.get(q).copied().flatten())
380                        .collect();
381                    if all_rows.len() >= 2 {
382                        let min_r = *all_rows.iter().min().unwrap();
383                        let max_r = *all_rows.iter().max().unwrap();
384                        for r in (min_r * 2 + 1)..=(max_r * 2 - 1) {
385                            if r % 2 == 1 {
386                                grid[r][m_idx] = GridCell::vert_connector(w);
387                            } else {
388                                let row_qubit_idx = r / 2;
389                                if !all_rows.contains(&row_qubit_idx) {
390                                    grid[r][m_idx] = GridCell::gate(&VERT.to_string(), w);
391                                }
392                            }
393                        }
394                    }
395                }
396                OpKind::TwoQubit => {
397                    let rows: Vec<usize> = op
398                        .qubits
399                        .iter()
400                        .filter_map(|&q| qubit_to_row.get(q).copied().flatten())
401                        .collect();
402                    if rows.len() >= 2 {
403                        let min_r = *rows.iter().min().unwrap();
404                        let max_r = *rows.iter().max().unwrap();
405                        grid[min_r * 2][m_idx] = GridCell::gate(&op.label, w);
406                        grid[max_r * 2][m_idx] = GridCell::gate(&op.label, w);
407                        for r in (min_r * 2 + 1)..=(max_r * 2 - 1) {
408                            if r % 2 == 1 {
409                                grid[r][m_idx] = GridCell::vert_connector(w);
410                            } else {
411                                let row_q = r / 2;
412                                if !rows.contains(&row_q) {
413                                    grid[r][m_idx] = GridCell::gate(&VERT.to_string(), w);
414                                }
415                            }
416                        }
417                    } else if let Some(&row) = rows.first() {
418                        grid[row * 2][m_idx] = GridCell::gate(&op.label, w);
419                    }
420                }
421                OpKind::Swap => {
422                    let rows: Vec<usize> = op
423                        .qubits
424                        .iter()
425                        .filter_map(|&q| qubit_to_row.get(q).copied().flatten())
426                        .collect();
427                    if rows.len() == 2 {
428                        let (min_r, max_r) = (rows[0].min(rows[1]), rows[0].max(rows[1]));
429                        grid[min_r * 2][m_idx] = GridCell::swap_marker(w);
430                        grid[max_r * 2][m_idx] = GridCell::swap_marker(w);
431                        for r in (min_r * 2 + 1)..=(max_r * 2 - 1) {
432                            if r % 2 == 1 {
433                                grid[r][m_idx] = GridCell::vert_connector(w);
434                            } else {
435                                let row_q = r / 2;
436                                if row_q != min_r && row_q != max_r {
437                                    grid[r][m_idx] = GridCell::gate(&VERT.to_string(), w);
438                                }
439                            }
440                        }
441                    }
442                }
443                OpKind::Barrier => {
444                    if opts.show_barriers {
445                        for &q in &op.qubits {
446                            if let Some(row) = qubit_to_row.get(q).copied().flatten() {
447                                grid[row * 2][m_idx] = GridCell::barrier(w);
448                            }
449                        }
450                    }
451                }
452                OpKind::Measure { cbit } => {
453                    if let Some(row) = op
454                        .qubits
455                        .first()
456                        .and_then(|&q| qubit_to_row.get(q).copied().flatten())
457                    {
458                        let label = format!("M{}", cbit);
459                        grid[row * 2][m_idx] = GridCell::gate(&label, w);
460                    }
461                }
462                OpKind::Reset => {
463                    if let Some(row) = op
464                        .qubits
465                        .first()
466                        .and_then(|&q| qubit_to_row.get(q).copied().flatten())
467                    {
468                        grid[row * 2][m_idx] = GridCell::gate("|0⟩", w);
469                    }
470                }
471                OpKind::Conditional { cbit_label } => {
472                    for &q in &op.qubits {
473                        if let Some(row) = qubit_to_row.get(q).copied().flatten() {
474                            let label = format!("{}?{}", cbit_label, op.label);
475                            grid[row * 2][m_idx] = GridCell::gate(&label, w);
476                        }
477                    }
478                }
479            }
480        }
481    }
482
483    let usable_width = opts.fold_width.saturating_sub(label_w + 2);
484
485    let mut sections: Vec<(usize, usize)> = Vec::new();
486    let mut start = 0;
487    let mut cur_width = 0;
488    for (i, &w) in col_widths.iter().enumerate() {
489        if cur_width + w > usable_width && i > start {
490            sections.push((start, i));
491            start = i;
492            cur_width = 0;
493        }
494        cur_width += w;
495    }
496    if start < col_widths.len() {
497        sections.push((start, col_widths.len()));
498    }
499
500    let mut lines: Vec<String> = Vec::new();
501
502    for (sec_idx, &(col_start, col_end)) in sections.iter().enumerate() {
503        if sec_idx > 0 {
504            let sec_width: usize =
505                col_widths[col_start..col_end].iter().sum::<usize>() + label_w + 2;
506            lines.push("\u{254C}".repeat(sec_width));
507        }
508        for (vis_row, &q) in show_qubits.iter().enumerate() {
509            let label = format!("q[{}]", q);
510            let padded = format!("{:>width$}: ", label, width = label_w);
511
512            let wire_row = vis_row * 2;
513            let mut line = padded;
514            for col in col_start..col_end {
515                line.push_str(&grid[wire_row][col].content);
516            }
517            let trimmed = line.trim_end();
518            lines.push(trimmed.to_string());
519
520            if vis_row < show_qubits.len() - 1 {
521                let conn_row = vis_row * 2 + 1;
522                let padding = " ".repeat(label_w + 2);
523                let mut conn_line = padding;
524                let mut has_content = false;
525                for col in col_start..col_end {
526                    let cell = &grid[conn_row][col];
527                    if cell.is_vert_connector {
528                        has_content = true;
529                    }
530                    conn_line.push_str(&cell.content);
531                }
532                if has_content {
533                    let trimmed = conn_line.trim_end();
534                    lines.push(trimmed.to_string());
535                }
536            }
537        }
538    }
539
540    if elided > 0 {
541        lines.push(format!("  ... and {} more qubits", elided));
542    }
543
544    if max_moments < moments.len() {
545        lines.push(format!(
546            "  ... truncated at moment {} of {}",
547            max_moments,
548            moments.len()
549        ));
550    }
551
552    lines
553}
554
555fn collect_2q_edges(circuit: &Circuit) -> Vec<(usize, usize)> {
556    let mut edges = Vec::new();
557    for inst in &circuit.instructions {
558        let targets: &[usize] = match inst {
559            Instruction::Gate { targets, .. } | Instruction::Conditional { targets, .. } => targets,
560            _ => continue,
561        };
562        if targets.len() == 2 {
563            let (a, b) = (targets[0].min(targets[1]), targets[0].max(targets[1]));
564            edges.push((a, b));
565        }
566    }
567    edges
568}
569
570fn render_connectivity(lines: &mut Vec<String>, circuit: &Circuit) {
571    let n = circuit.num_qubits;
572    let edges = collect_2q_edges(circuit);
573    if edges.is_empty() {
574        lines.push("Connectivity: none (no 2q gates)".to_string());
575        return;
576    }
577
578    let mut degree = vec![0usize; n];
579    let mut unique_neighbors: Vec<std::collections::HashSet<usize>> =
580        vec![std::collections::HashSet::new(); n];
581    for &(a, b) in &edges {
582        degree[a] += 1;
583        degree[b] += 1;
584        unique_neighbors[a].insert(b);
585        unique_neighbors[b].insert(a);
586    }
587
588    let total_2q = edges.len();
589    let unique_pairs: std::collections::HashSet<(usize, usize)> = edges.iter().copied().collect();
590    let num_unique_pairs = unique_pairs.len();
591
592    let max_degree_q = degree
593        .iter()
594        .enumerate()
595        .max_by_key(|(_, &d)| d)
596        .map(|(q, _)| q)
597        .unwrap_or(0);
598    let max_degree = degree[max_degree_q];
599
600    let max_neighbors = unique_neighbors.iter().map(|s| s.len()).max().unwrap_or(0);
601
602    let all_nn = unique_pairs.iter().all(|&(a, b)| b.saturating_sub(a) == 1);
603    let max_possible = n * (n - 1) / 2;
604    let topology = if all_nn && num_unique_pairs <= n {
605        "nearest-neighbor"
606    } else if num_unique_pairs == max_possible {
607        "all-to-all"
608    } else if max_neighbors <= 4 && n > 8 {
609        "sparse"
610    } else {
611        "mixed"
612    };
613
614    lines.push(format!(
615        "Connectivity: {} 2q gates across {} unique pairs ({})",
616        total_2q, num_unique_pairs, topology,
617    ));
618    lines.push(format!(
619        "  max degree: {} (q[{}], {} unique neighbors)",
620        max_degree,
621        max_degree_q,
622        unique_neighbors[max_degree_q].len(),
623    ));
624
625    let bar_max = 30usize;
626    let mut deg_dist: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
627    for &d in &degree {
628        if d > 0 {
629            *deg_dist.entry(d).or_default() += 1;
630        }
631    }
632    if deg_dist.len() > 1 {
633        let mut sorted_degrees: Vec<(usize, usize)> = deg_dist.into_iter().collect();
634        sorted_degrees.sort_by_key(|&(d, _)| d);
635        let max_freq = sorted_degrees.iter().map(|(_, c)| *c).max().unwrap_or(1);
636        lines.push("  degree distribution:".to_string());
637        for (d, count) in &sorted_degrees {
638            let bar_len = (*count * bar_max) / max_freq;
639            lines.push(format!(
640                "    {:>3}: {} {}",
641                d,
642                "\u{2588}".repeat(bar_len),
643                count,
644            ));
645        }
646    }
647}
648
649fn render_depth_profile(lines: &mut Vec<String>, moments: &[Vec<PlacedOp>]) {
650    if moments.is_empty() {
651        return;
652    }
653    let depth = moments.len();
654    let num_buckets = 20.min(depth);
655    let bucket_size = depth.div_ceil(num_buckets).max(1);
656    let actual_buckets = depth.div_ceil(bucket_size);
657
658    let mut bucket_counts = vec![0usize; actual_buckets];
659    let mut bucket_2q = vec![0usize; actual_buckets];
660    for (m_idx, moment) in moments.iter().enumerate() {
661        let b = m_idx / bucket_size;
662        for op in moment {
663            bucket_counts[b] += 1;
664            if op.qubits.len() >= 2 {
665                bucket_2q[b] += 1;
666            }
667        }
668    }
669
670    let max_gates = *bucket_counts.iter().max().unwrap_or(&1).max(&1);
671    let bar_max = 40usize;
672
673    lines.push("Depth profile (gates per time slice):".to_string());
674    let depth_w = ((depth - 1) as f64).log10().floor() as usize + 1;
675    for (b, &count) in bucket_counts.iter().enumerate() {
676        let start = b * bucket_size;
677        let end = ((b + 1) * bucket_size).min(depth) - 1;
678        let bar_1q = ((count - bucket_2q[b]) * bar_max) / max_gates;
679        let bar_2q = (bucket_2q[b] * bar_max) / max_gates;
680        lines.push(format!(
681            "  {:>w$}-{:<w$}: {}{} {}",
682            start,
683            end,
684            "\u{2588}".repeat(bar_1q),
685            "\u{2593}".repeat(bar_2q),
686            count,
687            w = depth_w,
688        ));
689    }
690    if bucket_2q.iter().any(|&c| c > 0) {
691        lines.push("  (\u{2588} = 1q  \u{2593} = 2q)".to_string());
692    }
693}
694
695const HEATMAP_CHARS: &[char] = &[' ', '\u{2591}', '\u{2592}', '\u{2593}', '\u{2588}'];
696
697fn render_heatmap(circuit: &Circuit, opts: &TextOptions) -> Vec<String> {
698    let moments = assign_moments(circuit);
699    if moments.is_empty() || circuit.num_qubits == 0 {
700        return vec!["(empty circuit)".to_string()];
701    }
702
703    let n = circuit.num_qubits;
704    let depth = moments.len();
705
706    let max_cols = (opts.fold_width.saturating_sub(14)).max(20);
707    let max_rows = 40.min(n);
708
709    let col_bucket = depth.div_ceil(max_cols).max(1);
710    let row_bucket = n.div_ceil(max_rows).max(1);
711    let actual_cols = depth.div_ceil(col_bucket);
712    let actual_rows = n.div_ceil(row_bucket);
713
714    let mut grid = vec![vec![0usize; actual_cols]; actual_rows];
715    for (m_idx, moment) in moments.iter().enumerate() {
716        let col = m_idx / col_bucket;
717        for op in moment {
718            for &q in &op.qubits {
719                if q < n {
720                    let row = q / row_bucket;
721                    grid[row][col] += 1;
722                }
723            }
724        }
725    }
726
727    let max_density = grid
728        .iter()
729        .flat_map(|r| r.iter())
730        .copied()
731        .max()
732        .unwrap_or(1)
733        .max(1);
734
735    let mut lines = Vec::new();
736    lines.push(format!(
737        "Gate density heatmap ({} qubits x {} moments):",
738        n, depth,
739    ));
740
741    let label_w = if row_bucket == 1 {
742        qubit_label_width(n)
743    } else {
744        let w = ((n - 1) as f64).log10().floor() as usize + 1;
745        w * 2 + 4
746    };
747
748    for (r, row) in grid.iter().enumerate() {
749        let label = if row_bucket == 1 {
750            format!("q[{}]", r)
751        } else {
752            let start = r * row_bucket;
753            let end = ((r + 1) * row_bucket).min(n) - 1;
754            format!("{}-{}", start, end)
755        };
756
757        let mut line = format!("{:>width$}: ", label, width = label_w);
758        for &count in row {
759            let level = if count == 0 {
760                0
761            } else {
762                (count * 4 / max_density).clamp(1, 4)
763            };
764            line.push(HEATMAP_CHARS[level]);
765        }
766        lines.push(line);
767    }
768
769    let moment_axis = format!("{:>width$}  ", "", width = label_w);
770    let end_label = format!("{}", depth - 1);
771    let axis_line = format!(
772        "{}0{}{}",
773        moment_axis,
774        " ".repeat(actual_cols.saturating_sub(1 + end_label.len())),
775        end_label,
776    );
777    lines.push(axis_line);
778
779    if max_density <= 4 {
780        let mut legend = String::from("  Legend: ' '=0");
781        for level in 1..=max_density {
782            let ch = HEATMAP_CHARS[((level * 4) / max_density).clamp(1, 4)];
783            legend.push_str(&format!("  {ch}={level}"));
784        }
785        lines.push(legend);
786    } else {
787        lines.push(format!(
788            "  Legend: ' '=0  \u{2591}=1-{}  \u{2592}={}-{}  \u{2593}={}-{}  \u{2588}={}-{}",
789            max_density / 4,
790            max_density / 4 + 1,
791            max_density / 2,
792            max_density / 2 + 1,
793            3 * max_density / 4,
794            3 * max_density / 4 + 1,
795            max_density,
796        ));
797    }
798
799    lines
800}
801
802fn render_summary(circuit: &Circuit) -> Vec<String> {
803    let mut lines = Vec::new();
804    let moments = assign_moments(circuit);
805    let depth = moments.len();
806    lines.push(format!(
807        "Circuit: {} qubits, {} gates, depth {}",
808        circuit.num_qubits,
809        circuit.gate_count(),
810        depth,
811    ));
812    lines.push(String::new());
813
814    let mut gate_counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
815    let mut measure_count = 0usize;
816    let mut barrier_count = 0usize;
817    let mut conditional_count = 0usize;
818
819    for inst in &circuit.instructions {
820        match inst {
821            Instruction::Gate { gate, .. } => {
822                *gate_counts.entry(gate.name()).or_default() += 1;
823            }
824            Instruction::Measure { .. } => measure_count += 1,
825            Instruction::Reset { .. } => {
826                *gate_counts.entry("reset").or_default() += 1;
827            }
828            Instruction::Barrier { .. } => barrier_count += 1,
829            Instruction::Conditional { gate, .. } => {
830                conditional_count += 1;
831                *gate_counts.entry(gate.name()).or_default() += 1;
832            }
833        }
834    }
835
836    let mut sorted: Vec<(&&str, &usize)> = gate_counts.iter().collect();
837    sorted.sort_by(|a, b| b.1.cmp(a.1));
838
839    lines.push("Gate counts:".to_string());
840    let mut gate_line = String::from("  ");
841    for (i, (name, count)) in sorted.iter().enumerate() {
842        if i > 0 {
843            gate_line.push_str("  ");
844        }
845        let entry = format!("{}: {}", name, count);
846        if gate_line.len() + entry.len() > 100 {
847            lines.push(gate_line);
848            gate_line = format!("  {}", entry);
849        } else {
850            gate_line.push_str(&entry);
851        }
852    }
853    if !gate_line.trim().is_empty() {
854        lines.push(gate_line);
855    }
856
857    if measure_count > 0 {
858        lines.push(format!("  measurements: {}", measure_count));
859    }
860    if barrier_count > 0 {
861        lines.push(format!("  barriers: {}", barrier_count));
862    }
863    if conditional_count > 0 {
864        lines.push(format!("  conditionals: {}", conditional_count));
865    }
866
867    lines.push(String::new());
868
869    render_connectivity(&mut lines, circuit);
870    lines.push(String::new());
871
872    render_depth_profile(&mut lines, &moments);
873    lines.push(String::new());
874
875    let n = circuit.num_qubits;
876    let mut qubit_gate_count = vec![0usize; n];
877    for inst in &circuit.instructions {
878        let targets: &[usize] = match inst {
879            Instruction::Gate { targets, .. } | Instruction::Conditional { targets, .. } => targets,
880            Instruction::Measure { qubit, .. } | Instruction::Reset { qubit } => {
881                std::slice::from_ref(qubit)
882            }
883            Instruction::Barrier { .. } => continue,
884        };
885        for &q in targets {
886            if q < n {
887                qubit_gate_count[q] += 1;
888            }
889        }
890    }
891
892    let max_count = *qubit_gate_count.iter().max().unwrap_or(&1).max(&1);
893    let bar_max = 40usize;
894
895    if n <= 32 {
896        lines.push("Qubit activity:".to_string());
897        for (q, &c) in qubit_gate_count.iter().enumerate() {
898            let bar_len = (c * bar_max) / max_count;
899            lines.push(format!(
900                "  q[{:>width$}]: {} {}",
901                q,
902                "\u{2588}".repeat(bar_len),
903                c,
904                width = ((n - 1) as f64).log10().floor() as usize + 1,
905            ));
906        }
907    } else {
908        lines.push("Qubit activity (bucketed):".to_string());
909        let bucket_size = n.div_ceil(10).max(1);
910        let mut bucket_start = 0;
911        while bucket_start < n {
912            let bucket_end = (bucket_start + bucket_size).min(n);
913            let min_c = qubit_gate_count[bucket_start..bucket_end]
914                .iter()
915                .copied()
916                .min()
917                .unwrap_or(0);
918            let max_c = qubit_gate_count[bucket_start..bucket_end]
919                .iter()
920                .copied()
921                .max()
922                .unwrap_or(0);
923            let avg_c = qubit_gate_count[bucket_start..bucket_end]
924                .iter()
925                .sum::<usize>()
926                / (bucket_end - bucket_start);
927            let bar_len = (avg_c * bar_max) / max_count;
928            lines.push(format!(
929                "  q[{:>4}..{:<4}]: {} {}-{}",
930                bucket_start,
931                bucket_end - 1,
932                "\u{2588}".repeat(bar_len),
933                min_c,
934                max_c,
935            ));
936            bucket_start = bucket_end;
937        }
938    }
939
940    lines.push(String::new());
941
942    let heatmap = render_heatmap(circuit, &TextOptions::default());
943    lines.extend(heatmap);
944    lines.push(String::new());
945
946    let classification = if circuit.is_clifford_only() {
947        "Clifford-only"
948    } else if circuit.is_clifford_plus_t() {
949        "Clifford+T"
950    } else {
951        "General"
952    };
953    lines.push(format!("Classification: {}", classification));
954
955    if circuit.has_terminal_measurements_only() {
956        lines.push(format!("Measurements: terminal-only ({})", measure_count));
957    } else if measure_count > 0 {
958        lines.push(format!("Measurements: mid-circuit ({})", measure_count));
959    } else {
960        lines.push("Measurements: none".to_string());
961    }
962
963    let components = circuit.independent_subsystems();
964    if components.len() > 1 {
965        let sizes: Vec<usize> = components.iter().map(|c| c.len()).collect();
966        let max_block = sizes.iter().max().unwrap_or(&0);
967        lines.push(format!(
968            "Components: {} (max block: {}q)",
969            components.len(),
970            max_block,
971        ));
972    }
973
974    lines
975}
976
977impl Circuit {
978    pub fn draw(&self, opts: &TextOptions) -> String {
979        let moments = assign_moments(self);
980        let use_summary =
981            self.num_qubits > SUMMARY_QUBIT_THRESHOLD || moments.len() > SUMMARY_MOMENT_THRESHOLD;
982
983        if use_summary {
984            return render_summary(self).join("\n");
985        }
986
987        let lines = render_moments(&moments, self.num_qubits, opts);
988        if lines.is_empty() {
989            return "(empty circuit)".to_string();
990        }
991        lines.join("\n")
992    }
993
994    pub fn summary(&self) -> String {
995        render_summary(self).join("\n")
996    }
997
998    pub fn heatmap(&self, opts: &TextOptions) -> String {
999        render_heatmap(self, opts).join("\n")
1000    }
1001}
1002
1003impl fmt::Display for Circuit {
1004    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1005        let opts = TextOptions::default();
1006        write!(f, "{}", self.draw(&opts))
1007    }
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012    use super::*;
1013    use crate::circuit::builder::CircuitBuilder;
1014
1015    #[test]
1016    fn gate_display_labels() {
1017        assert_eq!(Gate::H.to_string(), "H");
1018        assert_eq!(Gate::Cx.to_string(), "CX");
1019        assert_eq!(Gate::Rx(std::f64::consts::FRAC_PI_2).to_string(), "Rx(π/2)");
1020        assert_eq!(Gate::Rz(0.5).to_string(), "Rz(0.5000)");
1021    }
1022
1023    #[test]
1024    fn bell_pair_diagram() {
1025        let circuit = CircuitBuilder::new(2).h(0).cx(0, 1).build();
1026        let text = circuit.draw(&TextOptions::default());
1027        assert!(text.contains("q[0]"));
1028        assert!(text.contains("q[1]"));
1029        assert!(text.contains("H"));
1030        assert!(text.contains(CTRL.to_string().as_str()));
1031        assert!(text.contains("X"));
1032    }
1033
1034    #[test]
1035    fn ghz_3q_diagram() {
1036        let circuit = CircuitBuilder::new(3).h(0).cx(0, 1).cx(1, 2).build();
1037        let text = circuit.draw(&TextOptions::default());
1038        assert!(text.contains("q[0]"));
1039        assert!(text.contains("q[1]"));
1040        assert!(text.contains("q[2]"));
1041        let lines: Vec<&str> = text.lines().collect();
1042        assert!(lines.len() >= 3);
1043    }
1044
1045    fn char_col(s: &str, ch: char) -> Option<usize> {
1046        s.chars().position(|c| c == ch)
1047    }
1048
1049    #[test]
1050    fn connector_alignment_even_width() {
1051        let mut builder = CircuitBuilder::new(12);
1052        for i in (0..12).step_by(2) {
1053            builder.h(i).cx(i, i + 1);
1054        }
1055        let circuit = builder.build();
1056        let text = circuit.draw(&TextOptions::default());
1057        let lines: Vec<&str> = text.lines().collect();
1058        for (i, line) in lines.iter().enumerate() {
1059            if let Some(pos) = char_col(line, VERT) {
1060                if i > 0 {
1061                    if let Some(ctrl_pos) = char_col(lines[i - 1], CTRL) {
1062                        assert_eq!(
1063                            ctrl_pos,
1064                            pos,
1065                            "connector at line {} misaligned with control at line {}\n{}",
1066                            i,
1067                            i - 1,
1068                            text,
1069                        );
1070                    }
1071                }
1072                if i + 1 < lines.len() {
1073                    if let Some(x_pos) = char_col(lines[i + 1], 'X') {
1074                        assert_eq!(
1075                            x_pos,
1076                            pos,
1077                            "connector at line {} misaligned with target at line {}\n{}",
1078                            i,
1079                            i + 1,
1080                            text,
1081                        );
1082                    }
1083                }
1084            }
1085        }
1086    }
1087
1088    #[test]
1089    fn empty_circuit() {
1090        let circuit = Circuit::new(2, 0);
1091        let text = circuit.draw(&TextOptions::default());
1092        assert_eq!(text, "(empty circuit)");
1093    }
1094
1095    #[test]
1096    fn single_gate() {
1097        let circuit = CircuitBuilder::new(1).h(0).build();
1098        let text = circuit.draw(&TextOptions::default());
1099        assert!(text.contains("H"));
1100        assert!(text.contains("q[0]"));
1101    }
1102
1103    #[test]
1104    fn measurement_shown() {
1105        let circuit = CircuitBuilder::new(1).h(0).measure_all().build();
1106        // Circuit already has a measurement from measure_all()
1107        let text = circuit.draw(&TextOptions::default());
1108        assert!(text.contains("M"));
1109    }
1110
1111    #[test]
1112    fn idle_wire_elision() {
1113        let circuit = CircuitBuilder::new(5).h(0).cx(0, 4).build();
1114        let opts = TextOptions {
1115            show_idle_wires: false,
1116            ..Default::default()
1117        };
1118        let text = circuit.draw(&opts);
1119        assert!(text.contains("q[0]"));
1120        assert!(text.contains("q[4]"));
1121        assert!(!text.contains("q[1]"));
1122        assert!(!text.contains("q[2]"));
1123        assert!(!text.contains("q[3]"));
1124    }
1125
1126    #[test]
1127    fn summary_mode() {
1128        let circuit = crate::circuits::random_circuit(100, 10, 42);
1129        let summary = circuit.summary();
1130        assert!(summary.contains("Circuit: 100 qubits"));
1131        assert!(summary.contains("Gate counts:"));
1132        assert!(summary.contains("Qubit activity"));
1133    }
1134
1135    #[test]
1136    fn auto_summary_for_large() {
1137        let circuit = crate::circuits::random_circuit(100, 10, 42);
1138        let text = circuit.draw(&TextOptions::default());
1139        assert!(text.contains("Circuit: 100 qubits"));
1140    }
1141
1142    #[test]
1143    fn display_impl() {
1144        let circuit = CircuitBuilder::new(2).h(0).cx(0, 1).build();
1145        let text = format!("{}", circuit);
1146        assert!(text.contains("q[0]"));
1147        assert!(text.contains("q[1]"));
1148    }
1149
1150    #[test]
1151    fn fold_wraps_long_circuits() {
1152        let circuit = crate::circuits::random_circuit(3, 50, 42);
1153        let opts = TextOptions {
1154            fold_width: 60,
1155            ..Default::default()
1156        };
1157        let text = circuit.draw(&opts);
1158        assert!(text.contains("\u{254C}"));
1159    }
1160
1161    #[test]
1162    fn swap_gate_display() {
1163        let circuit = CircuitBuilder::new(2).swap(0, 1).build();
1164        let text = circuit.draw(&TextOptions::default());
1165        assert!(text.contains(SWAP_X.to_string().as_str()));
1166    }
1167
1168    #[test]
1169    fn parametric_gate_display() {
1170        let circuit = CircuitBuilder::new(1)
1171            .rx(std::f64::consts::FRAC_PI_4, 0)
1172            .build();
1173        let text = circuit.draw(&TextOptions::default());
1174        assert!(text.contains("Rx(π/4)"));
1175    }
1176
1177    #[test]
1178    fn qft_4q_diagram() {
1179        // `qft_circuit` emits a single `Gate::QftBlock`.
1180        // The block renders as a labelled box; users wanting the unrolled
1181        // H + cphase diagram can call `expand_qft_blocks` first.
1182        let circuit = crate::circuits::qft_circuit(4);
1183        let text = circuit.draw(&TextOptions::default());
1184        assert!(text.contains("q[0]"));
1185        assert!(text.contains("q[3]"));
1186
1187        let unrolled = crate::circuit::expand_qft_blocks(&circuit);
1188        let unrolled_text = unrolled.draw(&TextOptions::default());
1189        assert!(unrolled_text.contains("H"));
1190    }
1191
1192    #[test]
1193    fn summary_has_connectivity() {
1194        let circuit = crate::circuits::random_circuit(100, 10, 42);
1195        let summary = circuit.summary();
1196        assert!(summary.contains("Connectivity:"));
1197        assert!(summary.contains("max degree:"));
1198    }
1199
1200    #[test]
1201    fn summary_no_2q_connectivity() {
1202        let circuit = CircuitBuilder::new(4).h(0).h(1).h(2).h(3).build();
1203        let summary = circuit.summary();
1204        assert!(summary.contains("Connectivity: none"));
1205    }
1206
1207    #[test]
1208    fn summary_has_depth_profile() {
1209        let circuit = crate::circuits::random_circuit(100, 10, 42);
1210        let summary = circuit.summary();
1211        assert!(summary.contains("Depth profile"));
1212    }
1213
1214    #[test]
1215    fn summary_has_heatmap() {
1216        let circuit = crate::circuits::random_circuit(100, 10, 42);
1217        let summary = circuit.summary();
1218        assert!(summary.contains("Gate density heatmap"));
1219        assert!(summary.contains("Legend:"));
1220    }
1221
1222    #[test]
1223    fn heatmap_standalone() {
1224        let circuit = crate::circuits::ghz_circuit(10);
1225        let hm = circuit.heatmap(&TextOptions::default());
1226        assert!(hm.contains("Gate density heatmap"));
1227        assert!(hm.contains("q[0]"));
1228    }
1229
1230    #[test]
1231    fn connectivity_nearest_neighbor() {
1232        let circuit = crate::circuits::ghz_circuit(8);
1233        let summary = circuit.summary();
1234        assert!(summary.contains("nearest-neighbor"));
1235    }
1236}