- engine::draw::layout: the drawing as data. It gives the qubits, the columns (each item's cells as [qubit, label], and whether a line joins them) and the legend of conditions, packed as the text drawing packs them. The packing moves into one function, which both forms use.
- wai-web's engine binding gains engine_layout, and the desktop app gains `layout`.
- The Studio draws the circuit as SVG from it:
- wires, gate boxes, a control's dot joined to its target's ring, swaps;
- measurements as meters with their bit, resets;
- a gate under a condition with a dashed outline and the condition's number.
Colours come from the page's tokens, each item has a title naming its gates and qubits, and the SVG has a text alternative. "Text" shows the text drawing to copy. Past 400 columns the diagram stops and says so.
- Tests: the layout of a circuit with a shared column, a joined CX and a conditioned gate; the binding's layout. The Studio test adds the diagram's glyphs and text alternative, and the Text toggle.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit c2e89a9fd9)
359 lines
14 KiB
Rust
359 lines
14 KiB
Rust
//! A program's circuit as text: one wire per qubit, gates packed into the
|
||
//! earliest column their qubits are free, a control as `●`, a CX target as
|
||
//! `⊕`, a swap as `×`, a measurement into bit `k` as `Mk`, a reset as `|0⟩`.
|
||
//! A gate under a condition carries `?n`, and the legend under the drawing
|
||
//! says what condition `n` is. Wide circuits wrap at `width` columns.
|
||
|
||
use std::f64::consts::PI;
|
||
use crate::qasm::{Cond, Instr, Kind, Op, Program};
|
||
|
||
/// One thing drawn in a column: its label on each qubit it touches, and
|
||
/// whether a vertical line joins them.
|
||
struct Item {
|
||
cells: Vec<(u32, String)>,
|
||
joined: bool,
|
||
}
|
||
|
||
/// `x` as a multiple of π where it is one with a small denominator, else in
|
||
/// decimals.
|
||
pub fn angle(x: f64) -> String {
|
||
let r = x / PI;
|
||
for d in [1i64, 2, 3, 4, 6, 8, 12, 16] {
|
||
let n = r * d as f64;
|
||
if (n - n.round()).abs() < 1e-9 {
|
||
let n = n.round() as i64;
|
||
let g = gcd(n.abs(), d);
|
||
let (n, d) = (n / g, d / g);
|
||
return match (n, d) {
|
||
(0, _) => "0".into(),
|
||
(1, 1) => "π".into(),
|
||
(-1, 1) => "-π".into(),
|
||
(n, 1) => format!("{n}π"),
|
||
(1, d) => format!("π/{d}"),
|
||
(-1, d) => format!("-π/{d}"),
|
||
(n, d) => format!("{n}π/{d}"),
|
||
};
|
||
}
|
||
}
|
||
let t = format!("{x:.4}");
|
||
t.trim_end_matches('0').trim_end_matches('.').to_string()
|
||
}
|
||
|
||
fn gcd(a: i64, b: i64) -> i64 {
|
||
if b == 0 { a.max(1) } else { gcd(b, a % b) }
|
||
}
|
||
|
||
fn params(p: &[f64]) -> String {
|
||
p.iter().map(|&x| angle(x)).collect::<Vec<_>>().join(",")
|
||
}
|
||
|
||
/// The cells of one gate.
|
||
fn gate(op: &Op) -> Item {
|
||
use Kind::*;
|
||
let q = &op.qubits;
|
||
let p = params(&op.params);
|
||
let one = |l: &str| Item { cells: vec![(q[0], l.to_string())], joined: false };
|
||
let ctl = |rest: Vec<(u32, String)>| {
|
||
let mut cells = vec![(q[0], "●".to_string())];
|
||
cells.extend(rest);
|
||
Item { cells, joined: true }
|
||
};
|
||
match op.kind {
|
||
Id => one("I"),
|
||
X => one("X"),
|
||
Y => one("Y"),
|
||
Z => one("Z"),
|
||
H => one("H"),
|
||
S => one("S"),
|
||
Sdg => one("S†"),
|
||
T => one("T"),
|
||
Tdg => one("T†"),
|
||
Sx => one("√X"),
|
||
Sxdg => one("√X†"),
|
||
Rx => one(&format!("Rx({p})")),
|
||
Ry => one(&format!("Ry({p})")),
|
||
Rz => one(&format!("Rz({p})")),
|
||
P => one(&format!("P({p})")),
|
||
U => one(&format!("U({p})")),
|
||
Cx => ctl(vec![(q[1], "⊕".into())]),
|
||
Cy => ctl(vec![(q[1], "Y".into())]),
|
||
Cz => ctl(vec![(q[1], "●".into())]),
|
||
Ch => ctl(vec![(q[1], "H".into())]),
|
||
Cp => ctl(vec![(q[1], format!("P({p})"))]),
|
||
Crx => ctl(vec![(q[1], format!("Rx({p})"))]),
|
||
Cry => ctl(vec![(q[1], format!("Ry({p})"))]),
|
||
Crz => ctl(vec![(q[1], format!("Rz({p})"))]),
|
||
Cu => ctl(vec![(q[1], format!("U({p})"))]),
|
||
Swap => Item { cells: vec![(q[0], "×".into()), (q[1], "×".into())], joined: true },
|
||
Iswap => Item { cells: vec![(q[0], "iSWAP".into()), (q[1], "iSWAP".into())], joined: true },
|
||
Rxx => Item { cells: vec![(q[0], format!("Rxx({p})")), (q[1], format!("Rxx({p})"))], joined: true },
|
||
Ryy => Item { cells: vec![(q[0], format!("Ryy({p})")), (q[1], format!("Ryy({p})"))], joined: true },
|
||
Rzz => Item { cells: vec![(q[0], format!("Rzz({p})")), (q[1], format!("Rzz({p})"))], joined: true },
|
||
Ccx => Item { cells: vec![(q[0], "●".into()), (q[1], "●".into()), (q[2], "⊕".into())], joined: true },
|
||
Ccz => Item { cells: vec![(q[0], "●".into()), (q[1], "●".into()), (q[2], "●".into())], joined: true },
|
||
Cswap => Item { cells: vec![(q[0], "●".into()), (q[1], "×".into()), (q[2], "×".into())], joined: true },
|
||
}
|
||
}
|
||
|
||
/// A condition as the legend writes it: the bits, highest first, read as an
|
||
/// unsigned integer.
|
||
fn condition(c: &Cond) -> String {
|
||
let op = if c.equal { "=" } else { "≠" };
|
||
match c.bits.as_slice() {
|
||
[b] => format!("if bit {b} {op} {}", c.value),
|
||
bits => {
|
||
let names: Vec<String> = bits.iter().rev().map(|b| b.to_string()).collect();
|
||
format!("if bits ({}) {op} {}", names.join(" "), c.value)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The items to draw, in order, and the legend of conditions.
|
||
fn items(p: &Program) -> (Vec<Item>, Vec<String>) {
|
||
let mut out = Vec::new();
|
||
let mut legend = Vec::new();
|
||
let Some(steps) = &p.dynamic else {
|
||
out.extend(p.ops.iter().map(gate));
|
||
out.extend(p.measured.iter().map(|&(q, b)| Item { cells: vec![(q, format!("M{b}"))], joined: false }));
|
||
return (out, legend);
|
||
};
|
||
// Each step's condition: a JumpUnless guards the steps up to its target;
|
||
// a Jump at the end of that region skips an else branch, up to its own.
|
||
let mut guard: Vec<Option<usize>> = vec![None; steps.len()];
|
||
for (i, s) in steps.iter().enumerate() {
|
||
if let Instr::JumpUnless(c, to) = s {
|
||
legend.push(condition(c));
|
||
let n = legend.len();
|
||
let then_end = *to;
|
||
let else_end = match steps.get(then_end.wrapping_sub(1)) {
|
||
Some(Instr::Jump(e)) if then_end > i + 1 => Some(*e),
|
||
_ => None,
|
||
};
|
||
for g in guard.iter_mut().take(then_end.min(steps.len())).skip(i + 1) {
|
||
g.get_or_insert(n);
|
||
}
|
||
if let Some(e) = else_end {
|
||
legend.push(format!("else of ?{n}"));
|
||
let m = legend.len();
|
||
for g in guard.iter_mut().take(e.min(steps.len())).skip(then_end) {
|
||
g.get_or_insert(m);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for (i, s) in steps.iter().enumerate() {
|
||
let mut item = match s {
|
||
Instr::Gate(k) => gate(&p.ops[*k]),
|
||
Instr::Measure { qubit, bit } => Item { cells: vec![(*qubit, format!("M{bit}"))], joined: false },
|
||
Instr::Reset(q) => Item { cells: vec![(*q, "|0⟩".into())], joined: false },
|
||
Instr::JumpUnless(..) | Instr::Jump(_) => continue,
|
||
};
|
||
if let Some(n) = guard[i] {
|
||
for (_, l) in item.cells.iter_mut() {
|
||
l.push_str(&format!("?{n}"));
|
||
}
|
||
}
|
||
out.push(item);
|
||
}
|
||
(out, legend)
|
||
}
|
||
|
||
/// The items packed into columns: each goes to the first column after the
|
||
/// last one used on any wire it covers. With the legend of conditions.
|
||
fn columns(p: &Program) -> (Vec<Vec<Item>>, Vec<String>) {
|
||
let n = p.n_qubits as usize;
|
||
let (items, legend) = items(p);
|
||
let mut free = vec![0usize; n];
|
||
let mut columns: Vec<Vec<Item>> = Vec::new();
|
||
for it in items {
|
||
let lo = it.cells.iter().map(|c| c.0 as usize).min().unwrap_or(0);
|
||
let hi = if it.joined { it.cells.iter().map(|c| c.0 as usize).max().unwrap_or(lo) } else { lo };
|
||
let col = free[lo..=hi].iter().copied().max().unwrap_or(0);
|
||
for f in &mut free[lo..=hi] {
|
||
*f = col + 1;
|
||
}
|
||
if columns.len() <= col {
|
||
columns.resize_with(col + 1, Vec::new);
|
||
}
|
||
columns[col].push(it);
|
||
}
|
||
(columns, legend)
|
||
}
|
||
|
||
/// The same drawing as data, for a page to draw: the qubits, the columns
|
||
/// (each a list of items, each its cells as `[qubit, label]` and whether a
|
||
/// line joins them) and the legend of conditions. A label is what the text
|
||
/// drawing shows: `●` a control, `⊕` a CX target, `×` a swap, `Mk` a
|
||
/// measurement into bit k, `|0⟩` a reset, any other the gate; `?n` after it
|
||
/// marks condition n.
|
||
pub fn layout(p: &Program) -> serde_json::Value {
|
||
let (columns, legend) = columns(p);
|
||
serde_json::json!({
|
||
"qubits": p.n_qubits,
|
||
"columns": columns.iter().map(|col| col.iter().map(|it| serde_json::json!({
|
||
"cells": it.cells.iter().map(|(q, l)| serde_json::json!([q, l])).collect::<Vec<_>>(),
|
||
"joined": it.joined,
|
||
})).collect::<Vec<_>>()).collect::<Vec<_>>(),
|
||
"legend": legend,
|
||
})
|
||
}
|
||
|
||
/// The drawing, wrapped at `width` columns.
|
||
pub fn draw(p: &Program, width: usize) -> String {
|
||
let n = p.n_qubits as usize;
|
||
if n == 0 {
|
||
return "(no qubits)\n".into();
|
||
}
|
||
let (columns, legend) = columns(p);
|
||
let label_w = format!("q{}", n - 1).chars().count();
|
||
let prefix = |q: usize| format!("{:>label_w$}: ", format!("q{q}"));
|
||
let blank = " ".repeat(label_w + 2);
|
||
// Each column's text per wire, and its joins between wire q and q + 1.
|
||
let mut blocks: Vec<(usize, Vec<String>, Vec<bool>)> = Vec::new();
|
||
for col in &columns {
|
||
let w = col.iter().flat_map(|it| it.cells.iter().map(|c| c.1.chars().count())).max().unwrap_or(1).max(1);
|
||
let mut wires = vec![None::<String>; n];
|
||
let mut through = vec![false; n];
|
||
let mut joins = vec![false; n];
|
||
for it in col {
|
||
for (q, l) in &it.cells {
|
||
wires[*q as usize] = Some(l.clone());
|
||
}
|
||
if it.joined {
|
||
let lo = it.cells.iter().map(|c| c.0 as usize).min().unwrap();
|
||
let hi = it.cells.iter().map(|c| c.0 as usize).max().unwrap();
|
||
for (q, t) in through.iter_mut().enumerate().take(hi).skip(lo + 1) {
|
||
*t = wires[q].is_none();
|
||
}
|
||
for j in joins.iter_mut().take(hi).skip(lo) {
|
||
*j = true;
|
||
}
|
||
}
|
||
}
|
||
let text: Vec<String> = (0..n)
|
||
.map(|q| {
|
||
let l = match &wires[q] {
|
||
Some(l) => l.clone(),
|
||
None if through[q] => "┼".into(),
|
||
None => "─".into(),
|
||
};
|
||
let pad = w - l.chars().count();
|
||
format!("─{}{l}{}─", "─".repeat(pad / 2), "─".repeat(pad - pad / 2))
|
||
})
|
||
.collect();
|
||
blocks.push((w + 2, text, joins));
|
||
}
|
||
let mut out = String::new();
|
||
let avail = width.saturating_sub(label_w + 4).max(8);
|
||
let mut start = 0;
|
||
while start < blocks.len() || (start == 0 && blocks.is_empty()) {
|
||
let mut end = start;
|
||
let mut used = 0;
|
||
while end < blocks.len() && (end == start || used + blocks[end].0 <= avail) {
|
||
used += blocks[end].0;
|
||
end += 1;
|
||
}
|
||
if start > 0 {
|
||
out.push('\n');
|
||
}
|
||
for q in 0..n {
|
||
out.push_str(&prefix(q));
|
||
out.push('─');
|
||
for b in &blocks[start..end] {
|
||
out.push_str(&b.1[q]);
|
||
}
|
||
out.push_str("─\n");
|
||
if q + 1 < n {
|
||
let mut line = blank.clone();
|
||
line.push(' ');
|
||
for b in &blocks[start..end] {
|
||
// Where a one-character label sits in this column.
|
||
let w = b.0;
|
||
let mid = 1 + (w - 3) / 2;
|
||
let mut cell = vec![' '; w];
|
||
if b.2[q] {
|
||
cell[mid] = '│';
|
||
}
|
||
line.extend(cell);
|
||
}
|
||
out.push_str(line.trim_end());
|
||
out.push('\n');
|
||
}
|
||
}
|
||
if end == start {
|
||
break;
|
||
}
|
||
start = end;
|
||
}
|
||
for (i, l) in legend.iter().enumerate() {
|
||
out.push_str(&format!("?{} {l}\n", i + 1));
|
||
}
|
||
out
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::qasm::parse;
|
||
|
||
#[test]
|
||
fn angles_are_written_as_multiples_of_pi_where_they_are() {
|
||
assert_eq!(angle(PI / 4.0), "π/4");
|
||
assert_eq!(angle(-PI / 2.0), "-π/2");
|
||
assert_eq!(angle(3.0 * PI / 4.0), "3π/4");
|
||
assert_eq!(angle(2.0 * PI), "2π");
|
||
assert_eq!(angle(0.0), "0");
|
||
assert_eq!(angle(0.3), "0.3");
|
||
}
|
||
|
||
#[test]
|
||
fn a_bell_pair_is_h_then_a_controlled_x_joined_across_the_wires() {
|
||
let d = draw(&parse("OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];").unwrap(), 100);
|
||
let lines: Vec<&str> = d.lines().collect();
|
||
assert_eq!(lines.len(), 3, "{d}");
|
||
assert!(lines[0].starts_with("q0: ") && lines[0].contains('H') && lines[0].contains('●'), "{d}");
|
||
assert!(lines[1].contains('│'), "{d}");
|
||
assert!(lines[2].starts_with("q1: ") && lines[2].contains('⊕') && !lines[2].contains('H'), "{d}");
|
||
// The control sits after H, in the column of the target.
|
||
assert_eq!(lines[0].chars().position(|c| c == '●'), lines[2].chars().position(|c| c == '⊕'), "{d}");
|
||
}
|
||
|
||
#[test]
|
||
fn gates_on_different_wires_share_a_column_and_a_gate_between_crosses_the_middle() {
|
||
let d = draw(&parse("qubit[3] q; h q[0]; h q[1]; h q[2]; cx q[0], q[2];").unwrap(), 100);
|
||
let lines: Vec<&str> = d.lines().collect();
|
||
let h_at: Vec<Option<usize>> = [0, 2, 4].iter().map(|&i| lines[i].chars().position(|c| c == 'H')).collect();
|
||
assert!(h_at.iter().all(|&p| p == h_at[0]), "the three H share a column: {d}");
|
||
assert!(lines[2].contains('┼'), "the CX passes through q1: {d}");
|
||
}
|
||
|
||
#[test]
|
||
fn a_measurement_names_its_bit_and_a_conditioned_gate_its_condition() {
|
||
let d = draw(&parse("OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; c[0] = measure q[0]; if (c[0]) x q[1]; c[1] = measure q[1];").unwrap(), 100);
|
||
assert!(d.contains("M0") && d.contains("M1"), "{d}");
|
||
assert!(d.contains("X?1") && d.contains("?1 if bit 0 = 1"), "{d}");
|
||
}
|
||
|
||
#[test]
|
||
fn the_layout_is_the_drawing_as_data() {
|
||
let l = layout(&parse("OPENQASM 3.0; qubit[3] q; bit[3] c; h q[0]; h q[2]; cx q[0], q[2]; c[0] = measure q[0]; if (c[0]) x q[1];").unwrap());
|
||
assert_eq!(l["qubits"], 3);
|
||
let cols = l["columns"].as_array().unwrap();
|
||
// The two H share the first column; the CX joins q0 to q2 in the second.
|
||
assert_eq!(cols[0].as_array().unwrap().len(), 2, "{l}");
|
||
let cx = &cols[1][0];
|
||
assert_eq!(cx["joined"], true);
|
||
assert_eq!(cx["cells"], serde_json::json!([[0, "●"], [2, "⊕"]]));
|
||
assert!(l.to_string().contains("\"X?1\""), "{l}");
|
||
assert_eq!(l["legend"][0], "if bit 0 = 1");
|
||
}
|
||
|
||
#[test]
|
||
fn a_long_circuit_wraps_and_keeps_every_gate() {
|
||
let src = format!("qubit[2] q; {}", "h q[0]; cx q[0], q[1]; ".repeat(40));
|
||
let d = draw(&parse(&src).unwrap(), 60);
|
||
assert!(d.lines().all(|l| l.chars().count() <= 60), "{d}");
|
||
assert_eq!(d.matches('H').count(), 40, "{d}");
|
||
assert_eq!(d.matches('⊕').count(), 40, "{d}");
|
||
}
|
||
}
|