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}'; const VERT: char = '\u{2502}'; const CTRL: char = '@';
206const SWAP_X: char = '\u{00D7}'; fn 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 }
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), 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 °ree {
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 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 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}