From a4006557b4b0fcd0bacd14c5c1142eeecefbaa76 Mon Sep 17 00:00:00 2001 From: morcifer Date: Sat, 1 Feb 2025 17:00:59 +0100 Subject: [PATCH 1/7] How this code should have looked in the first place --- compiler/qsc_circuit/src/circuit.rs | 136 ++++++++++++---------------- 1 file changed, 56 insertions(+), 80 deletions(-) diff --git a/compiler/qsc_circuit/src/circuit.rs b/compiler/qsc_circuit/src/circuit.rs index 508845f3e0..6460da2eb0 100644 --- a/compiler/qsc_circuit/src/circuit.rs +++ b/compiler/qsc_circuit/src/circuit.rs @@ -110,6 +110,8 @@ enum Wire { } enum CircuitObject { + Blank, + Wire, WireCross, WireStart, DashedCross, @@ -192,24 +194,22 @@ impl Row { for column in 1..end_column { let val = self.objects.get(&column); let column_width = *column_widths.get(&column).unwrap_or(&MIN_COLUMN_WIDTH); - if let Some(v) = val { - s.write_str(&fmt_qubit_circuit_object(v, column_width))?; - } else { - s.write_str(&get_qubit_wire(column_width))?; - } + let object = val.unwrap_or(&CircuitObject::Wire); + + s.write_str(&fmt_qubit_circuit_object(object, column_width))?; } } Wire::Classical { start_column } => { for column in 0..end_column { let val = self.objects.get(&column); let column_width = *column_widths.get(&column).unwrap_or(&MIN_COLUMN_WIDTH); - if let Some(v) = val { - s.write_str(&fmt_classical_circuit_object(v, column_width))?; - } else if start_column.map_or(false, |s| column > s) { - s.write_str(&get_classical_wire(column_width))?; - } else { - s.write_str(&get_blank(column_width))?; - } + let object = match (val, start_column) { + (Some(v), _) => v, + (None, Some(s)) if column > *s => &CircuitObject::Wire, + _ => &CircuitObject::Blank, + }; + + s.write_str(&fmt_classical_circuit_object(object, column_width))?; } } } @@ -220,63 +220,23 @@ impl Row { const MIN_COLUMN_WIDTH: usize = 7; -/// "───────" -fn get_qubit_wire(column_width: usize) -> String { - "─".repeat(column_width) -} - -/// "═══════" -fn get_classical_wire(column_width: usize) -> String { - "═".repeat(column_width) -} +fn expand_template(template: &[char; 3], column_width: usize) -> String { + let left = template[0].to_string().repeat(column_width / 2); + let right = template[2].to_string().repeat(column_width / 2); -/// "───┼───" -fn get_qubit_wire_cross(column_width: usize) -> String { - let half_width = "─".repeat(column_width / 2); - format!("{}┼{}", half_width, half_width) + format!("{left}{}{right}", template[1]) } -/// "═══╪═══" -fn get_classical_wire_cross(column_width: usize) -> String { - let half_width = "═".repeat(column_width / 2); - format!("{}╪{}", half_width, half_width) -} - -/// " ╘═══" -fn get_classical_wire_start(column_width: usize) -> String { - let first_half_width = " ".repeat(column_width / 2); - let second_half_width = "═".repeat(column_width / 2); - format!("{}╘{}", first_half_width, second_half_width) -} - -/// "───┆───" -fn get_qubit_wire_dashed_cross(column_width: usize) -> String { - let half_width = "─".repeat(column_width / 2); - format!("{}┆{}", half_width, half_width) -} - -/// "═══┆═══" -fn get_classical_wire_dashed_cross(column_width: usize) -> String { - let half_width = "═".repeat(column_width / 2); - format!("{}┆{}", half_width, half_width) -} - -/// " │ " -fn get_vertical(column_width: usize) -> String { - let half_width = " ".repeat(column_width / 2); - format!("{}│{}", half_width, half_width) -} - -/// " ┆ " -fn get_vertical_dashed(column_width: usize) -> String { - let half_width = " ".repeat(column_width / 2); - format!("{}┆{}", half_width, half_width) -} - -/// " " -fn get_blank(column_width: usize) -> String { - " ".repeat(column_width) -} +const QUBIT_WIRE: [char; 3] = ['─', '─', '─']; // -> "───────" +const CLASSICAL_WIRE: [char; 3] = ['═', '═', '═']; // -> "═══════" +const QUBIT_WIRE_CROSS: [char; 3] = ['─', '┼', '─']; // -> "───┼───" +const CLASSICAL_WIRE_CROSS: [char; 3] = ['═', '╪', '═']; // -> "═══╪═══" +const CLASSICAL_WIRE_START: [char; 3] = [' ', '╘', '═']; // -> " ╘═══" +const QUBIT_WIRE_DASHED_CROSS: [char; 3] = ['─', '┆', '─']; // -> "───┆───" +const CLASSICAL_WIRE_DASHED_CROSS: [char; 3] = ['═', '┆', '═']; // -> "═══┆═══" +const VERTICAL_DASHED: [char; 3] = [' ', '┆', ' ']; // -> " │ " +const VERTICAL: [char; 3] = [' ', '│', ' ']; // -> " ┆ " +const BLANK: [char; 3] = [' ', ' ', ' ']; // -> " " /// "q_0 " #[allow(clippy::doc_markdown)] @@ -296,25 +256,41 @@ fn fmt_on_classical_wire(obj: &str, column_width: usize) -> String { } fn fmt_classical_circuit_object(circuit_object: &CircuitObject, column_width: usize) -> String { - match circuit_object { - CircuitObject::WireCross => get_classical_wire_cross(column_width), - CircuitObject::WireStart => get_classical_wire_start(column_width), - CircuitObject::DashedCross => get_classical_wire_dashed_cross(column_width), - CircuitObject::Vertical => get_vertical(column_width), - CircuitObject::VerticalDashed => get_vertical_dashed(column_width), - CircuitObject::Object(label) => fmt_on_classical_wire(label.as_str(), column_width), + if let CircuitObject::Object(label) = circuit_object { + return fmt_on_classical_wire(label.as_str(), column_width); } + + let template = match circuit_object { + CircuitObject::Blank => BLANK, + CircuitObject::Wire => CLASSICAL_WIRE, + CircuitObject::WireCross => CLASSICAL_WIRE_CROSS, + CircuitObject::WireStart => CLASSICAL_WIRE_START, + CircuitObject::DashedCross => CLASSICAL_WIRE_DASHED_CROSS, + CircuitObject::Vertical => VERTICAL, + CircuitObject::VerticalDashed => VERTICAL_DASHED, + CircuitObject::Object(_) => unreachable!("This case is covered in the early return."), + }; + + expand_template(&template, column_width) } fn fmt_qubit_circuit_object(circuit_object: &CircuitObject, column_width: usize) -> String { - match circuit_object { - CircuitObject::WireCross => get_qubit_wire_cross(column_width), - CircuitObject::WireStart => get_blank(column_width), // This should never happen - CircuitObject::DashedCross => get_qubit_wire_dashed_cross(column_width), - CircuitObject::Vertical => get_vertical(column_width), - CircuitObject::VerticalDashed => get_vertical_dashed(column_width), - CircuitObject::Object(label) => fmt_on_qubit_wire(label.as_str(), column_width), + if let CircuitObject::Object(label) = circuit_object { + return fmt_on_qubit_wire(label.as_str(), column_width); } + + let template = match circuit_object { + CircuitObject::Blank => BLANK, + CircuitObject::Wire => QUBIT_WIRE, + CircuitObject::WireCross => QUBIT_WIRE_CROSS, + CircuitObject::WireStart => BLANK, // This should never happen + CircuitObject::DashedCross => QUBIT_WIRE_DASHED_CROSS, + CircuitObject::Vertical => VERTICAL, + CircuitObject::VerticalDashed => VERTICAL_DASHED, + CircuitObject::Object(_) => unreachable!("This case is covered in the early return."), + }; + + expand_template(&template, column_width) } impl Display for Circuit { From c14179907ed45ebd02e07199db85a401edaf61b1 Mon Sep 17 00:00:00 2001 From: morcifer Date: Sat, 1 Feb 2025 18:14:21 +0100 Subject: [PATCH 2/7] This actually is nicer --- compiler/qsc_circuit/src/circuit.rs | 162 +++++++++++++++------------- 1 file changed, 90 insertions(+), 72 deletions(-) diff --git a/compiler/qsc_circuit/src/circuit.rs b/compiler/qsc_circuit/src/circuit.rs index 6460da2eb0..416bfeeb5e 100644 --- a/compiler/qsc_circuit/src/circuit.rs +++ b/compiler/qsc_circuit/src/circuit.rs @@ -96,7 +96,7 @@ impl Config { } type ObjectsByColumn = FxHashMap; -type ColumnWidthsByColumn = FxHashMap; +type ColumnWidthsByColumn = FxHashMap; struct Row { wire: Wire, @@ -183,33 +183,39 @@ impl Row { fn fmt( &self, f: &mut std::fmt::Formatter<'_>, - column_widths: &ColumnWidthsByColumn, + column_renderers: &ColumnWidthsByColumn, end_column: usize, ) -> std::fmt::Result { // Temporary string so we can trim whitespace at the end + let default_renderer = ColumnRenderer { + column_width: MIN_COLUMN_WIDTH, + }; + let mut s = String::new(); match &self.wire { Wire::Qubit { q_id: label } => { s.write_str(&fmt_qubit_label(*label))?; for column in 1..end_column { let val = self.objects.get(&column); - let column_width = *column_widths.get(&column).unwrap_or(&MIN_COLUMN_WIDTH); + let renderer = column_renderers.get(&column).unwrap_or(&default_renderer); + let object = val.unwrap_or(&CircuitObject::Wire); - s.write_str(&fmt_qubit_circuit_object(object, column_width))?; + s.write_str(&renderer.fmt_qubit_circuit_object(object))?; } } Wire::Classical { start_column } => { for column in 0..end_column { let val = self.objects.get(&column); - let column_width = *column_widths.get(&column).unwrap_or(&MIN_COLUMN_WIDTH); + let renderer = column_renderers.get(&column).unwrap_or(&default_renderer); + let object = match (val, start_column) { (Some(v), _) => v, (None, Some(s)) if column > *s => &CircuitObject::Wire, _ => &CircuitObject::Blank, }; - s.write_str(&fmt_classical_circuit_object(object, column_width))?; + s.write_str(&renderer.fmt_classical_circuit_object(object))?; } } } @@ -220,23 +226,16 @@ impl Row { const MIN_COLUMN_WIDTH: usize = 7; -fn expand_template(template: &[char; 3], column_width: usize) -> String { - let left = template[0].to_string().repeat(column_width / 2); - let right = template[2].to_string().repeat(column_width / 2); - - format!("{left}{}{right}", template[1]) -} - -const QUBIT_WIRE: [char; 3] = ['─', '─', '─']; // -> "───────" -const CLASSICAL_WIRE: [char; 3] = ['═', '═', '═']; // -> "═══════" -const QUBIT_WIRE_CROSS: [char; 3] = ['─', '┼', '─']; // -> "───┼───" -const CLASSICAL_WIRE_CROSS: [char; 3] = ['═', '╪', '═']; // -> "═══╪═══" -const CLASSICAL_WIRE_START: [char; 3] = [' ', '╘', '═']; // -> " ╘═══" -const QUBIT_WIRE_DASHED_CROSS: [char; 3] = ['─', '┆', '─']; // -> "───┆───" -const CLASSICAL_WIRE_DASHED_CROSS: [char; 3] = ['═', '┆', '═']; // -> "═══┆═══" -const VERTICAL_DASHED: [char; 3] = [' ', '┆', ' ']; // -> " │ " -const VERTICAL: [char; 3] = [' ', '│', ' ']; // -> " ┆ " -const BLANK: [char; 3] = [' ', ' ', ' ']; // -> " " +const QUBIT_WIRE: [char; 3] = ['─', '─', '─']; // "───────" +const CLASSICAL_WIRE: [char; 3] = ['═', '═', '═']; // "═══════" +const QUBIT_WIRE_CROSS: [char; 3] = ['─', '┼', '─']; // "───┼───" +const CLASSICAL_WIRE_CROSS: [char; 3] = ['═', '╪', '═']; // "═══╪═══" +const CLASSICAL_WIRE_START: [char; 3] = [' ', '╘', '═']; // " ╘═══" +const QUBIT_WIRE_DASHED_CROSS: [char; 3] = ['─', '┆', '─']; // "───┆───" +const CLASSICAL_WIRE_DASHED_CROSS: [char; 3] = ['═', '┆', '═']; // "═══┆═══" +const VERTICAL_DASHED: [char; 3] = [' ', '┆', ' ']; // " │ " +const VERTICAL: [char; 3] = [' ', '│', ' ']; // " ┆ " +const BLANK: [char; 3] = [' ', ' ', ' ']; // " " /// "q_0 " #[allow(clippy::doc_markdown)] @@ -245,52 +244,68 @@ fn fmt_qubit_label(id: usize) -> String { format!("q_{id: String { - format!("{:─^column_width$}", format!(" {obj} ")) +struct ColumnRenderer { + column_width: usize, } -/// "══ A ══" -fn fmt_on_classical_wire(obj: &str, column_width: usize) -> String { - format!("{:═^column_width$}", format!(" {obj} ")) -} +impl ColumnRenderer { + /// "── A ──" + fn fmt_on_qubit_wire(&self, obj: &str) -> String { + let column_width = self.column_width; + format!("{:─^column_width$}", format!(" {obj} ")) + } -fn fmt_classical_circuit_object(circuit_object: &CircuitObject, column_width: usize) -> String { - if let CircuitObject::Object(label) = circuit_object { - return fmt_on_classical_wire(label.as_str(), column_width); + /// "══ A ══" + fn fmt_on_classical_wire(&self, obj: &str) -> String { + let column_width = self.column_width; + format!("{:═^column_width$}", format!(" {obj} ")) } - let template = match circuit_object { - CircuitObject::Blank => BLANK, - CircuitObject::Wire => CLASSICAL_WIRE, - CircuitObject::WireCross => CLASSICAL_WIRE_CROSS, - CircuitObject::WireStart => CLASSICAL_WIRE_START, - CircuitObject::DashedCross => CLASSICAL_WIRE_DASHED_CROSS, - CircuitObject::Vertical => VERTICAL, - CircuitObject::VerticalDashed => VERTICAL_DASHED, - CircuitObject::Object(_) => unreachable!("This case is covered in the early return."), - }; - - expand_template(&template, column_width) -} + fn expand_template(&self, template: &[char; 3]) -> String { + let half_width = self.column_width / 2; + let left = template[0].to_string().repeat(half_width); + let right = template[2].to_string().repeat(half_width); + + format!("{left}{}{right}", template[1]) + } + + fn fmt_classical_circuit_object(&self, circuit_object: &CircuitObject) -> String { + if let CircuitObject::Object(label) = circuit_object { + return self.fmt_on_classical_wire(label.as_str()); + } -fn fmt_qubit_circuit_object(circuit_object: &CircuitObject, column_width: usize) -> String { - if let CircuitObject::Object(label) = circuit_object { - return fmt_on_qubit_wire(label.as_str(), column_width); + let template = match circuit_object { + CircuitObject::Blank => BLANK, + CircuitObject::Wire => CLASSICAL_WIRE, + CircuitObject::WireCross => CLASSICAL_WIRE_CROSS, + CircuitObject::WireStart => CLASSICAL_WIRE_START, + CircuitObject::DashedCross => CLASSICAL_WIRE_DASHED_CROSS, + CircuitObject::Vertical => VERTICAL, + CircuitObject::VerticalDashed => VERTICAL_DASHED, + CircuitObject::Object(_) => unreachable!("This case is covered in the early return."), + }; + + self.expand_template(&template) } - let template = match circuit_object { - CircuitObject::Blank => BLANK, - CircuitObject::Wire => QUBIT_WIRE, - CircuitObject::WireCross => QUBIT_WIRE_CROSS, - CircuitObject::WireStart => BLANK, // This should never happen - CircuitObject::DashedCross => QUBIT_WIRE_DASHED_CROSS, - CircuitObject::Vertical => VERTICAL, - CircuitObject::VerticalDashed => VERTICAL_DASHED, - CircuitObject::Object(_) => unreachable!("This case is covered in the early return."), - }; - - expand_template(&template, column_width) + fn fmt_qubit_circuit_object(&self, circuit_object: &CircuitObject) -> String { + if let CircuitObject::Object(label) = circuit_object { + return self.fmt_on_qubit_wire(label.as_str()); + } + + let template = match circuit_object { + CircuitObject::Blank => BLANK, + CircuitObject::Wire => QUBIT_WIRE, + CircuitObject::WireCross => QUBIT_WIRE_CROSS, + CircuitObject::WireStart => BLANK, // This should never happen + CircuitObject::DashedCross => QUBIT_WIRE_DASHED_CROSS, + CircuitObject::Vertical => VERTICAL, + CircuitObject::VerticalDashed => VERTICAL_DASHED, + CircuitObject::Object(_) => unreachable!("This case is covered in the early return."), + }; + + self.expand_template(&template) + } } impl Display for Circuit { @@ -407,26 +422,29 @@ impl Display for Circuit { // To be able to fit long-named operations, we calculate the required width for each column, // based on the maximum length needed for gates, where a gate X is printed as "- X -". - let column_widths = (0..end_column) + let column_renderers = (0..end_column) .map(|column| { ( column, - rows.iter() - .filter_map(|row| row.objects.get(&column)) - .filter_map(|object| match object { - CircuitObject::Object(string) => Some((string.len() + 4) | 1), // Column lengths need to be odd numbers - _ => None, - }) - .chain(std::iter::once(MIN_COLUMN_WIDTH)) - .max() - .unwrap(), + ColumnRenderer { + column_width: rows + .iter() + .filter_map(|row| row.objects.get(&column)) + .filter_map(|object| match object { + CircuitObject::Object(string) => Some((string.len() + 4) | 1), // Column lengths need to be odd numbers + _ => None, + }) + .chain(std::iter::once(MIN_COLUMN_WIDTH)) + .max() + .unwrap(), + }, ) }) .collect::(); // Draw the diagram for row in rows { - row.fmt(f, &column_widths, end_column)?; + row.fmt(f, &column_renderers, end_column)?; } Ok(()) From 4012ee8378f0080cf79b2220dead041ccc964edf Mon Sep 17 00:00:00 2001 From: morcifer Date: Sun, 2 Feb 2025 18:20:10 +0100 Subject: [PATCH 3/7] Minor simplification with new and default --- compiler/qsc_circuit/src/circuit.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/compiler/qsc_circuit/src/circuit.rs b/compiler/qsc_circuit/src/circuit.rs index 416bfeeb5e..a284e99f69 100644 --- a/compiler/qsc_circuit/src/circuit.rs +++ b/compiler/qsc_circuit/src/circuit.rs @@ -187,9 +187,7 @@ impl Row { end_column: usize, ) -> std::fmt::Result { // Temporary string so we can trim whitespace at the end - let default_renderer = ColumnRenderer { - column_width: MIN_COLUMN_WIDTH, - }; + let default_renderer = ColumnRenderer::default(); let mut s = String::new(); match &self.wire { @@ -248,7 +246,23 @@ struct ColumnRenderer { column_width: usize, } +impl Default for ColumnRenderer { + fn default() -> Self { + Self { + column_width: MIN_COLUMN_WIDTH, + } + } +} + impl ColumnRenderer { + fn new(column_width: usize) -> Self { + // Column widths should be odd numbers for the renderer to work well + let odd_column_width = column_width | 1; + Self { + column_width: odd_column_width, + } + } + /// "── A ──" fn fmt_on_qubit_wire(&self, obj: &str) -> String { let column_width = self.column_width; @@ -426,18 +440,17 @@ impl Display for Circuit { .map(|column| { ( column, - ColumnRenderer { - column_width: rows - .iter() + ColumnRenderer::new( + rows.iter() .filter_map(|row| row.objects.get(&column)) .filter_map(|object| match object { - CircuitObject::Object(string) => Some((string.len() + 4) | 1), // Column lengths need to be odd numbers + CircuitObject::Object(string) => Some(string.len() + 4), _ => None, }) .chain(std::iter::once(MIN_COLUMN_WIDTH)) .max() .unwrap(), - }, + ), ) }) .collect::(); From 99910c5d289dcb2218734149d4a565add48ce0c6 Mon Sep 17 00:00:00 2001 From: morcifer Date: Sun, 9 Feb 2025 14:15:14 +0100 Subject: [PATCH 4/7] Inefficient in terms of complexity, but works as expected --- compiler/qsc/src/interpret/circuit_tests.rs | 87 ++++++++++++++++++++- compiler/qsc_circuit/src/circuit.rs | 15 +++- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/compiler/qsc/src/interpret/circuit_tests.rs b/compiler/qsc/src/interpret/circuit_tests.rs index f3188cba2d..11e500d08a 100644 --- a/compiler/qsc/src/interpret/circuit_tests.rs +++ b/compiler/qsc/src/interpret/circuit_tests.rs @@ -490,18 +490,25 @@ fn custom_intrinsic_mixed_args() { .circuit(CircuitEntryPoint::EntryPoint, false) .expect("circuit generation should succeed"); - // This is one gate that spans ten target wires, even though the - // text visualization doesn't convey that clearly. expect![[r" q_0 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_1 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_2 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_3 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_4 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_5 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_6 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_7 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_8 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_9 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── "]] .assert_eq(&circ.to_string()); @@ -887,6 +894,82 @@ fn operation_with_long_gates_properly_aligned() { .assert_eq(&circ.to_string()); } +#[test] +fn operation_with_subsequent_qubits_gets_horizontal_lines() { + let mut interpreter = interpreter( + r" + namespace Test { + import Std.Measurement.*; + + @EntryPoint() + operation Main() : Unit { + use q0 = Qubit(); + use q1 = Qubit(); + Rxx(1.0, q0, q1); + + use q2 = Qubit(); + use q3 = Qubit(); + Rxx(1.0, q2, q3); + } + } + ", + Profile::Unrestricted, + ); + + let circ = interpreter + .circuit(CircuitEntryPoint::EntryPoint, false) + .expect("circuit generation should succeed"); + + expect![[r#" + q_0 ─ rxx(1.0000) ─ + ┆ + q_1 ─ rxx(1.0000) ─ + q_2 ─ rxx(1.0000) ─ + ┆ + q_3 ─ rxx(1.0000) ─ + "#]] + .assert_eq(&circ.to_string()); +} + +#[test] +fn operation_with_subsequent_qubits_no_empty_rows() { + let mut interpreter = interpreter( + r" + namespace Test { + import Std.Measurement.*; + + @EntryPoint() + operation Main() : Result[] { + use q0 = Qubit(); + use q1 = Qubit(); + Rxx(1.0, q0, q1); + + use q2 = Qubit(); + use q3 = Qubit(); + Rxx(1.0, q2, q3); + + [M(q0), M(q2)] + } + } + ", + Profile::Unrestricted, + ); + + let circ = interpreter + .circuit(CircuitEntryPoint::EntryPoint, false) + .expect("circuit generation should succeed"); + + expect![[r#" + q_0 ─ rxx(1.0000) ─── M ── + ┆ ╘═══ + q_1 ─ rxx(1.0000) ──────── + q_2 ─ rxx(1.0000) ─── M ── + ┆ ╘═══ + q_3 ─ rxx(1.0000) ──────── + "#]] + .assert_eq(&circ.to_string()); +} + /// Tests that invoke circuit generation throught the debugger. mod debugger_stepping { use super::Debugger; diff --git a/compiler/qsc_circuit/src/circuit.rs b/compiler/qsc_circuit/src/circuit.rs index a284e99f69..b79fee12d2 100644 --- a/compiler/qsc_circuit/src/circuit.rs +++ b/compiler/qsc_circuit/src/circuit.rs @@ -6,7 +6,7 @@ mod tests; use rustc_hash::FxHashMap; use serde::Serialize; -use std::{fmt::Display, fmt::Write, ops::Not, vec}; +use std::{cmp, fmt::Display, fmt::Write, ops::Not, vec}; /// Representation of a quantum circuit. /// Implementation of @@ -340,7 +340,18 @@ impl Display for Circuit { register_to_row.insert((q.id, None), rows.len() - 1); - for i in 0..q.num_children { + // If this qubit has no children, but it is in a multi-qubit operation with + // the next qubit, we will want to add one classical row for clarity. + let extra_row = self + .operations + .iter() + .filter(|o| { + o.targets.iter().any(|t| t.q_id == q.id) + && o.targets.iter().any(|t| t.q_id == q.id + 1) + }) + .count(); + + for i in 0..(cmp::max(q.num_children, extra_row)) { rows.push(Row { wire: Wire::Classical { start_column: None }, objects: FxHashMap::default(), From cfa7382630b83ee0aefd64ee414ca6b5b600f2d2 Mon Sep 17 00:00:00 2001 From: morcifer Date: Sun, 9 Feb 2025 14:15:14 +0100 Subject: [PATCH 5/7] Both efficient and correct (probably) --- compiler/qsc/src/interpret/circuit_tests.rs | 118 +++++++++++++++++++- compiler/qsc_circuit/src/circuit.rs | 34 +++++- 2 files changed, 147 insertions(+), 5 deletions(-) diff --git a/compiler/qsc/src/interpret/circuit_tests.rs b/compiler/qsc/src/interpret/circuit_tests.rs index f3188cba2d..53bb19d4cc 100644 --- a/compiler/qsc/src/interpret/circuit_tests.rs +++ b/compiler/qsc/src/interpret/circuit_tests.rs @@ -490,18 +490,25 @@ fn custom_intrinsic_mixed_args() { .circuit(CircuitEntryPoint::EntryPoint, false) .expect("circuit generation should succeed"); - // This is one gate that spans ten target wires, even though the - // text visualization doesn't convey that clearly. expect![[r" q_0 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_1 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_2 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_3 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_4 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_5 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_6 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_7 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_8 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── + ┆ q_9 ─ AccountForEstimatesInternal([(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 1) ── "]] .assert_eq(&circ.to_string()); @@ -887,6 +894,113 @@ fn operation_with_long_gates_properly_aligned() { .assert_eq(&circ.to_string()); } +#[test] +fn operation_with_subsequent_qubits_gets_horizontal_lines() { + let mut interpreter = interpreter( + r" + namespace Test { + import Std.Measurement.*; + + @EntryPoint() + operation Main() : Unit { + use q0 = Qubit(); + use q1 = Qubit(); + Rxx(1.0, q0, q1); + + use q2 = Qubit(); + use q3 = Qubit(); + Rxx(1.0, q2, q3); + } + } + ", + Profile::Unrestricted, + ); + + let circ = interpreter + .circuit(CircuitEntryPoint::EntryPoint, false) + .expect("circuit generation should succeed"); + + expect![[r#" + q_0 ─ rxx(1.0000) ─ + ┆ + q_1 ─ rxx(1.0000) ─ + q_2 ─ rxx(1.0000) ─ + ┆ + q_3 ─ rxx(1.0000) ─ + "#]] + .assert_eq(&circ.to_string()); +} + +#[test] +fn operation_with_subsequent_qubits_no_double_rows() { + let mut interpreter = interpreter( + r" + namespace Test { + import Std.Measurement.*; + + @EntryPoint() + operation Main() : Unit { + use q0 = Qubit(); + use q1 = Qubit(); + Rxx(1.0, q0, q1); + Rxx(1.0, q0, q1); + } + } + ", + Profile::Unrestricted, + ); + + let circ = interpreter + .circuit(CircuitEntryPoint::EntryPoint, false) + .expect("circuit generation should succeed"); + + expect![[r#" + q_0 ─ rxx(1.0000) ── rxx(1.0000) ─ + ┆ ┆ + q_1 ─ rxx(1.0000) ── rxx(1.0000) ─ + "#]] + .assert_eq(&circ.to_string()); +} + +#[test] +fn operation_with_subsequent_qubits_no_added_rows() { + let mut interpreter = interpreter( + r" + namespace Test { + import Std.Measurement.*; + + @EntryPoint() + operation Main() : Result[] { + use q0 = Qubit(); + use q1 = Qubit(); + Rxx(1.0, q0, q1); + + use q2 = Qubit(); + use q3 = Qubit(); + Rxx(1.0, q2, q3); + + [M(q0), M(q2)] + } + } + ", + Profile::Unrestricted, + ); + + let circ = interpreter + .circuit(CircuitEntryPoint::EntryPoint, false) + .expect("circuit generation should succeed"); + + expect![[r#" + q_0 ─ rxx(1.0000) ─── M ── + ┆ ╘═══ + q_1 ─ rxx(1.0000) ──────── + q_2 ─ rxx(1.0000) ─── M ── + ┆ ╘═══ + q_3 ─ rxx(1.0000) ──────── + "#]] + .assert_eq(&circ.to_string()); +} + /// Tests that invoke circuit generation throught the debugger. mod debugger_stepping { use super::Debugger; diff --git a/compiler/qsc_circuit/src/circuit.rs b/compiler/qsc_circuit/src/circuit.rs index a284e99f69..82f7b1b93b 100644 --- a/compiler/qsc_circuit/src/circuit.rs +++ b/compiler/qsc_circuit/src/circuit.rs @@ -4,9 +4,9 @@ #[cfg(test)] mod tests; -use rustc_hash::FxHashMap; +use rustc_hash::{FxHashMap, FxHashSet}; use serde::Serialize; -use std::{fmt::Display, fmt::Write, ops::Not, vec}; +use std::{cmp, fmt::Display, fmt::Write, ops::Not, vec}; /// Representation of a quantum circuit. /// Implementation of @@ -330,6 +330,26 @@ impl Display for Circuit { // to row in the diagram let mut register_to_row = FxHashMap::default(); + // Keep track of which qubits have the qubit after them in the same multi-qubit operation. + let mut consequtive_qubits = FxHashSet::default(); + + for operation in self.operations.iter() { + for target in operation.targets.iter() { + let qubit = target.q_id; + + if consequtive_qubits.contains(&qubit) { + continue; + } + + let next_qubit = qubit + 1; + + // Check if the next qubit is also in this operation. + if operation.targets.iter().any(|t| t.q_id == next_qubit) { + consequtive_qubits.insert(qubit); + } + } + } + // Initialize all qubit and classical wires for q in &self.qubits { rows.push(Row { @@ -340,7 +360,15 @@ impl Display for Circuit { register_to_row.insert((q.id, None), rows.len() - 1); - for i in 0..q.num_children { + // If this qubit has no children, but it is in a multi-qubit operation with + // the next qubit, we add one classical row for clarity. + let extra_rows = if consequtive_qubits.contains(&q.id) { + cmp::max(1, q.num_children) + } else { + q.num_children + }; + + for i in 0..extra_rows { rows.push(Row { wire: Wire::Classical { start_column: None }, objects: FxHashMap::default(), From 1d1e03960246d4d8edecf0565af76cf998d691f0 Mon Sep 17 00:00:00 2001 From: morcifer Date: Wed, 19 Feb 2025 23:07:46 +0100 Subject: [PATCH 6/7] Rework part 2 - 1.5 comments, one stone. --- compiler/qsc_circuit/src/circuit.rs | 40 +++++++++++------------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/compiler/qsc_circuit/src/circuit.rs b/compiler/qsc_circuit/src/circuit.rs index 5b084d5025..7cec0368ab 100644 --- a/compiler/qsc_circuit/src/circuit.rs +++ b/compiler/qsc_circuit/src/circuit.rs @@ -96,7 +96,6 @@ impl Config { } type ObjectsByColumn = FxHashMap; -type ColumnWidthsByColumn = FxHashMap; struct Row { wire: Wire, @@ -183,29 +182,23 @@ impl Row { fn fmt( &self, f: &mut std::fmt::Formatter<'_>, - column_renderers: &ColumnWidthsByColumn, - end_column: usize, + column_renderers: &[ColumnRenderer], ) -> std::fmt::Result { // Temporary string so we can trim whitespace at the end - let default_renderer = ColumnRenderer::default(); - let mut s = String::new(); match &self.wire { Wire::Qubit { q_id: label } => { s.write_str(&fmt_qubit_label(*label))?; - for column in 1..end_column { + for (column, renderer) in column_renderers.iter().enumerate().skip(1) { let val = self.objects.get(&column); - let renderer = column_renderers.get(&column).unwrap_or(&default_renderer); - let object = val.unwrap_or(&CircuitObject::Wire); s.write_str(&renderer.fmt_qubit_circuit_object(object))?; } } Wire::Classical { start_column } => { - for column in 0..end_column { + for (column, renderer) in column_renderers.iter().enumerate() { let val = self.objects.get(&column); - let renderer = column_renderers.get(&column).unwrap_or(&default_renderer); let object = match (val, start_column) { (Some(v), _) => v, @@ -468,26 +461,23 @@ impl Display for Circuit { // based on the maximum length needed for gates, where a gate X is printed as "- X -". let column_renderers = (0..end_column) .map(|column| { - ( - column, - ColumnRenderer::new( - rows.iter() - .filter_map(|row| row.objects.get(&column)) - .filter_map(|object| match object { - CircuitObject::Object(string) => Some(string.len() + 4), - _ => None, - }) - .chain(std::iter::once(MIN_COLUMN_WIDTH)) - .max() - .unwrap(), - ), + ColumnRenderer::new( + rows.iter() + .filter_map(|row| row.objects.get(&column)) + .filter_map(|object| match object { + CircuitObject::Object(string) => Some(string.len() + 4), + _ => None, + }) + .chain(std::iter::once(MIN_COLUMN_WIDTH)) + .max() + .unwrap(), ) }) - .collect::(); + .collect::>(); // Draw the diagram for row in rows { - row.fmt(f, &column_renderers, end_column)?; + row.fmt(f, &column_renderers)?; } Ok(()) From fc96b10bcea367402b24a32ed0d6c33e52e284fe Mon Sep 17 00:00:00 2001 From: morcifer Date: Sat, 22 Feb 2025 09:52:02 +0100 Subject: [PATCH 7/7] ColumnRenderer -> Column rename --- compiler/qsc_circuit/src/circuit.rs | 34 +++++++++++++---------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/compiler/qsc_circuit/src/circuit.rs b/compiler/qsc_circuit/src/circuit.rs index 7cec0368ab..2bcc9747d6 100644 --- a/compiler/qsc_circuit/src/circuit.rs +++ b/compiler/qsc_circuit/src/circuit.rs @@ -179,34 +179,30 @@ impl Row { self.next_column = column + 1; } - fn fmt( - &self, - f: &mut std::fmt::Formatter<'_>, - column_renderers: &[ColumnRenderer], - ) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>, columns: &[Column]) -> std::fmt::Result { // Temporary string so we can trim whitespace at the end let mut s = String::new(); match &self.wire { Wire::Qubit { q_id: label } => { s.write_str(&fmt_qubit_label(*label))?; - for (column, renderer) in column_renderers.iter().enumerate().skip(1) { - let val = self.objects.get(&column); + for (column_index, column) in columns.iter().enumerate().skip(1) { + let val = self.objects.get(&column_index); let object = val.unwrap_or(&CircuitObject::Wire); - s.write_str(&renderer.fmt_qubit_circuit_object(object))?; + s.write_str(&column.fmt_qubit_circuit_object(object))?; } } Wire::Classical { start_column } => { - for (column, renderer) in column_renderers.iter().enumerate() { - let val = self.objects.get(&column); + for (column_index, column) in columns.iter().enumerate() { + let val = self.objects.get(&column_index); let object = match (val, start_column) { (Some(v), _) => v, - (None, Some(s)) if column > *s => &CircuitObject::Wire, + (None, Some(s)) if column_index > *s => &CircuitObject::Wire, _ => &CircuitObject::Blank, }; - s.write_str(&renderer.fmt_classical_circuit_object(object))?; + s.write_str(&column.fmt_classical_circuit_object(object))?; } } } @@ -235,11 +231,11 @@ fn fmt_qubit_label(id: usize) -> String { format!("q_{id: Self { Self { column_width: MIN_COLUMN_WIDTH, @@ -247,9 +243,9 @@ impl Default for ColumnRenderer { } } -impl ColumnRenderer { +impl Column { fn new(column_width: usize) -> Self { - // Column widths should be odd numbers for the renderer to work well + // Column widths should be odd numbers for this struct to work well let odd_column_width = column_width | 1; Self { column_width: odd_column_width, @@ -459,9 +455,9 @@ impl Display for Circuit { // To be able to fit long-named operations, we calculate the required width for each column, // based on the maximum length needed for gates, where a gate X is printed as "- X -". - let column_renderers = (0..end_column) + let columns = (0..end_column) .map(|column| { - ColumnRenderer::new( + Column::new( rows.iter() .filter_map(|row| row.objects.get(&column)) .filter_map(|object| match object { @@ -477,7 +473,7 @@ impl Display for Circuit { // Draw the diagram for row in rows { - row.fmt(f, &column_renderers)?; + row.fmt(f, &columns)?; } Ok(())