cqlib_core/visualization/circuit/
text.rs1use crate::circuit::Circuit;
38use crate::visualization::VisualizationError;
39use crate::visualization::circuit::ir::{
40 VisualCircuit, VisualControlFlowKind, VisualOpStyle, VisualOperation,
41 flatten_control_flow_visual, reverse_visual_lanes,
42};
43use crate::visualization::circuit::layout::{VisualBuildOptions, build_visual_circuit};
44
45const MIN_LINE_WIDTH: isize = 10;
46const DEFAULT_LINE_WIDTH: isize = 80;
47
48#[derive(Debug, Clone, Copy)]
53pub struct TextDrawerOptions {
54 pub cell_width: usize,
56 pub show_params: bool,
58 pub decompose_circuit_gates: bool,
60 pub line_width: isize,
62 pub initial_state: bool,
64 pub reverse_bits: bool,
66}
67
68impl TextDrawerOptions {
69 fn effective_line_width(&self) -> usize {
70 let width = self.line_width;
71 if width > MIN_LINE_WIDTH {
72 return width as usize;
73 }
74 if width < 0 {
75 return usize::MAX;
76 }
77 DEFAULT_LINE_WIDTH as usize
78 }
79}
80
81impl Default for TextDrawerOptions {
82 fn default() -> Self {
83 Self {
84 cell_width: 9,
85 show_params: true,
86 decompose_circuit_gates: false,
87 line_width: DEFAULT_LINE_WIDTH,
88 initial_state: false,
89 reverse_bits: false,
90 }
91 }
92}
93
94struct BoxChar;
95
96#[allow(dead_code)]
97impl BoxChar {
98 const TOP: &'static str = "╵";
99 const BOTTOM: &'static str = "╷";
100 const LEFT: &'static str = "╴";
101 const RIGHT: &'static str = "╶";
102 const TOP_BOTTOM: &'static str = "│";
103 const LEFT_RIGHT: &'static str = "─";
104
105 const TOP_LEFT: &'static str = "┘";
106 const TOP_RIGHT: &'static str = "└";
107 const BOTTOM_LEFT: &'static str = "┐";
108 const BOTTOM_RIGHT: &'static str = "┌";
109
110 const TOP_BOTTOM_LEFT: &'static str = "┤";
111 const TOP_BOTTOM_RIGHT: &'static str = "├";
112 const TOP_LEFT_RIGHT: &'static str = "┴";
113 const BOTTOM_LEFT_RIGHT: &'static str = "┬";
114 const TOP_BOTTOM_LEFT_RIGHT: &'static str = "┼";
115
116 const DOT: &'static str = "■";
117 const CONNECT: &'static str = "X";
118 const LEFT_ARROW: &'static str = "«";
119 const RIGHT_ARROW: &'static str = "»";
120}
121
122pub fn circuit_to_text(
150 circuit: &Circuit,
151 options: &TextDrawerOptions,
152) -> Result<String, VisualizationError> {
153 let visual_options = VisualBuildOptions {
154 decompose_circuit_gates: options.decompose_circuit_gates,
155 ..VisualBuildOptions::default()
156 };
157 let visual = build_visual_circuit(circuit, &visual_options)?;
158 draw_text_from_visual(&visual, options)
159}
160
161pub fn draw_text_from_visual(
194 visual: &VisualCircuit,
195 options: &TextDrawerOptions,
196) -> Result<String, VisualizationError> {
197 if visual.num_qubits() == 0 {
198 return Ok("empty circuit".to_string());
199 }
200
201 let mut flattened = flatten_control_flow_visual(visual);
202 if options.reverse_bits {
203 flattened = reverse_visual_lanes(flattened);
204 }
205 let lines = make_lines(&flattened, options.show_params, options.initial_state);
206 let lines_count = total_rows(flattened.num_qubits());
207 let max_line_width = options.effective_line_width();
208
209 let start_qubits = lines[0].clone();
210 let mut current_data = start_qubits.clone();
211 let mut current_width = 0usize;
212 let mut data: Vec<Vec<Vec<String>>> = Vec::new();
213
214 for moment in lines.iter().skip(1) {
215 let moment_len = str_len(&moment[0].concat());
216 if max_line_width != usize::MAX && moment_len.saturating_add(current_width) > max_line_width
217 {
218 for row in &mut current_data {
219 row.push(BoxChar::RIGHT_ARROW.to_string());
220 }
221 data.push(current_data);
222
223 let mut next_data = Vec::with_capacity(lines_count);
224 for (i, sq) in start_qubits.iter().enumerate().take(lines_count) {
225 let mut row = vec![BoxChar::LEFT_ARROW.to_string()];
226 row.extend(sq.clone());
227 next_data.push(row);
228 let _ = i;
229 }
230 current_data = next_data;
231 current_width = 0;
232 }
233
234 for (i, mrow) in moment.iter().enumerate().take(lines_count) {
235 current_data[i].extend(mrow.clone());
236 }
237 current_width = current_width.saturating_add(moment_len);
238 }
239 data.push(current_data);
240
241 let block_strings: Vec<String> = data
242 .into_iter()
243 .map(|block| {
244 block
245 .into_iter()
246 .map(|row| row.concat())
247 .collect::<Vec<_>>()
248 .join("\n")
249 })
250 .collect();
251 Ok(block_strings.join("\n\n"))
252}
253
254fn y_wire(lane: usize) -> usize {
255 lane * 2 + 1
256}
257
258fn total_rows(num_qubits: usize) -> usize {
259 num_qubits * 2 + 1
260}
261
262fn make_lines(
263 visual: &VisualCircuit,
264 show_params: bool,
265 initial_state: bool,
266) -> Vec<Vec<Vec<String>>> {
267 let mut lines = Vec::new();
268 let lines_count = total_rows(visual.num_qubits());
269 let qubit_len = visual
270 .qubits
271 .iter()
272 .map(|q| q.id().to_string().len())
273 .max()
274 .unwrap_or(1);
275
276 let initial_prefix = if initial_state { "|0>" } else { "" };
277 let empty_line = " ".repeat(qubit_len + if initial_state { 9 } else { 6 });
278 let mut start_lines: Vec<Vec<String>> = Vec::with_capacity(lines_count);
279 for qubit in &visual.qubits {
280 start_lines.push(vec![empty_line.clone()]);
281 let q_label = format!("Q{}", qubit.id());
282 let wire_head = BoxChar::LEFT_RIGHT.repeat(2);
283 start_lines.push(vec![format!(
284 " {:>width$}: {}{}",
285 q_label,
286 initial_prefix,
287 wire_head,
288 width = qubit_len + 1
289 )]);
290 }
291 start_lines.push(vec![empty_line]);
292 lines.push(start_lines);
293
294 for moment in generate_moments(visual) {
295 let mut moment_lines: Vec<Vec<String>> = (0..lines_count)
296 .map(|i| {
297 if i % 2 == 1 {
298 vec![BoxChar::LEFT_RIGHT.to_string()]
299 } else {
300 vec![" ".to_string()]
301 }
302 })
303 .collect();
304
305 let columns = moment_to_columns(visual, &moment);
306 for column in columns.clone() {
307 let column_lines = draw_column(visual, &column, show_params);
308 for (line_i, line) in column_lines.into_iter().enumerate() {
309 moment_lines[line_i].extend(line);
310 }
311 }
312
313 for (i, row) in moment_lines.iter_mut().enumerate() {
314 row.push(if i % 2 == 1 {
315 BoxChar::LEFT_RIGHT.to_string()
316 } else {
317 " ".to_string()
318 });
319 }
320
321 let col_len = columns.len();
322 if col_len > 1 {
323 let s =
324 BoxChar::LEFT_RIGHT.repeat(str_len(&moment_lines[0].concat()).saturating_sub(2));
325 moment_lines[0] = vec![
326 BoxChar::BOTTOM_RIGHT.to_string(),
327 s.clone(),
328 BoxChar::BOTTOM_LEFT.to_string(),
329 ];
330 moment_lines[lines_count - 1] = vec![
331 BoxChar::TOP_RIGHT.to_string(),
332 s,
333 BoxChar::TOP_LEFT.to_string(),
334 ];
335 }
336 lines.push(moment_lines);
337 }
338
339 lines
340}
341
342fn generate_moments(visual: &VisualCircuit) -> Vec<Vec<usize>> {
343 let mut layers = vec![Vec::new(); visual.num_columns.max(1)];
344 for (op_idx, op) in visual.operations.iter().enumerate() {
345 if op.column >= layers.len() {
346 layers.resize(op.column + 1, Vec::new());
347 }
348 layers[op.column].push(op_idx);
349 }
350 layers
351}
352
353fn moment_to_columns(visual: &VisualCircuit, moment: &[usize]) -> Vec<Vec<usize>> {
354 let mut ranges: Vec<(usize, usize, usize)> = Vec::new();
355 for op_idx in moment {
356 let op = &visual.operations[*op_idx];
357 if op.lanes.is_empty() {
358 continue;
359 }
360 let mut ys = op.lanes.iter().map(|lane| y_wire(*lane));
361 if let Some(first) = ys.next() {
362 let mut min_y = first;
363 let mut max_y = first;
364 for y in ys {
365 min_y = min_y.min(y);
366 max_y = max_y.max(y);
367 }
368 ranges.push((min_y, max_y, *op_idx));
369 }
370 }
371
372 let mut columns: Vec<Vec<(usize, usize, usize)>> = Vec::new();
373 for range in ranges {
374 let (min_y, max_y, _) = range;
375 let mut placed = false;
376 for column in &mut columns {
377 let overlaps = column
378 .iter()
379 .any(|(cmin, cmax, _)| max_y >= *cmin && min_y <= *cmax);
380 if !overlaps {
381 column.push(range);
382 placed = true;
383 break;
384 }
385 }
386 if !placed {
387 columns.push(vec![range]);
388 }
389 }
390
391 columns
392 .into_iter()
393 .map(|col| col.into_iter().map(|(_, _, op_idx)| op_idx).collect())
394 .collect()
395}
396
397fn draw_column(visual: &VisualCircuit, column: &[usize], show_params: bool) -> Vec<Vec<String>> {
398 let lines_count = total_rows(visual.num_qubits());
399 let mut lines: Vec<Vec<String>> = vec![Vec::new(); lines_count];
400
401 for op_idx in column {
402 let op = &visual.operations[*op_idx];
403 draw_operation(visual, op, &mut lines, show_params);
404 }
405
406 let mut max_width = 1usize;
407 for line in &lines {
408 for s in line {
409 max_width = max_width.max(str_len(s));
410 }
411 }
412
413 let empty_line = " ".repeat(max_width);
414 let wire_line = BoxChar::LEFT_RIGHT.repeat(max_width);
415 for (i, line) in lines.iter_mut().enumerate() {
416 if i % 2 == 0 {
417 if let Some(token) = line.first() {
418 *line = vec![center_fill(token, max_width, ' ')];
419 } else {
420 *line = vec![empty_line.clone()];
421 }
422 } else if let Some(token) = line.first() {
423 *line = vec![center_fill(
424 token,
425 max_width,
426 BoxChar::LEFT_RIGHT.chars().next().unwrap_or('─'),
427 )];
428 } else {
429 *line = vec![wire_line.clone()];
430 }
431 }
432
433 lines
434}
435
436fn draw_operation(
437 visual: &VisualCircuit,
438 op: &VisualOperation,
439 lines: &mut [Vec<String>],
440 show_params: bool,
441) {
442 let label = compose_label(op, show_params);
443 match op.style {
444 VisualOpStyle::Gate => {
445 if op.lanes.len() <= 1 {
446 if let Some(lane) = op.lanes.first().copied() {
447 let y = y_wire(lane);
448 lines[y].push(label);
449 }
450 return;
451 }
452
453 if op.label == "FSIM" {
454 draw_fsim(lines, op);
455 return;
456 }
457
458 let mark_targets =
459 op.label == "UNITARY" || matches!(op.label.as_str(), "RXX" | "RYY" | "RZX" | "RZZ");
460 if op.span_box || mark_targets {
461 draw_span_box(lines, &op.lanes, &label, mark_targets);
462 return;
463 }
464
465 for lane in &op.lanes {
466 lines[y_wire(*lane)].push(label.clone());
467 }
468 draw_connect_vertical(lines, &op.covered_lanes);
469 }
470 VisualOpStyle::Controlled { num_controls } => {
471 for lane in op.lanes.iter().take(num_controls) {
472 lines[y_wire(*lane)].push(BoxChar::DOT.to_string());
473 }
474 for lane in op.lanes.iter().skip(num_controls) {
475 lines[y_wire(*lane)].push(label.clone());
476 }
477 draw_connect_vertical(lines, &op.covered_lanes);
478 }
479 VisualOpStyle::Cz => {
480 for lane in &op.lanes {
481 lines[y_wire(*lane)].push(BoxChar::DOT.to_string());
482 }
483 draw_connect_vertical(lines, &op.covered_lanes);
484 }
485 VisualOpStyle::Swap => {
486 for lane in &op.lanes {
487 lines[y_wire(*lane)].push(BoxChar::CONNECT.to_string());
488 }
489 draw_connect_vertical(lines, &op.covered_lanes);
490 }
491 VisualOpStyle::Barrier => {
492 let lanes: Vec<usize> = if op.lanes.is_empty() {
493 (0..visual.num_qubits()).collect()
494 } else {
495 op.lanes.clone()
496 };
497 for lane in lanes {
498 let y = y_wire(lane);
499 if y > 1 && y - 1 < lines.len() {
500 lines[y - 1].push(BoxChar::TOP_BOTTOM.to_string());
501 }
502 if y < lines.len() {
503 lines[y].push(BoxChar::TOP_BOTTOM.to_string());
504 }
505 }
506 }
507 VisualOpStyle::Measure | VisualOpStyle::Reset | VisualOpStyle::Delay => {
508 for lane in &op.lanes {
509 lines[y_wire(*lane)].push(label.clone());
510 }
511 }
512 VisualOpStyle::ControlFlow { .. } => {
513 if is_control_flow_marker(op) {
514 let span_lanes: &[usize] = if op.covered_lanes.is_empty() {
515 &op.lanes
516 } else {
517 &op.covered_lanes
518 };
519 draw_control_flow_marker(lines, span_lanes, &op.label);
520 }
521 }
522 }
523}
524
525fn draw_control_flow_marker(lines: &mut [Vec<String>], lanes: &[usize], label: &str) {
526 if lanes.is_empty() {
527 return;
528 }
529 let q_indices: Vec<usize> = lanes.iter().map(|lane| y_wire(*lane)).collect();
530 if q_indices.is_empty() {
531 return;
532 }
533 let min_idx = q_indices.iter().copied().min().unwrap_or(0);
534 let max_idx = q_indices.iter().copied().max().unwrap_or(min_idx);
535 let top_line = min_idx.saturating_sub(1);
536 let bottom_line = (max_idx + 1).min(lines.len().saturating_sub(1));
537 if bottom_line <= top_line {
538 lines[min_idx].push(label.to_string());
539 return;
540 }
541
542 let inner_width = (str_len(label) + 2).max(6);
543 lines[top_line].push(format!(
544 "{}{}{}",
545 BoxChar::BOTTOM_RIGHT,
546 BoxChar::LEFT_RIGHT.repeat(inner_width),
547 BoxChar::BOTTOM_LEFT
548 ));
549 lines[bottom_line].push(format!(
550 "{}{}{}",
551 BoxChar::TOP_RIGHT,
552 BoxChar::LEFT_RIGHT.repeat(inner_width),
553 BoxChar::TOP_LEFT
554 ));
555
556 let label_y = select_label_row(top_line, bottom_line);
557
558 #[allow(clippy::needless_range_loop)]
559 for y in (top_line + 1)..bottom_line {
560 let is_wire = y % 2 == 1;
561 let fill = ' ';
562 let mut body = fill.to_string().repeat(inner_width);
563 if y == label_y {
564 body = overlay_text_center(&body, label);
565 }
566 let left_side = if is_wire {
567 BoxChar::TOP_BOTTOM_LEFT
568 } else {
569 BoxChar::TOP_BOTTOM
570 };
571 let right_side = if is_wire {
572 BoxChar::TOP_BOTTOM_RIGHT
573 } else {
574 BoxChar::TOP_BOTTOM
575 };
576 lines[y].push(format!("{left_side}{body}{right_side}"));
577 }
578}
579
580fn is_control_flow_marker(op: &VisualOperation) -> bool {
581 matches!(op.style, VisualOpStyle::ControlFlow { .. })
582 && op.children.is_none()
583 && matches!(
584 op.style,
585 VisualOpStyle::ControlFlow {
586 kind: VisualControlFlowKind::IfStart
587 | VisualControlFlowKind::ElseStart
588 | VisualControlFlowKind::WhileStart
589 | VisualControlFlowKind::ForStart
590 | VisualControlFlowKind::SwitchStart
591 | VisualControlFlowKind::CaseStart
592 | VisualControlFlowKind::DefaultStart
593 | VisualControlFlowKind::Break
594 | VisualControlFlowKind::Continue
595 | VisualControlFlowKind::End
596 }
597 )
598}
599
600fn draw_connect_vertical(lines: &mut [Vec<String>], lanes: &[usize]) {
601 if lanes.is_empty() {
602 return;
603 }
604 let mut ys = lanes.iter().map(|lane| y_wire(*lane));
605 let Some(first) = ys.next() else {
606 return;
607 };
608 let mut min_y = first;
609 let mut max_y = first;
610 for y in ys {
611 min_y = min_y.min(y);
612 max_y = max_y.max(y);
613 }
614
615 #[allow(clippy::needless_range_loop)]
616 for y in (min_y + 1)..max_y {
617 if y % 2 == 1 {
618 lines[y].push(BoxChar::TOP_BOTTOM_LEFT_RIGHT.to_string());
619 } else {
620 lines[y].push(BoxChar::TOP_BOTTOM.to_string());
621 }
622 }
623}
624
625fn draw_fsim(lines: &mut [Vec<String>], op: &VisualOperation) {
626 let span_lanes: &[usize] = if op.covered_lanes.is_empty() {
627 &op.lanes
628 } else {
629 &op.covered_lanes
630 };
631 if span_lanes.is_empty() {
632 return;
633 }
634
635 let mut ys = span_lanes.iter().map(|lane| y_wire(*lane));
636 let Some(first) = ys.next() else {
637 return;
638 };
639 let mut min_y = first;
640 let mut max_y = first;
641 for y in ys {
642 min_y = min_y.min(y);
643 max_y = max_y.max(y);
644 }
645
646 let block_width = 7usize;
647 let mid = block_width / 2;
648
649 let end_token = center_fill("FSIM", block_width, '─');
651 for lane in &op.lanes {
652 lines[y_wire(*lane)].push(end_token.clone());
653 }
654
655 #[allow(clippy::needless_range_loop)]
656 for y in (min_y + 1)..max_y {
657 let base_char = if y % 2 == 1 {
658 BoxChar::TOP_BOTTOM_LEFT_RIGHT.chars().next().unwrap_or('┼')
659 } else {
660 BoxChar::TOP_BOTTOM.chars().next().unwrap_or('│')
661 };
662
663 let mut token_chars: Vec<char> = if y % 2 == 1 {
664 vec!['─'; block_width]
665 } else {
666 vec![' '; block_width]
667 };
668 token_chars[mid] = base_char;
669 lines[y].push(token_chars.into_iter().collect());
670 }
671}
672
673fn draw_span_box(lines: &mut [Vec<String>], lanes: &[usize], label: &str, mark_targets: bool) {
674 let q_indices: Vec<usize> = lanes.iter().map(|lane| y_wire(*lane)).collect();
675 if q_indices.is_empty() {
676 return;
677 }
678 let min_idx = q_indices.iter().copied().min().unwrap_or(0);
679 let max_idx = q_indices.iter().copied().max().unwrap_or(min_idx);
680 let width = (str_len(label) + 4).max(7);
681
682 let label_bar = center_fill(label, width - 2, ' ');
683 let top_border = format!(
684 "{}{}{}",
685 BoxChar::BOTTOM_RIGHT,
686 BoxChar::LEFT_RIGHT.repeat(width - 2),
687 BoxChar::BOTTOM_LEFT
688 );
689 let bottom_border = format!(
690 "{}{}{}",
691 BoxChar::TOP_RIGHT,
692 BoxChar::LEFT_RIGHT.repeat(width - 2),
693 BoxChar::TOP_LEFT
694 );
695 let mid_blank = format!(
696 "{}{}{}",
697 BoxChar::TOP_BOTTOM,
698 " ".repeat(width - 2),
699 BoxChar::TOP_BOTTOM
700 );
701 let mid_label = format!(
702 "{}{}{}",
703 BoxChar::TOP_BOTTOM,
704 label_bar,
705 BoxChar::TOP_BOTTOM
706 );
707
708 let top_line = min_idx.saturating_sub(1);
709 let bottom_line = (max_idx + 1).min(lines.len().saturating_sub(1));
710 let center_line = (top_line + bottom_line) / 2;
711
712 lines[top_line].push(top_border);
713 lines[bottom_line].push(bottom_border);
714
715 let mut markers: Vec<(usize, String)> = Vec::new();
716 if mark_targets {
717 let mut uniq = Vec::new();
718 for q in q_indices {
719 if !uniq.contains(&q) {
720 uniq.push(q);
721 }
722 }
723 for (idx, qline) in uniq.into_iter().enumerate() {
724 markers.push((qline, idx.to_string()));
725 }
726 }
727
728 for (idx, line) in lines
729 .iter_mut()
730 .enumerate()
731 .take(bottom_line)
732 .skip(top_line + 1)
733 {
734 let mut content = if idx == center_line {
735 mid_label.clone()
736 } else {
737 mid_blank.clone()
738 };
739 if idx != center_line
740 && let Some((_, marker)) = markers.iter().find(|(q, _)| *q == idx)
741 {
742 content = with_marker(&content, marker);
743 }
744 line.push(content);
745 }
746}
747
748fn select_label_row(top_line: usize, bottom_line: usize) -> usize {
749 let mid = (top_line + bottom_line) / 2;
750 if mid > top_line && mid < bottom_line && mid & 1 == 0 {
751 return mid;
752 }
753 if mid > top_line + 1 {
754 let upper = mid - 1;
755 if upper & 1 == 0 {
756 return upper;
757 }
758 }
759 if mid + 1 < bottom_line {
760 let lower = mid + 1;
761 if lower & 1 == 0 {
762 return lower;
763 }
764 }
765 if mid > top_line && mid < bottom_line {
766 return mid;
767 }
768 top_line.saturating_add(1).min(bottom_line)
769}
770
771fn overlay_text_center(base: &str, text: &str) -> String {
772 let mut chars: Vec<char> = base.chars().collect();
773 if chars.is_empty() {
774 return text.to_string();
775 }
776 let text_chars: Vec<char> = text.chars().collect();
777 if text_chars.len() > chars.len() {
778 return text.to_string();
779 }
780 let text_len = text_chars.len();
781 let start = (chars.len() - text_len) / 2;
782 for (i, ch) in text_chars.into_iter().enumerate() {
783 chars[start + i] = ch;
784 }
785 chars.into_iter().collect()
786}
787
788fn with_marker(base: &str, marker: &str) -> String {
789 let chars: Vec<char> = base.chars().collect();
790 if chars.len() < 3 {
791 return base.to_string();
792 }
793 let marker_chars: Vec<char> = marker.chars().collect();
794 let mut out = Vec::with_capacity(chars.len());
795 out.push(chars[0]);
796 for ch in marker_chars {
797 out.push(ch);
798 }
799 let start = marker.len() + 1;
800 if start < chars.len() {
801 out.extend_from_slice(&chars[start..]);
802 }
803 out.into_iter().collect()
804}
805
806fn compose_label(op: &VisualOperation, show_params: bool) -> String {
807 if matches!(op.style, VisualOpStyle::Reset) {
808 return "|0>".to_string();
809 }
810 if show_params && !op.params.is_empty() {
811 format!("{}({})", op.label, op.params.join(","))
812 } else {
813 op.label.clone()
814 }
815}
816
817fn str_len(s: &str) -> usize {
818 s.chars().count()
819}
820
821fn center_fill(text: &str, width: usize, fill: char) -> String {
822 let len = str_len(text);
823 if len >= width {
824 return text.to_string();
825 }
826 let pad = width - len;
827 let left = pad / 2;
828 let right = pad - left;
829 format!(
830 "{}{}{}",
831 fill.to_string().repeat(left),
832 text,
833 fill.to_string().repeat(right)
834 )
835}