diff --git a/compiler/qsc/src/interpret/circuit_tests.rs b/compiler/qsc/src/interpret/circuit_tests.rs index 821b60b345..f3188cba2d 100644 --- a/compiler/qsc/src/interpret/circuit_tests.rs +++ b/compiler/qsc/src/interpret/circuit_tests.rs @@ -148,10 +148,8 @@ fn rotation_gate() { .circuit(CircuitEntryPoint::EntryPoint, false) .expect("circuit generation should succeed"); - // The wire isn't visible here since the gate label is longer - // than the static column width, but we can live with it. expect![[r" - q_0 rx(1.5708) + q_0 ─ rx(1.5708) ── "]] .assert_eq(&circ.to_string()); } @@ -262,8 +260,8 @@ fn mresetz_unrestricted_profile() { .expect("circuit generation should succeed"); expect![[r" - q_0 ── H ──── M ─── |0〉 ─ - ╘══════════ + q_0 ── H ──── M ──── |0〉 ── + ╘════════════ "]] .assert_eq(&circ.to_string()); } @@ -349,10 +347,10 @@ fn unrestricted_profile_result_comparison() { .expect("circuit generation should succeed"); expect![[r" - q_0 ── H ──── M ──── X ─── |0〉 ─ - ╘═════════════════ - q_1 ── H ──── M ─── |0〉 ──────── - ╘═════════════════ + q_0 ── H ──── M ───── X ───── |0〉 ── + ╘═════════════════════ + q_1 ── H ──── M ──── |0〉 ─────────── + ╘═════════════════════ "]] .assert_eq(&circ.to_string()); @@ -366,10 +364,10 @@ fn unrestricted_profile_result_comparison() { let circuit = interpreter.get_circuit(); expect![[r" - q_0 ── H ──── M ──── X ─── |0〉 ─ - ╘═════════════════ - q_1 ── H ──── M ─── |0〉 ──────── - ╘═════════════════ + q_0 ── H ──── M ───── X ───── |0〉 ── + ╘═════════════════════ + q_1 ── H ──── M ──── |0〉 ─────────── + ╘═════════════════════ "]] .assert_eq(&circuit.to_string()); } @@ -456,10 +454,8 @@ fn custom_intrinsic_one_classical_arg() { .circuit(CircuitEntryPoint::EntryPoint, false) .expect("circuit generation should succeed"); - // A custom intrinsic that doesn't take qubits just doesn't - // show up on the circuit. expect![[r" - q_0 ── X ── foo(4) + q_0 ── X ─── foo(4) ── "]] .assert_eq(&circ.to_string()); } @@ -497,16 +493,16 @@ fn custom_intrinsic_mixed_args() { // 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) + 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()); @@ -536,7 +532,7 @@ fn custom_intrinsic_apply_idle_noise() { // ConfigurePauliNoise has no qubit arguments so it shouldn't show up. // ApplyIdleNoise is a quantum operation so it shows up. expect![[r#" - q_0 ApplyIdleNoise + q_0 ─ ApplyIdleNoise ── "#]] .assert_eq(&circ.to_string()); } @@ -834,6 +830,63 @@ fn operation_with_non_qubit_args() { .assert_debug_eq(&circ_err); } +#[test] +fn operation_with_long_gates_properly_aligned() { + let mut interpreter = interpreter( + r" + namespace Test { + import Std.Measurement.*; + + @EntryPoint() + operation Main() : Result[] { + use q0 = Qubit(); + use q1 = Qubit(); + + H(q0); + H(q1); + X(q1); + Ry(1.0, q1); + CNOT(q0, q1); + M(q0); + + use q2 = Qubit(); + + H(q2); + Rx(1.0, q2); + H(q2); + Rx(1.0, q2); + H(q2); + Rx(1.0, q2); + + use q3 = Qubit(); + + Rxx(1.0, q1, q3); + + CNOT(q0, q3); + + [M(q1), M(q3)] + } + } + ", + Profile::Unrestricted, + ); + + let circ = interpreter + .circuit(CircuitEntryPoint::EntryPoint, false) + .expect("circuit generation should succeed"); + + expect![[r#" + q_0 ── H ────────────────────────────────────── ● ──────── M ────────────────────────────────── ● ───────── + │ ╘════════════════════════════════════╪══════════ + q_1 ── H ──────── X ─────── ry(1.0000) ──────── X ───────────────────────────── rxx(1.0000) ────┼───── M ── + ┆ │ ╘═══ + q_2 ── H ─── rx(1.0000) ──────── H ─────── rx(1.0000) ──── H ─── rx(1.0000) ─────────┆──────────┼────────── + q_3 ─────────────────────────────────────────────────────────────────────────── rxx(1.0000) ─── X ──── M ── + ╘═══ + "#]] + .assert_eq(&circ.to_string()); +} + /// Tests that invoke circuit generation throught the debugger. mod debugger_stepping { use super::Debugger; @@ -915,17 +968,17 @@ mod debugger_stepping { step: q_0 ── H ── step: - q_0 ── H ──── Z ─────────────────────── - q_1 ── H ──── ● ──── H ──── M ─── |0〉 ─ - ╘══════════ + q_0 ── H ──── Z ───────────────────────── + q_1 ── H ──── ● ──── H ──── M ──── |0〉 ── + ╘════════════ step: - q_0 ── H ──── Z ─── |0〉 ─────────────── - q_1 ── H ──── ● ──── H ──── M ─── |0〉 ─ - ╘══════════ + q_0 ── H ──── Z ──── |0〉 ────────────────── + q_1 ── H ──── ● ───── H ───── M ──── |0〉 ── + ╘════════════ step: - q_0 ── H ──── Z ─── |0〉 ─────────────── - q_1 ── H ──── ● ──── H ──── M ─── |0〉 ─ - ╘══════════ + q_0 ── H ──── Z ──── |0〉 ────────────────── + q_1 ── H ──── ● ───── H ───── M ──── |0〉 ── + ╘════════════ "]] .assert_eq(&circs); } @@ -959,11 +1012,11 @@ mod debugger_stepping { q_0 ── H ──── M ── ╘═══ step: - q_0 ── H ──── M ─── |0〉 ─ - ╘══════════ + q_0 ── H ──── M ──── |0〉 ── + ╘════════════ step: - q_0 ── H ──── M ─── |0〉 ─ - ╘══════════ + q_0 ── H ──── M ──── |0〉 ── + ╘════════════ "]] .assert_eq(&circs); } diff --git a/compiler/qsc_circuit/src/circuit.rs b/compiler/qsc_circuit/src/circuit.rs index e9493b8bf5..508845f3e0 100644 --- a/compiler/qsc_circuit/src/circuit.rs +++ b/compiler/qsc_circuit/src/circuit.rs @@ -95,7 +95,8 @@ impl Config { pub const DEFAULT_MAX_OPERATIONS: usize = 10001; } -type ObjectsByColumn = FxHashMap; +type ObjectsByColumn = FxHashMap; +type ColumnWidthsByColumn = FxHashMap; struct Row { wire: Wire, @@ -108,16 +109,18 @@ enum Wire { Classical { start_column: Option }, } +enum CircuitObject { + WireCross, + WireStart, + DashedCross, + Vertical, + VerticalDashed, + Object(String), +} + impl Row { fn add_object(&mut self, column: usize, object: &str) { - match &mut self.wire { - Wire::Qubit { .. } => { - self.add(column, fmt_on_qubit_wire(object)); - } - Wire::Classical { .. } => { - self.add(column, fmt_on_classical_wire(object)); - } - }; + self.add(column, CircuitObject::Object(object.to_string())); } fn add_gate(&mut self, column: usize, gate: &str, args: Option<&str>, is_adjoint: bool) { @@ -129,18 +132,19 @@ impl Row { if let Some(args) = args { let _ = write!(&mut gate_label, "({args})"); } - self.add_object(column, &gate_label); + + self.add_object(column, gate_label.as_str()); } fn add_vertical(&mut self, column: usize) { if !self.objects.contains_key(&column) { match self.wire { - Wire::Qubit { .. } => self.add(column, QUBIT_WIRE_CROSS), + Wire::Qubit { .. } => self.add(column, CircuitObject::WireCross), Wire::Classical { start_column } => { if start_column.is_some() { - self.add(column, CLASSICAL_WIRE_CROSS); + self.add(column, CircuitObject::WireCross); } else { - self.add(column, VERTICAL); + self.add(column, CircuitObject::Vertical); } } } @@ -150,12 +154,12 @@ impl Row { fn add_dashed_vertical(&mut self, column: usize) { if !self.objects.contains_key(&column) { match self.wire { - Wire::Qubit { .. } => self.add(column, QUBIT_WIRE_DASHED_CROSS), + Wire::Qubit { .. } => self.add(column, CircuitObject::DashedCross), Wire::Classical { start_column } => { if start_column.is_some() { - self.add(column, CLASSICAL_WIRE_DASHED_CROSS); + self.add(column, CircuitObject::DashedCross); } else { - self.add(column, VERTICAL_DASHED); + self.add(column, CircuitObject::VerticalDashed); } } } @@ -163,18 +167,23 @@ impl Row { } fn start_classical(&mut self, column: usize) { - self.add(column, CLASSICAL_WIRE_START); + self.add(column, CircuitObject::WireStart); if let Wire::Classical { start_column } = &mut self.wire { start_column.replace(column); } } - fn add(&mut self, column: usize, v: impl Into) { - self.objects.insert(column, v.into()); + fn add(&mut self, column: usize, circuit_object: CircuitObject) { + self.objects.insert(column, circuit_object); self.next_column = column + 1; } - fn fmt(&self, f: &mut std::fmt::Formatter<'_>, end_column: usize) -> std::fmt::Result { + fn fmt( + &self, + f: &mut std::fmt::Formatter<'_>, + column_widths: &ColumnWidthsByColumn, + end_column: usize, + ) -> std::fmt::Result { // Temporary string so we can trim whitespace at the end let mut s = String::new(); match &self.wire { @@ -182,22 +191,24 @@ impl Row { 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); if let Some(v) = val { - s.write_str(v)?; + s.write_str(&fmt_qubit_circuit_object(v, column_width))?; } else { - s.write_str(QUBIT_WIRE)?; + s.write_str(&get_qubit_wire(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(v)?; + s.write_str(&fmt_classical_circuit_object(v, column_width))?; } else if start_column.map_or(false, |s| column > s) { - s.write_str(CLASSICAL_WIRE)?; + s.write_str(&get_classical_wire(column_width))?; } else { - s.write_str(BLANK)?; + s.write_str(&get_blank(column_width))?; } } } @@ -207,33 +218,103 @@ impl Row { } } -const COLUMN_WIDTH: usize = 7; -const QUBIT_WIRE: &str = "───────"; -const CLASSICAL_WIRE: &str = "═══════"; -const QUBIT_WIRE_CROSS: &str = "───┼───"; -const CLASSICAL_WIRE_CROSS: &str = "═══╪═══"; -const CLASSICAL_WIRE_START: &str = " ╘═══"; -const QUBIT_WIRE_DASHED_CROSS: &str = "───┆───"; -const CLASSICAL_WIRE_DASHED_CROSS: &str = "═══┆═══"; -const VERTICAL_DASHED: &str = " ┆ "; -const VERTICAL: &str = " │ "; -const BLANK: &str = " "; +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 get_qubit_wire_cross(column_width: usize) -> String { + let half_width = "─".repeat(column_width / 2); + format!("{}┼{}", half_width, half_width) +} + +/// "═══╪═══" +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) +} /// "q_0 " #[allow(clippy::doc_markdown)] fn fmt_qubit_label(id: usize) -> String { - let rest = COLUMN_WIDTH - 2; + let rest = MIN_COLUMN_WIDTH - 2; format!("q_{id: String { - format!("{:─^COLUMN_WIDTH$}", format!(" {obj} ")) +fn fmt_on_qubit_wire(obj: &str, column_width: usize) -> String { + format!("{:─^column_width$}", format!(" {obj} ")) } /// "══ A ══" -fn fmt_on_classical_wire(obj: &str) -> String { - format!("{:═^COLUMN_WIDTH$}", format!(" {obj} ")) +fn fmt_on_classical_wire(obj: &str, column_width: usize) -> String { + format!("{:═^column_width$}", format!(" {obj} ")) +} + +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), + } +} + +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), + } } impl Display for Circuit { @@ -289,6 +370,7 @@ impl Display for Circuit { let mut all_rows = targets.clone(); all_rows.extend(controls.iter()); all_rows.sort_unstable(); + // We'll need to know the entire range of rows for this operation so we can // figure out the starting column and also so we can draw any // vertical lines that cross wires. @@ -298,12 +380,11 @@ impl Display for Circuit { // The starting column - the first available column in all // the rows that this operation spans. - let mut column = 1; - for row in &rows[begin..end] { - if row.next_column > column { - column = row.next_column; - } - } + let column = rows[begin..end] + .iter() + .map(|r| r.next_column) + .max() + .unwrap_or(1); // Add the operation to the diagram for i in targets { @@ -348,9 +429,28 @@ impl Display for Circuit { .max_by_key(|r| r.next_column) .map_or(1, |r| r.next_column); + // 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) + .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(), + ) + }) + .collect::(); + // Draw the diagram for row in rows { - row.fmt(f, end_column)?; + row.fmt(f, &column_widths, end_column)?; } Ok(()) diff --git a/compiler/qsc_circuit/src/circuit/tests.rs b/compiler/qsc_circuit/src/circuit/tests.rs index 3a9f992a82..d62e0c70a5 100644 --- a/compiler/qsc_circuit/src/circuit/tests.rs +++ b/compiler/qsc_circuit/src/circuit/tests.rs @@ -221,10 +221,8 @@ fn with_args() { }], }; - // This looks wonky because the gate label is longer - // than the static column width, but we can live with it. expect![[r" - q_0 rx(1.5708) + q_0 ─ rx(1.5708) ── "]] .assert_eq(&c.to_string()); } @@ -258,12 +256,10 @@ fn two_targets() { ], }; - // This looks wonky because the gate label is longer - // than the static column width, but we can live with it. expect![[r" - q_0 rzz(1.0000) - q_1 ───┆─── - q_2 rzz(1.0000) + q_0 ─ rzz(1.0000) ─ + q_1 ───────┆─────── + q_2 ─ rzz(1.0000) ─ "]] .assert_eq(&c.to_string()); }