From e3b1564aad627a3edfaa303c539fd6666f818884 Mon Sep 17 00:00:00 2001 From: zzzappy Date: Fri, 19 Sep 2025 10:43:24 -0700 Subject: [PATCH 1/4] Add crumpy changes in glue/crumble Add crumpy's .py files (both src and test), update some .js files of Crumble, update .gitignore and package.json for JS bundling, add pyproject.toml with file path updates --- .gitignore | 2 + glue/crumble/crumpy/__init__.py | 115 +++++ glue/crumble/crumpy/py.typed | 0 glue/crumble/crumpy/test_package.py | 9 + glue/crumble/crumpy/test_widget.py | 124 ++++++ glue/crumble/draw/config.js | 57 ++- glue/crumble/draw/main_draw.js | 637 ++++++++++----------------- glue/crumble/draw/timeline_viewer.js | 453 +++++++++++-------- glue/crumble/gates/gate_draw_util.js | 349 ++++++++------- glue/crumble/main.js | 612 ++++--------------------- glue/crumble/package-lock.json | 497 +++++++++++++++++++++ glue/crumble/package.json | 13 +- glue/crumble/pyproject.toml | 176 ++++++++ 13 files changed, 1761 insertions(+), 1283 deletions(-) create mode 100644 glue/crumble/crumpy/__init__.py create mode 100644 glue/crumble/crumpy/py.typed create mode 100644 glue/crumble/crumpy/test_package.py create mode 100644 glue/crumble/crumpy/test_widget.py create mode 100644 glue/crumble/package-lock.json create mode 100644 glue/crumble/pyproject.toml diff --git a/.gitignore b/.gitignore index 76441ea0b..c7e8eaf63 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ build.ninja node_modules MODULE.bazel.lock .ninja_lock +glue/crumble/node_modules +glue/crumble/crumpy/bundle.js \ No newline at end of file diff --git a/glue/crumble/crumpy/__init__.py b/glue/crumble/crumpy/__init__.py new file mode 100644 index 000000000..97cf93274 --- /dev/null +++ b/glue/crumble/crumpy/__init__.py @@ -0,0 +1,115 @@ +""" +Copyright (c) 2025 Riverlane. All rights reserved. + +crumpy: A python library for visualizing Crumble circuits in Jupyter +""" + +# mypy: ignore-errors +# pylint: disable=abstract-method + +from __future__ import annotations + +import pathlib +from importlib.metadata import version + +import anywidget +import cirq +import qiskit +import qiskit.qasm3 +import stim +import stimcirq +import traitlets +from cirq.contrib.qasm_import import circuit_from_qasm + +__version__ = version(__name__) + +__all__ = ["__version__"] + +bundler_output_dir = pathlib.Path(__file__).parent + + +class CircuitWidget(anywidget.AnyWidget): + """A Jupyter widget for displaying Crumble circuit diagrams. + + Attributes + ---------- + stim : str + Stim circuit to be drawn. + indentCircuitLines : bool + If circuit lines for subsequent qubits in the same row get indented when drawn. + Defaults to True (matching Crumble). + curveConnectors : bool + If connectors (e.g., the line between control and target of a CNOT) can be drawn curved. + Defaults to True (matching Crumble). + showAnnotationRegions : bool + If detector region and observable marks are shown. + Defaults to True (matching Crumble). + """ + + _esm = bundler_output_dir / "bundle.js" + stim = traitlets.Unicode(default_value="", help="Stim circuit to be drawn").tag( + sync=True + ) + indentCircuitLines = traitlets.Bool(True).tag(sync=True) + curveConnectors = traitlets.Bool(True).tag(sync=True) + showAnnotationRegions = traitlets.Bool(True).tag(sync=True) + + @staticmethod + def from_cirq(cirq_circuit: cirq.Circuit): + """Create a `CircuitWidget` from a `cirq.Circuit`. + + `cirq_circuit` will be transpiled to a `stim.Circuit` before use with `CircuitWidget`. + `cirq_circuit` must be transpilable to a `stim.Circuit`. + + Parameters + ---------- + cirq_circuit : cirq.Circuit + cirq circuit to visualize + + Raises + ------ + TypeError + If `cirq_circuit` is unable to be converted to a `stim.Circuit` + """ + try: + stim_circuit = stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit) + return CircuitWidget(stim=str(stim_circuit)) + except TypeError as ex: + msg = "Unable to translate cirq circuit to stim." + raise TypeError(msg) from ex + + @staticmethod + def from_qiskit(qiskit_circuit: qiskit.QuantumCircuit): + """Create a `CircuitWidget` from a `qiskit.QuantumCircuit`. + + `qiskit_circuit` will be transpiled to a `stim.Circuit` before use with `CircuitWidget`. + `qiskit_circuit` must be transpilable to a `stim.Circuit`. + + Parameters + ---------- + qiskit_circuit : qiskit.QuantumCircuit + qiskit circuit to visualize + + Raises + ------ + TypeError + If `qiskit_circuit` is unable to be converted to a `stim.Circuit` + """ + try: + qasm_circuit = qiskit.qasm3.dumps(qiskit_circuit) + cirq_circuit = circuit_from_qasm(qasm_circuit) + return CircuitWidget.from_cirq(cirq_circuit) + except TypeError as ex: + msg = "Unable to translate qiskit circuit to stim." + raise TypeError(msg) from ex + + @staticmethod + def from_stim(stim_circuit: stim.Circuit): + """Create a `CircuitWidget` from a `stim.Circuit`. + + Parameters + ---------- + stim_circuit : stim.Circuit + stim circuit to visualize + """ + return CircuitWidget(stim=str(stim_circuit)) diff --git a/glue/crumble/crumpy/py.typed b/glue/crumble/crumpy/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/glue/crumble/crumpy/test_package.py b/glue/crumble/crumpy/test_package.py new file mode 100644 index 000000000..73f7cd39f --- /dev/null +++ b/glue/crumble/crumpy/test_package.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +import importlib.metadata + +import crumpy as m + + +def test_version(): + assert m.__version__ in importlib.metadata.version("crumpy") diff --git a/glue/crumble/crumpy/test_widget.py b/glue/crumble/crumpy/test_widget.py new file mode 100644 index 000000000..dfd9d3df4 --- /dev/null +++ b/glue/crumble/crumpy/test_widget.py @@ -0,0 +1,124 @@ +# mypy: ignore-errors + +from __future__ import annotations + +import cirq +import ipywidgets +import pytest +import qiskit +import stim +import traitlets + +from crumpy import CircuitWidget + + +def test_basic_no_exception(): + """CircuitWidget should initialize without errors.""" + CircuitWidget(stim="H 0;", indentCircuitLines=False, curveConnectors=False) + + +def test_bad_stim_instruction_no_exception(): + """CircuitWidget should not raise an error if given any invalid stim instructions.""" + CircuitWidget(stim="H 0;thisIsNotAnInstruction 123;CNOT 0 1;") + + +def test_bad_trait_assignment_exception(): + """Assigning a non-string value to a CircuitWidget's stim should raise a TraitError.""" + widg = CircuitWidget() + with pytest.raises(traitlets.TraitError): + widg.stim = 123 + + +def test_has_traits(): + """CircuitWidget should have the expected traits and sync metadata.""" + widg = CircuitWidget() + assert widg.has_trait("stim") + assert widg.has_trait("indentCircuitLines") + assert widg.has_trait("curveConnectors") + assert widg.has_trait("showAnnotationRegions") + assert widg.trait_metadata("stim", "sync") + assert widg.trait_metadata("indentCircuitLines", "sync") + assert widg.trait_metadata("curveConnectors", "sync") + assert widg.trait_metadata("showAnnotationRegions", "sync") + + +def test_use_case_dlink(): + """CircuitWidget should update its stim when linked to an IntSlider via ipywidgets.dlink.""" + + def sliderToCircuit(slider_val): + return "H 0;" * int(slider_val) + + circuit = CircuitWidget() + slider_default_value = 1 + slider = ipywidgets.IntSlider( + description="# of H gates", value=slider_default_value, min=0, max=30 + ) + ipywidgets.dlink((slider, "value"), (circuit, "stim"), sliderToCircuit) + assert circuit.stim == sliderToCircuit(slider_default_value) + + new_slider_value = 5 + slider.value = new_slider_value + assert circuit.stim == sliderToCircuit(new_slider_value) + + new_stim = "Y 0;" + circuit.stim = new_stim + assert circuit.stim == new_stim + + +class Test_CircuitImporting: + def test_from_cirq_valid(self): + """CircuitWidget.from_cirq should create a CircuitWidget with the correct circuit when given a valid (stim-transpilable) cirq circuit.""" + + q0 = cirq.LineQubit(0) + q1 = cirq.LineQubit(1) + cirq_circuit = cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1)) + + widg = CircuitWidget.from_cirq(cirq_circuit) + + assert "H 0" in widg.stim + assert "CNOT 0 1" in widg.stim or "CX 0 1" in widg.stim + + def test_from_cirq_invalid(self): + """CircuitWidget.from_cirq should raise an error when given an invalid (non-stim-transpilable) cirq circuit.""" + + q0 = cirq.LineQubit(0) + q1 = cirq.LineQubit(1) + cirq_circuit = cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1), cirq.rx(2).on(q0)) + with pytest.raises(TypeError): + CircuitWidget.from_cirq(cirq_circuit) + + def test_from_qiskit_valid(self): + """CircuitWidget.from_qiskit should create a CircuitWidget with the correct circuit when given a valid (stim-transpilable) qiskit circuit.""" + + qc = qiskit.QuantumCircuit(2) + qc.z(0) + qc.cx(1, 0) + + widg = CircuitWidget.from_qiskit(qc) + + assert "Z 0" in widg.stim + assert "CNOT 1 0" in widg.stim or "CX 1 0" in widg.stim + + def test_from_qiskit_invalid(self): + """CircuitWidget.from_qiskit should raise an error when given an invalid (non-stim-transpilable) qiskit circuit.""" + + qc = qiskit.QuantumCircuit(2) + qc.z(0) + qc.cx(1, 0) + qc.ry(1.234, 0) + + with pytest.raises(TypeError): + CircuitWidget.from_qiskit(qc) + + def test_from_stim(self): + """CircuitWidget.from_stim should create a CircuitWidget with the given stim circuit.""" + + stim_circuit = stim.Circuit(""" + X 1 + CY 0 1 + """) + + widg = CircuitWidget.from_stim(stim_circuit) + + assert "X 1" in widg.stim + assert "CY 0 1" in widg.stim # also covers alternate name ZCY diff --git a/glue/crumble/draw/config.js b/glue/crumble/draw/config.js index 25c6c739b..61bd5e866 100644 --- a/glue/crumble/draw/config.js +++ b/glue/crumble/draw/config.js @@ -1,6 +1,61 @@ +/** + * Copyright 2023 Craig Gidney + * Copyright 2025 Riverlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications: + * - Refactored for CrumPy + */ + const pitch = 50; const rad = 10; const OFFSET_X = -pitch + Math.floor(pitch / 4) + 0.5; const OFFSET_Y = -pitch + Math.floor(pitch / 4) + 0.5; +let indentCircuitLines = true; +let curveConnectors = true; +let showAnnotationRegions = true; + +const setIndentCircuitLines = (newBool) => { + if (typeof newBool !== "boolean") { + throw new TypeError(`Expected a boolean, but got ${typeof newBool}`); + } + indentCircuitLines = newBool; +}; + +const setCurveConnectors = (newBool) => { + if (typeof newBool !== "boolean") { + throw new TypeError(`Expected a boolean, but got ${typeof newBool}`); + } + curveConnectors = newBool; +}; + +const setShowAnnotationRegions = (newBool) => { + if (typeof newBool !== "boolean") { + throw new TypeError(`Expected a boolean, but got ${typeof newBool}`); + } + showAnnotationRegions = newBool; +}; -export {pitch, rad, OFFSET_X, OFFSET_Y}; +export { + pitch, + rad, + OFFSET_X, + OFFSET_Y, + indentCircuitLines, + curveConnectors, + showAnnotationRegions, + setIndentCircuitLines, + setCurveConnectors, + setShowAnnotationRegions, +}; diff --git a/glue/crumble/draw/main_draw.js b/glue/crumble/draw/main_draw.js index 4636133e2..22c7dfca1 100644 --- a/glue/crumble/draw/main_draw.js +++ b/glue/crumble/draw/main_draw.js @@ -1,9 +1,35 @@ -import {pitch, rad, OFFSET_X, OFFSET_Y} from "./config.js" -import {marker_placement} from "../gates/gateset_markers.js"; -import {drawTimeline} from "./timeline_viewer.js"; -import {PropagatedPauliFrames} from "../circuit/propagated_pauli_frames.js"; -import {stroke_connector_to} from "../gates/gate_draw_util.js" -import {beginPathPolygon} from './draw_util.js'; +/** + * Copyright 2023 Craig Gidney + * Copyright 2025 Riverlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications: + * - Refactored for CrumPy + */ + +import { + pitch, + rad, + showAnnotationRegions, + OFFSET_X, + OFFSET_Y, +} from "./config.js"; +import { marker_placement } from "../gates/gateset_markers.js"; +import { drawTimeline } from "./timeline_viewer.js"; +import { PropagatedPauliFrames } from "../circuit/propagated_pauli_frames.js"; +import { stroke_connector_to } from "../gates/gate_draw_util.js"; +import { beginPathPolygon } from "./draw_util.js"; /** * @param {!number|undefined} x @@ -11,19 +37,23 @@ import {beginPathPolygon} from './draw_util.js'; * @return {![undefined, undefined]|![!number, !number]} */ function xyToPos(x, y) { - if (x === undefined || y === undefined) { - return [undefined, undefined]; - } - let focusX = x / pitch; - let focusY = y / pitch; - let roundedX = Math.floor(focusX * 2 + 0.5) / 2; - let roundedY = Math.floor(focusY * 2 + 0.5) / 2; - let centerX = roundedX*pitch; - let centerY = roundedY*pitch; - if (Math.abs(centerX - x) <= rad && Math.abs(centerY - y) <= rad && roundedX % 1 === roundedY % 1) { - return [roundedX, roundedY]; - } + if (x === undefined || y === undefined) { return [undefined, undefined]; + } + let focusX = x / pitch; + let focusY = y / pitch; + let roundedX = Math.floor(focusX * 2 + 0.5) / 2; + let roundedY = Math.floor(focusY * 2 + 0.5) / 2; + let centerX = roundedX * pitch; + let centerY = roundedY * pitch; + if ( + Math.abs(centerX - x) <= rad && + Math.abs(centerY - y) <= rad && + roundedX % 1 === roundedY % 1 + ) { + return [roundedX, roundedY]; + } + return [undefined, undefined]; } /** @@ -34,25 +64,25 @@ function xyToPos(x, y) { * @param {!int} mi */ function drawCrossMarkers(ctx, snap, qubitCoordsFunc, propagatedMarkers, mi) { - let crossings = propagatedMarkers.atLayer(snap.curLayer).crossings; - if (crossings !== undefined) { - for (let {q1, q2, color} of crossings) { - let [x1, y1] = qubitCoordsFunc(q1); - let [x2, y2] = qubitCoordsFunc(q2); - if (color === 'X') { - ctx.strokeStyle = 'red'; - } else if (color === 'Y') { - ctx.strokeStyle = 'green'; - } else if (color === 'Z') { - ctx.strokeStyle = 'blue'; - } else { - ctx.strokeStyle = 'purple' - } - ctx.lineWidth = 8; - stroke_connector_to(ctx, x1, y1, x2, y2); - ctx.lineWidth = 1; - } + let crossings = propagatedMarkers.atLayer(snap.curLayer).crossings; + if (crossings !== undefined) { + for (let { q1, q2, color } of crossings) { + let [x1, y1] = qubitCoordsFunc(q1); + let [x2, y2] = qubitCoordsFunc(q2); + if (color === "X") { + ctx.strokeStyle = "red"; + } else if (color === "Y") { + ctx.strokeStyle = "green"; + } else if (color === "Z") { + ctx.strokeStyle = "blue"; + } else { + ctx.strokeStyle = "purple"; + } + ctx.lineWidth = 8; + stroke_connector_to(ctx, x1, y1, x2, y2); + ctx.lineWidth = 1; } + } } /** @@ -62,11 +92,11 @@ function drawCrossMarkers(ctx, snap, qubitCoordsFunc, propagatedMarkers, mi) { * @param {!Map} propagatedMarkerLayers */ function drawMarkers(ctx, snap, qubitCoordsFunc, propagatedMarkerLayers) { - let obsCount = new Map(); - let detCount = new Map(); - for (let [mi, p] of propagatedMarkerLayers.entries()) { - drawSingleMarker(ctx, snap, qubitCoordsFunc, p, mi, obsCount, detCount); - } + let obsCount = new Map(); + let detCount = new Map(); + for (let [mi, p] of propagatedMarkerLayers.entries()) { + drawSingleMarker(ctx, snap, qubitCoordsFunc, p, mi, obsCount, detCount); + } } /** @@ -77,89 +107,96 @@ function drawMarkers(ctx, snap, qubitCoordsFunc, propagatedMarkerLayers) { * @param {!int} mi * @param {!Map} hitCount */ -function drawSingleMarker(ctx, snap, qubitCoordsFunc, propagatedMarkers, mi, hitCount) { - let basesQubitMap = propagatedMarkers.atLayer(snap.curLayer + 0.5).bases; +function drawSingleMarker( + ctx, + snap, + qubitCoordsFunc, + propagatedMarkers, + mi, + hitCount, +) { + let basesQubitMap = propagatedMarkers.atLayer(snap.curLayer + 0.5).bases; - // Convert qubit indices to draw coordinates. - let basisCoords = []; - for (let [q, b] of basesQubitMap.entries()) { - basisCoords.push([b, qubitCoordsFunc(q)]); - } + // Convert qubit indices to draw coordinates. + let basisCoords = []; + for (let [q, b] of basesQubitMap.entries()) { + basisCoords.push([b, qubitCoordsFunc(q)]); + } - // Draw a polygon for the marker set. - if (mi >= 0 && basisCoords.length > 0) { - if (basisCoords.every(e => e[0] === 'X')) { - ctx.fillStyle = 'red'; - } else if (basisCoords.every(e => e[0] === 'Y')) { - ctx.fillStyle = 'green'; - } else if (basisCoords.every(e => e[0] === 'Z')) { - ctx.fillStyle = 'blue'; - } else { - ctx.fillStyle = 'black'; - } - ctx.strokeStyle = ctx.fillStyle; - let coords = basisCoords.map(e => e[1]); - let cx = 0; - let cy = 0; - for (let [x, y] of coords) { - cx += x; - cy += y; - } - cx /= coords.length; - cy /= coords.length; - coords.sort((a, b) => { - let [ax, ay] = a; - let [bx, by] = b; - let av = Math.atan2(ay - cy, ax - cx); - let bv = Math.atan2(by - cy, bx - cx); - if (ax === cx && ay === cy) { - av = -100; - } - if (bx === cx && by === cy) { - bv = -100; - } - return av - bv; - }) - beginPathPolygon(ctx, coords); - ctx.globalAlpha *= 0.25; - ctx.fill(); - ctx.globalAlpha *= 4; - ctx.lineWidth = 2; - ctx.stroke(); - ctx.lineWidth = 1; + // Draw a polygon for the marker set. + if (mi >= 0 && basisCoords.length > 0) { + if (basisCoords.every((e) => e[0] === "X")) { + ctx.fillStyle = "red"; + } else if (basisCoords.every((e) => e[0] === "Y")) { + ctx.fillStyle = "green"; + } else if (basisCoords.every((e) => e[0] === "Z")) { + ctx.fillStyle = "blue"; + } else { + ctx.fillStyle = "black"; } + ctx.strokeStyle = ctx.fillStyle; + let coords = basisCoords.map((e) => e[1]); + let cx = 0; + let cy = 0; + for (let [x, y] of coords) { + cx += x; + cy += y; + } + cx /= coords.length; + cy /= coords.length; + coords.sort((a, b) => { + let [ax, ay] = a; + let [bx, by] = b; + let av = Math.atan2(ay - cy, ax - cx); + let bv = Math.atan2(by - cy, bx - cx); + if (ax === cx && ay === cy) { + av = -100; + } + if (bx === cx && by === cy) { + bv = -100; + } + return av - bv; + }); + beginPathPolygon(ctx, coords); + ctx.globalAlpha *= 0.25; + ctx.fill(); + ctx.globalAlpha *= 4; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.lineWidth = 1; + } - // Draw individual qubit markers. - for (let [b, [x, y]] of basisCoords) { - let {dx, dy, wx, wy} = marker_placement(mi, `${x}:${y}`, hitCount); - if (b === 'X') { - ctx.fillStyle = 'red' - } else if (b === 'Y') { - ctx.fillStyle = 'green' - } else if (b === 'Z') { - ctx.fillStyle = 'blue' - } else { - throw new Error('Not a pauli: ' + b); - } - ctx.fillRect(x - dx, y - dy, wx, wy); + // Draw individual qubit markers. + for (let [b, [x, y]] of basisCoords) { + let { dx, dy, wx, wy } = marker_placement(mi, `${x}:${y}`, hitCount); + if (b === "X") { + ctx.fillStyle = "red"; + } else if (b === "Y") { + ctx.fillStyle = "green"; + } else if (b === "Z") { + ctx.fillStyle = "blue"; + } else { + throw new Error("Not a pauli: " + b); } + ctx.fillRect(x - dx, y - dy, wx, wy); + } - // Show error highlights. - let errorsQubitSet = propagatedMarkers.atLayer(snap.curLayer).errors; - for (let q of errorsQubitSet) { - let [x, y] = qubitCoordsFunc(q); - let {dx, dy, wx, wy} = marker_placement(mi, `${x}:${y}`, hitCount); - if (mi < 0) { - ctx.lineWidth = 2; - } else { - ctx.lineWidth = 8; - } - ctx.strokeStyle = 'magenta' - ctx.strokeRect(x - dx, y - dy, wx, wy); - ctx.lineWidth = 1; - ctx.fillStyle = 'black' - ctx.fillRect(x - dx, y - dy, wx, wy); + // Show error highlights. + let errorsQubitSet = propagatedMarkers.atLayer(snap.curLayer).errors; + for (let q of errorsQubitSet) { + let [x, y] = qubitCoordsFunc(q); + let { dx, dy, wx, wy } = marker_placement(mi, `${x}:${y}`, hitCount); + if (mi < 0) { + ctx.lineWidth = 2; + } else { + ctx.lineWidth = 8; } + ctx.strokeStyle = "magenta"; + ctx.strokeRect(x - dx, y - dy, wx, wy); + ctx.lineWidth = 1; + ctx.fillStyle = "black"; + ctx.fillRect(x - dx, y - dy, wx, wy); + } } let _defensive_draw_enabled = true; @@ -168,7 +205,7 @@ let _defensive_draw_enabled = true; * @param {!boolean} val */ function setDefensiveDrawEnabled(val) { - _defensive_draw_enabled = val; + _defensive_draw_enabled = val; } /** @@ -176,20 +213,20 @@ function setDefensiveDrawEnabled(val) { * @param {!function} body */ function defensiveDraw(ctx, body) { - ctx.save(); - try { - if (_defensive_draw_enabled) { - body(); - } else { - try { - body(); - } catch (ex) { - console.error(ex); - } - } - } finally { - ctx.restore(); + ctx.save(); + try { + if (_defensive_draw_enabled) { + body(); + } else { + try { + body(); + } catch (ex) { + console.error(ex); + } } + } finally { + ctx.restore(); + } } /** @@ -197,294 +234,72 @@ function defensiveDraw(ctx, body) { * @param {!StateSnapshot} snap */ function draw(ctx, snap) { - let circuit = snap.circuit; + let circuit = snap.circuit; - let numPropagatedLayers = 0; - for (let layer of circuit.layers) { - for (let op of layer.markers) { - let gate = op.gate; - if (gate.name === "MARKX" || gate.name === "MARKY" || gate.name === "MARKZ") { - numPropagatedLayers = Math.max(numPropagatedLayers, op.args[0] + 1); - } - } + let numPropagatedLayers = 0; + for (let layer of circuit.layers) { + for (let op of layer.markers) { + let gate = op.gate; + if ( + gate.name === "MARKX" || + gate.name === "MARKY" || + gate.name === "MARKZ" + ) { + numPropagatedLayers = Math.max(numPropagatedLayers, op.args[0] + 1); + } } + } - let c2dCoordTransform = (x, y) => [x*pitch - OFFSET_X, y*pitch - OFFSET_Y]; - let qubitDrawCoords = q => { - let x = circuit.qubitCoordData[2 * q]; - let y = circuit.qubitCoordData[2 * q + 1]; - return c2dCoordTransform(x, y); - }; - let propagatedMarkerLayers = /** @type {!Map} */ new Map(); - for (let mi = 0; mi < numPropagatedLayers; mi++) { - propagatedMarkerLayers.set(mi, PropagatedPauliFrames.fromCircuit(circuit, mi)); - } - let {dets: dets, obs: obs} = circuit.collectDetectorsAndObservables(false); - let batch_input = []; - for (let mi = 0; mi < dets.length; mi++) { - batch_input.push(dets[mi].mids); - } - for (let mi of obs.keys()) { - batch_input.push(obs.get(mi)); - } - let batch_output = PropagatedPauliFrames.batchFromMeasurements(circuit, batch_input); - let batch_index = 0; + let c2dCoordTransform = (x, y) => [ + x * pitch - OFFSET_X, + y * pitch - OFFSET_Y, + ]; + let qubitDrawCoords = (q) => { + let x = circuit.qubitCoordData[2 * q]; + let y = circuit.qubitCoordData[2 * q + 1]; + return c2dCoordTransform(x, y); + }; + let propagatedMarkerLayers = + /** @type {!Map} */ new Map(); + for (let mi = 0; mi < numPropagatedLayers; mi++) { + propagatedMarkerLayers.set( + mi, + PropagatedPauliFrames.fromCircuit(circuit, mi), + ); + } + + let { dets: dets, obs: obs } = circuit.collectDetectorsAndObservables(false); + let batch_input = []; + for (let mi = 0; mi < dets.length; mi++) { + batch_input.push(dets[mi].mids); + } + for (let mi of obs.keys()) { + batch_input.push(obs.get(mi)); + } + let batch_output = PropagatedPauliFrames.batchFromMeasurements( + circuit, + batch_input, + ); + let batch_index = 0; + + if (showAnnotationRegions) { for (let mi = 0; mi < dets.length; mi++) { - propagatedMarkerLayers.set(~mi, batch_output[batch_index++]); + propagatedMarkerLayers.set(~mi, batch_output[batch_index++]); } for (let mi of obs.keys()) { - propagatedMarkerLayers.set(~mi ^ (1 << 30), batch_output[batch_index++]); + propagatedMarkerLayers.set(~mi ^ (1 << 30), batch_output[batch_index++]); } + } - let operatedOnQubits = new Set(); - for (let layer of circuit.layers) { - for (let t of layer.id_ops.keys()) { - operatedOnQubits.add(t); - } - } - let usedQubitCoordSet = new Set(); - let operatedOnQubitSet = new Set(); - for (let q of circuit.allQubits()) { - let qx = circuit.qubitCoordData[q * 2]; - let qy = circuit.qubitCoordData[q * 2 + 1]; - usedQubitCoordSet.add(`${qx},${qy}`); - if (operatedOnQubits.has(q)) { - operatedOnQubitSet.add(`${qx},${qy}`); - } - } - - defensiveDraw(ctx, () => { - ctx.fillStyle = 'white'; - ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); - let [focusX, focusY] = xyToPos(snap.curMouseX, snap.curMouseY); - - // Draw the background polygons. - let lastPolygonLayer = snap.curLayer; - for (let r = 0; r <= snap.curLayer; r++) { - for (let op of circuit.layers[r].markers) { - if (op.gate.name === 'POLYGON') { - lastPolygonLayer = r; - break; - } - } - } - let polygonMarkers = [...circuit.layers[lastPolygonLayer].markers]; - polygonMarkers.sort((a, b) => b.id_targets.length - a.id_targets.length); - for (let op of polygonMarkers) { - if (op.gate.name === 'POLYGON') { - op.id_draw(qubitDrawCoords, ctx); - } - } - - // Draw the grid of qubits. - defensiveDraw(ctx, () => { - for (let qx = 0; qx < 100; qx += 0.5) { - let [x, _] = c2dCoordTransform(qx, 0); - let s = `${qx}`; - ctx.fillStyle = 'black'; - ctx.fillText(s, x - ctx.measureText(s).width / 2, 15); - } - for (let qy = 0; qy < 100; qy += 0.5) { - let [_, y] = c2dCoordTransform(0, qy); - let s = `${qy}`; - ctx.fillStyle = 'black'; - ctx.fillText(s, 18 - ctx.measureText(s).width, y); - } - - ctx.strokeStyle = 'black'; - for (let qx = 0; qx < 100; qx += 0.5) { - let [x, _] = c2dCoordTransform(qx, 0); - let s = `${qx}`; - ctx.fillStyle = 'black'; - ctx.fillText(s, x - ctx.measureText(s).width / 2, 15); - for (let qy = qx % 1; qy < 100; qy += 1) { - let [x, y] = c2dCoordTransform(qx, qy); - ctx.fillStyle = 'white'; - let isUnused = !usedQubitCoordSet.has(`${qx},${qy}`); - let isVeryUnused = !operatedOnQubitSet.has(`${qx},${qy}`); - if (isUnused) { - ctx.globalAlpha *= 0.25; - } - if (isVeryUnused) { - ctx.globalAlpha *= 0.25; - } - ctx.fillRect(x - rad, y - rad, 2*rad, 2*rad); - ctx.strokeRect(x - rad, y - rad, 2*rad, 2*rad); - if (isUnused) { - ctx.globalAlpha *= 4; - } - if (isVeryUnused) { - ctx.globalAlpha *= 4; - } - } - } - }); - - for (let [mi, p] of propagatedMarkerLayers.entries()) { - drawCrossMarkers(ctx, snap, qubitDrawCoords, p, mi); - } - - for (let op of circuit.layers[snap.curLayer].iter_gates_and_markers()) { - if (op.gate.name !== 'POLYGON') { - op.id_draw(qubitDrawCoords, ctx); - } - } - - defensiveDraw(ctx, () => { - ctx.globalAlpha *= 0.25 - for (let [qx, qy] of snap.timelineSet.values()) { - let [x, y] = c2dCoordTransform(qx, qy); - ctx.fillStyle = 'yellow'; - ctx.fillRect(x - rad * 1.25, y - rad * 1.25, 2.5*rad, 2.5*rad); - } - }); + drawTimeline( + ctx, + snap, + propagatedMarkerLayers, + qubitDrawCoords, + circuit.layers.length, + ); - defensiveDraw(ctx, () => { - ctx.globalAlpha *= 0.5 - for (let [qx, qy] of snap.focusedSet.values()) { - let [x, y] = c2dCoordTransform(qx, qy); - ctx.fillStyle = 'blue'; - ctx.fillRect(x - rad * 1.25, y - rad * 1.25, 2.5*rad, 2.5*rad); - } - }); - - drawMarkers(ctx, snap, qubitDrawCoords, propagatedMarkerLayers); - - if (focusX !== undefined) { - ctx.save(); - ctx.globalAlpha *= 0.5; - let [x, y] = c2dCoordTransform(focusX, focusY); - ctx.fillStyle = 'red'; - ctx.fillRect(x - rad, y - rad, 2*rad, 2*rad); - ctx.restore(); - } - - defensiveDraw(ctx, () => { - ctx.globalAlpha *= 0.25; - ctx.fillStyle = 'blue'; - if (snap.mouseDownX !== undefined && snap.curMouseX !== undefined) { - let x1 = Math.min(snap.curMouseX, snap.mouseDownX); - let x2 = Math.max(snap.curMouseX, snap.mouseDownX); - let y1 = Math.min(snap.curMouseY, snap.mouseDownY); - let y2 = Math.max(snap.curMouseY, snap.mouseDownY); - x1 -= 1; - x2 += 1; - y1 -= 1; - y2 += 1; - x1 -= OFFSET_X; - x2 -= OFFSET_X; - y1 -= OFFSET_Y; - y2 -= OFFSET_Y; - ctx.fillRect(x1, y1, x2 - x1, y2 - y1); - } - for (let [qx, qy] of snap.boxHighlightPreview) { - let [x, y] = c2dCoordTransform(qx, qy); - ctx.fillRect(x - rad, y - rad, rad*2, rad*2); - } - }); - }); - - drawTimeline(ctx, snap, propagatedMarkerLayers, qubitDrawCoords, circuit.layers.length); - - // Draw scrubber. - ctx.save(); - try { - ctx.strokeStyle = 'black'; - ctx.translate(Math.floor(ctx.canvas.width / 2), 0); - for (let k = 0; k < circuit.layers.length; k++) { - let hasPolygons = false; - let hasXMarker = false; - let hasYMarker = false; - let hasZMarker = false; - let hasResetOperations = circuit.layers[k].hasResetOperations(); - let hasMeasurements = circuit.layers[k].hasMeasurementOperations(); - let hasTwoQubitGate = false; - let hasMultiQubitGate = false; - let hasSingleQubitClifford = circuit.layers[k].hasSingleQubitCliffords(); - for (let op of circuit.layers[k].markers) { - hasPolygons |= op.gate.name === "POLYGON"; - hasXMarker |= op.gate.name === "MARKX"; - hasYMarker |= op.gate.name === "MARKY"; - hasZMarker |= op.gate.name === "MARKZ"; - } - for (let op of circuit.layers[k].id_ops.values()) { - hasTwoQubitGate |= op.id_targets.length === 2; - hasMultiQubitGate |= op.id_targets.length > 2; - } - ctx.fillStyle = 'white'; - ctx.fillRect(k * 8, 0, 8, 20); - if (hasSingleQubitClifford) { - ctx.fillStyle = '#FF0'; - ctx.fillRect(k * 8, 0, 8, 20); - } else if (hasPolygons) { - ctx.fillStyle = '#FBB'; - ctx.fillRect(k * 8, 0, 8, 7); - ctx.fillStyle = '#BFB'; - ctx.fillRect(k * 8, 7, 8, 7); - ctx.fillStyle = '#BBF'; - ctx.fillRect(k * 8, 14, 8, 6); - } - if (hasMeasurements) { - ctx.fillStyle = '#DDD'; - ctx.fillRect(k * 8, 0, 8, 20); - } else if (hasResetOperations) { - ctx.fillStyle = '#DDD'; - ctx.fillRect(k * 8, 0, 4, 20); - } - if (hasXMarker) { - ctx.fillStyle = 'red'; - ctx.fillRect(k * 8 + 3, 14, 3, 3); - } - if (hasYMarker) { - ctx.fillStyle = 'green'; - ctx.fillRect(k * 8 + 3, 9, 3, 3); - } - if (hasZMarker) { - ctx.fillStyle = 'blue'; - ctx.fillRect(k * 8 + 3, 3, 3, 3); - } - if (hasMultiQubitGate) { - ctx.strokeStyle = 'black'; - ctx.beginPath(); - let x = k * 8 + 0.5; - for (let dx of [3, 5]) { - ctx.moveTo(x + dx, 6); - ctx.lineTo(x + dx, 15); - } - ctx.stroke(); - } - if (hasTwoQubitGate) { - ctx.strokeStyle = 'black'; - ctx.beginPath(); - ctx.moveTo(k * 8 + 0.5 + 4, 6); - ctx.lineTo(k * 8 + 0.5 + 4, 15); - ctx.stroke(); - } - } - ctx.fillStyle = 'black'; - ctx.beginPath(); - ctx.moveTo(snap.curLayer * 8 + 0.5 + 4, 16); - ctx.lineTo(snap.curLayer * 8 + 0.5 - 2, 28); - ctx.lineTo(snap.curLayer * 8 + 0.5 + 10, 28); - ctx.closePath(); - ctx.fill(); - - for (let k = 0; k < circuit.layers.length; k++) { - let has_errors = ![...propagatedMarkerLayers.values()].every(p => p.atLayer(k).errors.size === 0); - let hasOps = circuit.layers[k].id_ops.size > 0 || circuit.layers[k].markers.length > 0; - if (has_errors) { - ctx.strokeStyle = 'magenta'; - ctx.lineWidth = 4; - ctx.strokeRect(k*8 + 0.5 - 1, 0.5 - 1, 7 + 2, 20 + 2); - ctx.lineWidth = 1; - } else { - ctx.strokeStyle = '#000'; - ctx.strokeRect(k*8 + 0.5, 0.5, 8, 20); - } - } - } finally { - ctx.restore(); - } + ctx.save(); } -export {xyToPos, draw, setDefensiveDrawEnabled, OFFSET_X, OFFSET_Y} +export { xyToPos, draw, setDefensiveDrawEnabled, OFFSET_X, OFFSET_Y }; diff --git a/glue/crumble/draw/timeline_viewer.js b/glue/crumble/draw/timeline_viewer.js index 2bab08cfb..2087ec369 100644 --- a/glue/crumble/draw/timeline_viewer.js +++ b/glue/crumble/draw/timeline_viewer.js @@ -1,8 +1,30 @@ -import {OFFSET_Y, rad} from "./config.js"; -import {stroke_connector_to} from "../gates/gate_draw_util.js" -import {marker_placement} from '../gates/gateset_markers.js'; +/** + * Copyright 2023 Craig Gidney + * Copyright 2025 Riverlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications: + * - Refactored for CrumPy + */ + +import { indentCircuitLines, OFFSET_Y, rad } from "./config.js"; +import { stroke_connector_to } from "../gates/gate_draw_util.js"; +import { marker_placement } from "../gates/gateset_markers.js"; let TIMELINE_PITCH = 32; +let PADDING_VERTICAL = rad; +let MAX_CANVAS_WIDTH = 4096; /** * @param {!CanvasRenderingContext2D} ctx @@ -15,79 +37,89 @@ let TIMELINE_PITCH = 32; * @param {!number} x_pitch * @param {!Map} hitCounts */ -function drawTimelineMarkers(ctx, ds, qubitTimeCoordFunc, propagatedMarkers, mi, min_t, max_t, x_pitch, hitCounts) { - for (let t = min_t - 1; t <= max_t; t++) { - if (!hitCounts.has(t)) { - hitCounts.set(t, new Map()); - } - let hitCount = hitCounts.get(t); - let p1 = propagatedMarkers.atLayer(t + 0.5); - let p0 = propagatedMarkers.atLayer(t); - for (let [q, b] of p1.bases.entries()) { - let {dx, dy, wx, wy} = marker_placement(mi, q, hitCount); - if (mi >= 0 && mi < 4) { - dx = 0; - wx = x_pitch; - wy = 5; - if (mi === 0) { - dy = 10; - } else if (mi === 1) { - dy = 5; - } else if (mi === 2) { - dy = 0; - } else if (mi === 3) { - dy = -5; - } - } else { - dx -= x_pitch / 2; - } - let [x, y] = qubitTimeCoordFunc(q, t); - if (x === undefined || y === undefined) { - continue; - } - if (b === 'X') { - ctx.fillStyle = 'red' - } else if (b === 'Y') { - ctx.fillStyle = 'green' - } else if (b === 'Z') { - ctx.fillStyle = 'blue' - } else { - throw new Error('Not a pauli: ' + b); - } - ctx.fillRect(x - dx, y - dy, wx, wy); - } - for (let q of p0.errors) { - let {dx, dy, wx, wy} = marker_placement(mi, q, hitCount); - dx -= x_pitch / 2; - - let [x, y] = qubitTimeCoordFunc(q, t - 0.5); - if (x === undefined || y === undefined) { - continue; - } - ctx.strokeStyle = 'magenta'; - ctx.lineWidth = 8; - ctx.strokeRect(x - dx, y - dy, wx, wy); - ctx.lineWidth = 1; - ctx.fillStyle = 'black'; - ctx.fillRect(x - dx, y - dy, wx, wy); - } - for (let {q1, q2, color} of p0.crossings) { - let [x1, y1] = qubitTimeCoordFunc(q1, t); - let [x2, y2] = qubitTimeCoordFunc(q2, t); - if (color === 'X') { - ctx.strokeStyle = 'red'; - } else if (color === 'Y') { - ctx.strokeStyle = 'green'; - } else if (color === 'Z') { - ctx.strokeStyle = 'blue'; - } else { - ctx.strokeStyle = 'purple' - } - ctx.lineWidth = 8; - stroke_connector_to(ctx, x1, y1, x2, y2); - ctx.lineWidth = 1; +function drawTimelineMarkers( + ctx, + ds, + qubitTimeCoordFunc, + propagatedMarkers, + mi, + min_t, + max_t, + x_pitch, + hitCounts, +) { + for (let t = min_t - 1; t <= max_t; t++) { + if (!hitCounts.has(t)) { + hitCounts.set(t, new Map()); + } + let hitCount = hitCounts.get(t); + let p1 = propagatedMarkers.atLayer(t + 0.5); + let p0 = propagatedMarkers.atLayer(t); + for (let [q, b] of p1.bases.entries()) { + let { dx, dy, wx, wy } = marker_placement(mi, q, hitCount); + if (mi >= 0 && mi < 4) { + dx = 0; + wx = x_pitch; + wy = 5; + if (mi === 0) { + dy = 10; + } else if (mi === 1) { + dy = 5; + } else if (mi === 2) { + dy = 0; + } else if (mi === 3) { + dy = -5; } + } else { + dx -= x_pitch / 2; + } + let [x, y] = qubitTimeCoordFunc(q, t); + if (x === undefined || y === undefined) { + continue; + } + if (b === "X") { + ctx.fillStyle = "red"; + } else if (b === "Y") { + ctx.fillStyle = "green"; + } else if (b === "Z") { + ctx.fillStyle = "blue"; + } else { + throw new Error("Not a pauli: " + b); + } + ctx.fillRect(x - dx, y - dy, wx, wy); } + for (let q of p0.errors) { + let { dx, dy, wx, wy } = marker_placement(mi, q, hitCount); + dx -= x_pitch / 2; + + let [x, y] = qubitTimeCoordFunc(q, t - 0.5); + if (x === undefined || y === undefined) { + continue; + } + ctx.strokeStyle = "magenta"; + ctx.lineWidth = 8; + ctx.strokeRect(x - dx, y - dy, wx, wy); + ctx.lineWidth = 1; + ctx.fillStyle = "black"; + ctx.fillRect(x - dx, y - dy, wx, wy); + } + for (let { q1, q2, color } of p0.crossings) { + let [x1, y1] = qubitTimeCoordFunc(q1, t); + let [x2, y2] = qubitTimeCoordFunc(q2, t); + if (color === "X") { + ctx.strokeStyle = "red"; + } else if (color === "Y") { + ctx.strokeStyle = "green"; + } else if (color === "Z") { + ctx.strokeStyle = "blue"; + } else { + ctx.strokeStyle = "purple"; + } + ctx.lineWidth = 8; + stroke_connector_to(ctx, x1, y1, x2, y2); + ctx.lineWidth = 1; + } + } } /** @@ -97,136 +129,169 @@ function drawTimelineMarkers(ctx, ds, qubitTimeCoordFunc, propagatedMarkers, mi, * @param {!function(!int): ![!number, !number]} timesliceQubitCoordsFunc * @param {!int} numLayers */ -function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFunc, numLayers) { - let w = Math.floor(ctx.canvas.width / 2); - - let qubits = snap.timelineQubits(); - qubits.sort((a, b) => { - let [x1, y1] = timesliceQubitCoordsFunc(a); - let [x2, y2] = timesliceQubitCoordsFunc(b); - if (y1 !== y2) { - return y1 - y2; - } - return x1 - x2; - }); - - let base_y2xy = new Map(); - let prev_y = undefined; - let cur_x = 0; - let cur_y = 0; - let max_run = 0; - let cur_run = 0; - for (let q of qubits) { - let [x, y] = timesliceQubitCoordsFunc(q); - cur_y += TIMELINE_PITCH; - if (prev_y !== y) { - prev_y = y; - cur_x = w * 1.5; - max_run = Math.max(max_run, cur_run); - cur_run = 0; - cur_y += TIMELINE_PITCH * 0.25; - } else { - cur_x += rad * 0.25; - cur_run++; - } - base_y2xy.set(`${x},${y}`, [Math.round(cur_x) + 0.5, Math.round(cur_y) + 0.5]); +function drawTimeline( + ctx, + snap, + propagatedMarkerLayers, + timesliceQubitCoordsFunc, + numLayers, +) { + let w = MAX_CANVAS_WIDTH; + + let qubits = snap.timelineQubits(); + qubits.sort((a, b) => { + let [x1, y1] = timesliceQubitCoordsFunc(a); + let [x2, y2] = timesliceQubitCoordsFunc(b); + if (y1 !== y2) { + return y1 - y2; } + return x1 - x2; + }); - let x_pitch = TIMELINE_PITCH + Math.ceil(rad*max_run*0.25); - let num_cols_half = Math.floor(ctx.canvas.width / 4 / x_pitch); - let min_t_free = snap.curLayer - num_cols_half + 1; - let min_t_clamp = Math.max(0, Math.min(min_t_free, numLayers - num_cols_half*2 + 1)); - let max_t = Math.min(min_t_clamp + num_cols_half*2 + 2, numLayers); - let t2t = t => { - let dt = t - snap.curLayer; - dt -= min_t_clamp - min_t_free; - return dt*x_pitch; + let base_y2xy = new Map(); + let prev_y = undefined; + let cur_x = 0; + let cur_y = PADDING_VERTICAL + rad; + let max_run = 0; + let cur_run = 0; + for (let q of qubits) { + let [x, y] = timesliceQubitCoordsFunc(q); + if (prev_y !== y) { + cur_x = w / 2; + max_run = Math.max(max_run, cur_run); + cur_run = 0; + if (prev_y !== undefined) { + // first qubit's y is at initial cur_y value + cur_y += TIMELINE_PITCH * 0.25; + } + prev_y = y; + } else { + if (indentCircuitLines) { + cur_x += rad * 0.25; // slight x offset between qubits in a row + } + cur_run++; } - let coordTransform_t = ([x, y, t]) => { - let key = `${x},${y}`; - if (!base_y2xy.has(key)) { - return [undefined, undefined]; - } - let [xb, yb] = base_y2xy.get(key); - return [xb + t2t(t), yb]; - }; - let qubitTimeCoords = (q, t) => { - let [x, y] = timesliceQubitCoordsFunc(q); - return coordTransform_t([x, y, t]); + base_y2xy.set(`${x},${y}`, [ + Math.round(cur_x) + 0.5, + Math.round(cur_y) + 0.5, + ]); + cur_y += TIMELINE_PITCH; + } + + let x_pitch = TIMELINE_PITCH + Math.ceil(rad * max_run * 0.25); + let num_cols_half = Math.floor(w / 2 / x_pitch); + let min_t_free = snap.curLayer - num_cols_half + 1; + let min_t_clamp = Math.max( + 0, + Math.min(min_t_free, numLayers - num_cols_half * 2 + 1), + ); + let max_t = Math.min(min_t_clamp + num_cols_half * 2 + 2, numLayers); + let t2t = (t) => { + let dt = t - snap.curLayer; + dt -= min_t_clamp - min_t_free; + return dt * x_pitch; + }; + let coordTransform_t = ([x, y, t]) => { + let key = `${x},${y}`; + if (!base_y2xy.has(key)) { + return [undefined, undefined]; } + let [xb, yb] = base_y2xy.get(key); + return [xb + t2t(t), yb]; + }; + let qubitTimeCoords = (q, t) => { + let [x, y] = timesliceQubitCoordsFunc(q); + return coordTransform_t([x, y, t]); + }; - ctx.save(); - try { - ctx.clearRect(w, 0, w, ctx.canvas.height); + ctx.save(); - // Draw colored indicators showing Pauli propagation. - let hitCounts = new Map(); - for (let [mi, p] of propagatedMarkerLayers.entries()) { - drawTimelineMarkers(ctx, snap, qubitTimeCoords, p, mi, min_t_clamp, max_t, x_pitch, hitCounts); - } + // Using coords function, see if any qubit labels would get cut off + let maxLabelWidth = 0; + let topLeftX = qubitTimeCoords(qubits[0], min_t_clamp - 1)[0]; + for (let q of qubits) { + let [x, y] = qubitTimeCoords(q, min_t_clamp - 1); + let qx = snap.circuit.qubitCoordData[q * 2]; + let qy = snap.circuit.qubitCoordData[q * 2 + 1]; + let label = `${qx},${qy}:`; + let labelWidth = ctx.measureText(label).width; + let labelWidthFromTop = labelWidth - (x - topLeftX); + maxLabelWidth = Math.max(maxLabelWidth, labelWidthFromTop); + } + let textOverflowLen = Math.max(0, maxLabelWidth - topLeftX); - // Draw highlight of current layer. - ctx.globalAlpha *= 0.5; - ctx.fillStyle = 'black'; - { - let x1 = t2t(snap.curLayer) + w * 1.5 - x_pitch / 2; - ctx.fillRect(x1, 0, x_pitch, ctx.canvas.height); - } - ctx.globalAlpha *= 2; - - ctx.strokeStyle = 'black'; - ctx.fillStyle = 'black'; - - // Draw wire lines. - for (let q of qubits) { - let [x0, y0] = qubitTimeCoords(q, min_t_clamp - 1); - let [x1, y1] = qubitTimeCoords(q, max_t + 1); - ctx.beginPath(); - ctx.moveTo(x0, y0); - ctx.lineTo(x1, y1); - ctx.stroke(); - } + // Adjust coords function to ensure all qubit labels fit on canvas (+ small pad) + let labelShiftedQTC = (q, t) => { + let [x, y] = qubitTimeCoords(q, t); + return [x + Math.ceil(textOverflowLen) + 3, y]; + }; - // Draw wire labels. - ctx.textAlign = 'right'; - ctx.textBaseline = 'middle'; - for (let q of qubits) { - let [x, y] = qubitTimeCoords(q, min_t_clamp - 1); - let qx = snap.circuit.qubitCoordData[q * 2]; - let qy = snap.circuit.qubitCoordData[q * 2 + 1]; - ctx.fillText(`${qx},${qy}:`, x, y); - } + // Resize canvas to fit circuit + let timelineHeight = + labelShiftedQTC(qubits.at(-1), max_t + 1)[1] + rad + PADDING_VERTICAL; // y of lowest qubit line + padding + let timelineWidth = Math.max( + ...qubits.map((q) => labelShiftedQTC(q, max_t + 1)[0]), + ); // max x of any qubit line's endpoint + ctx.canvas.width = Math.floor(timelineWidth); + ctx.canvas.height = Math.floor(timelineHeight); - // Draw layers of gates. - for (let time = min_t_clamp; time <= max_t; time++) { - let qubitsCoordsFuncForLayer = q => qubitTimeCoords(q, time); - let layer = snap.circuit.layers[time]; - if (layer === undefined) { - continue; - } - for (let op of layer.iter_gates_and_markers()) { - op.id_draw(qubitsCoordsFuncForLayer, ctx); - } - } + try { + ctx.fillStyle = "white"; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); - // Draw links to timeslice viewer. - ctx.globalAlpha = 0.5; - for (let q of qubits) { - let [x0, y0] = qubitTimeCoords(q, min_t_clamp - 1); - let [x1, y1] = timesliceQubitCoordsFunc(q); - if (snap.curMouseX > ctx.canvas.width / 2 && snap.curMouseY >= y0 + OFFSET_Y - TIMELINE_PITCH * 0.55 && snap.curMouseY <= y0 + TIMELINE_PITCH * 0.55 + OFFSET_Y) { - ctx.beginPath(); - ctx.moveTo(x0, y0); - ctx.lineTo(x1, y1); - ctx.stroke(); - ctx.fillStyle = 'black'; - ctx.fillRect(x1 - 20, y1 - 20, 40, 40); - ctx.fillRect(ctx.canvas.width / 2, y0 - TIMELINE_PITCH / 3, ctx.canvas.width / 2, TIMELINE_PITCH * 2 / 3); - } - } - } finally { - ctx.restore(); + // Draw colored indicators showing Pauli propagation. + let hitCounts = new Map(); + for (let [mi, p] of propagatedMarkerLayers.entries()) { + drawTimelineMarkers( + ctx, + snap, + labelShiftedQTC, + p, + mi, + min_t_clamp, + max_t, + x_pitch, + hitCounts, + ); + } + + // Draw wire lines. + ctx.strokeStyle = "black"; + ctx.fillStyle = "black"; + for (let q of qubits) { + let [x0, y0] = labelShiftedQTC(q, min_t_clamp - 1); + let [x1, y1] = labelShiftedQTC(q, max_t + 1); + ctx.beginPath(); + ctx.moveTo(x0, y0); + ctx.lineTo(x1, y1); + ctx.stroke(); + } + + // Draw wire labels. + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + for (let q of qubits) { + let [x, y] = labelShiftedQTC(q, min_t_clamp - 1); + let qx = snap.circuit.qubitCoordData[q * 2]; + let qy = snap.circuit.qubitCoordData[q * 2 + 1]; + let label = `${qx},${qy}:`; + ctx.fillText(label, x, y); + } + + // Draw layers of gates. + for (let time = min_t_clamp; time <= max_t; time++) { + let qubitsCoordsFuncForLayer = (q) => labelShiftedQTC(q, time); + let layer = snap.circuit.layers[time]; + if (layer === undefined) { + continue; + } + for (let op of layer.iter_gates_and_markers()) { + op.id_draw(qubitsCoordsFuncForLayer, ctx); + } } + } finally { + ctx.restore(); + } } -export {drawTimeline} \ No newline at end of file +export { drawTimeline }; diff --git a/glue/crumble/gates/gate_draw_util.js b/glue/crumble/gates/gate_draw_util.js index 54bb30bcc..3384acff6 100644 --- a/glue/crumble/gates/gate_draw_util.js +++ b/glue/crumble/gates/gate_draw_util.js @@ -1,4 +1,24 @@ -import {pitch, rad} from "../draw/config.js" +/** + * Copyright 2023 Craig Gidney + * Copyright 2025 Riverlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications: + * - Refactored for CrumPy + */ + +import { curveConnectors, pitch, rad } from "../draw/config.js"; /** * @param {!CanvasRenderingContext2D} ctx @@ -6,25 +26,25 @@ import {pitch, rad} from "../draw/config.js" * @param {undefined|!number} y */ function draw_x_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - - ctx.strokeStyle = 'black'; - ctx.fillStyle = 'white'; - ctx.beginPath(); - ctx.arc(x, y, rad, 0, 2 * Math.PI); - ctx.fill(); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(x, y - rad); - ctx.lineTo(x, y + rad); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(x - rad, y); - ctx.lineTo(x + rad, y); - ctx.stroke(); + if (x === undefined || y === undefined) { + return; + } + + ctx.strokeStyle = "black"; + ctx.fillStyle = "white"; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(x, y - rad); + ctx.lineTo(x, y + rad); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(x - rad, y); + ctx.lineTo(x + rad, y); + ctx.stroke(); } /** @@ -33,18 +53,18 @@ function draw_x_control(ctx, x, y) { * @param {undefined|!number} y */ function draw_y_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - ctx.strokeStyle = 'black'; - ctx.fillStyle = '#AAA'; - ctx.beginPath(); - ctx.moveTo(x, y + rad); - ctx.lineTo(x + rad, y - rad); - ctx.lineTo(x - rad, y - rad); - ctx.lineTo(x, y + rad); - ctx.stroke(); - ctx.fill(); + if (x === undefined || y === undefined) { + return; + } + ctx.strokeStyle = "black"; + ctx.fillStyle = "#AAA"; + ctx.beginPath(); + ctx.moveTo(x, y + rad); + ctx.lineTo(x + rad, y - rad); + ctx.lineTo(x - rad, y - rad); + ctx.lineTo(x, y + rad); + ctx.stroke(); + ctx.fill(); } /** @@ -53,13 +73,13 @@ function draw_y_control(ctx, x, y) { * @param {undefined|!number} y */ function draw_z_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - ctx.fillStyle = 'black'; - ctx.beginPath(); - ctx.arc(x, y, rad, 0, 2 * Math.PI); - ctx.fill(); + if (x === undefined || y === undefined) { + return; + } + ctx.fillStyle = "black"; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, 2 * Math.PI); + ctx.fill(); } /** @@ -68,27 +88,27 @@ function draw_z_control(ctx, x, y) { * @param {undefined|!number} y */ function draw_xswap_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - ctx.fillStyle = 'white'; - ctx.strokeStyle = 'black'; - ctx.beginPath(); - ctx.arc(x, y, rad, 0, 2 * Math.PI); - ctx.fill(); - ctx.stroke(); - - let r = rad * 0.4; - ctx.strokeStyle = 'black'; - ctx.lineWidth = 3; - ctx.beginPath(); - ctx.moveTo(x - r, y - r); - ctx.lineTo(x + r, y + r); - ctx.stroke(); - ctx.moveTo(x - r, y + r); - ctx.lineTo(x + r, y - r); - ctx.stroke(); - ctx.lineWidth = 1; + if (x === undefined || y === undefined) { + return; + } + ctx.fillStyle = "white"; + ctx.strokeStyle = "black"; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + + let r = rad * 0.4; + ctx.strokeStyle = "black"; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(x - r, y - r); + ctx.lineTo(x + r, y + r); + ctx.stroke(); + ctx.moveTo(x - r, y + r); + ctx.lineTo(x + r, y - r); + ctx.stroke(); + ctx.lineWidth = 1; } /** @@ -97,27 +117,27 @@ function draw_xswap_control(ctx, x, y) { * @param {undefined|!number} y */ function draw_zswap_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - ctx.fillStyle = 'black'; - ctx.strokeStyle = 'black'; - ctx.beginPath(); - ctx.arc(x, y, rad, 0, 2 * Math.PI); - ctx.fill(); - ctx.stroke(); - - let r = rad * 0.4; - ctx.strokeStyle = 'white'; - ctx.lineWidth = 3; - ctx.beginPath(); - ctx.moveTo(x - r, y - r); - ctx.lineTo(x + r, y + r); - ctx.stroke(); - ctx.moveTo(x - r, y + r); - ctx.lineTo(x + r, y - r); - ctx.stroke(); - ctx.lineWidth = 1; + if (x === undefined || y === undefined) { + return; + } + ctx.fillStyle = "black"; + ctx.strokeStyle = "black"; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + + let r = rad * 0.4; + ctx.strokeStyle = "white"; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(x - r, y - r); + ctx.lineTo(x + r, y + r); + ctx.stroke(); + ctx.moveTo(x - r, y + r); + ctx.lineTo(x + r, y - r); + ctx.stroke(); + ctx.lineWidth = 1; } /** @@ -126,27 +146,27 @@ function draw_zswap_control(ctx, x, y) { * @param {undefined|!number} y */ function draw_iswap_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - ctx.fillStyle = '#888'; - ctx.strokeStyle = '#222'; - ctx.beginPath(); - ctx.arc(x, y, rad, 0, 2 * Math.PI); - ctx.fill(); - ctx.stroke(); - - let r = rad * 0.4; - ctx.lineWidth = 3; - ctx.strokeStyle = 'black'; - ctx.beginPath(); - ctx.moveTo(x - r, y - r); - ctx.lineTo(x + r, y + r); - ctx.stroke(); - ctx.moveTo(x - r, y + r); - ctx.lineTo(x + r, y - r); - ctx.stroke(); - ctx.lineWidth = 1; + if (x === undefined || y === undefined) { + return; + } + ctx.fillStyle = "#888"; + ctx.strokeStyle = "#222"; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + + let r = rad * 0.4; + ctx.lineWidth = 3; + ctx.strokeStyle = "black"; + ctx.beginPath(); + ctx.moveTo(x - r, y - r); + ctx.lineTo(x + r, y + r); + ctx.stroke(); + ctx.moveTo(x - r, y + r); + ctx.lineTo(x + r, y - r); + ctx.stroke(); + ctx.lineWidth = 1; } /** @@ -155,18 +175,18 @@ function draw_iswap_control(ctx, x, y) { * @param {undefined|!number} y */ function draw_swap_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - let r = rad / 3; - ctx.strokeStyle = 'black'; - ctx.beginPath(); - ctx.moveTo(x - r, y - r); - ctx.lineTo(x + r, y + r); - ctx.stroke(); - ctx.moveTo(x - r, y + r); - ctx.lineTo(x + r, y - r); - ctx.stroke(); + if (x === undefined || y === undefined) { + return; + } + let r = rad / 3; + ctx.strokeStyle = "black"; + ctx.beginPath(); + ctx.moveTo(x - r, y - r); + ctx.lineTo(x + r, y + r); + ctx.stroke(); + ctx.moveTo(x - r, y + r); + ctx.lineTo(x + r, y - r); + ctx.stroke(); } /** @@ -175,11 +195,11 @@ function draw_swap_control(ctx, x, y) { * @param {undefined|!number} y */ function stroke_degenerate_connector(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - let r = rad * 1.1; - ctx.strokeRect(x - r, y - r, r * 2, r * 2); + if (x === undefined || y === undefined) { + return; + } + let r = rad * 1.1; + ctx.strokeRect(x - r, y - r, r * 2, r * 2); } /** @@ -190,32 +210,45 @@ function stroke_degenerate_connector(ctx, x, y) { * @param {undefined|!number} y2 */ function stroke_connector_to(ctx, x1, y1, x2, y2) { - if (x1 === undefined || y1 === undefined || x2 === undefined || y2 === undefined) { - stroke_degenerate_connector(ctx, x1, y1); - stroke_degenerate_connector(ctx, x2, y2); - return; - } - if (x2 < x1 || (x2 === x1 && y2 < y1)) { - stroke_connector_to(ctx, x2, y2, x1, y1); - return; - } - - let dx = x2 - x1; - let dy = y2 - y1; - let d = Math.sqrt(dx*dx + dy*dy); - let ux = dx / d * 14; - let uy = dy / d * 14; - let px = uy; - let py = -ux; - - ctx.beginPath(); - ctx.moveTo(x1, y1); - if (d < pitch * 1.1) { - ctx.lineTo(x2, y2); - } else { - ctx.bezierCurveTo(x1 + ux + px, y1 + uy + py, x2 - ux + px, y2 - uy + py, x2, y2); - } - ctx.stroke(); + if ( + x1 === undefined || + y1 === undefined || + x2 === undefined || + y2 === undefined + ) { + stroke_degenerate_connector(ctx, x1, y1); + stroke_degenerate_connector(ctx, x2, y2); + return; + } + if (x2 < x1 || (x2 === x1 && y2 < y1)) { + stroke_connector_to(ctx, x2, y2, x1, y1); + return; + } + + let dx = x2 - x1; + let dy = y2 - y1; + let d = Math.sqrt(dx * dx + dy * dy); + let ux = (dx / d) * 14; + let uy = (dy / d) * 14; + let px = uy; + let py = -ux; + + ctx.beginPath(); + ctx.moveTo(x1, y1); + if (!curveConnectors || d < pitch * 1.1) { + ctx.lineTo(x2, y2); + } else { + // curve connectors between far lines + ctx.bezierCurveTo( + x1 + ux + px, + y1 + uy + py, + x2 - ux + px, + y2 - uy + py, + x2, + y2, + ); + } + ctx.stroke(); } /** @@ -226,20 +259,20 @@ function stroke_connector_to(ctx, x1, y1, x2, y2) { * @param {undefined|!number} y2 */ function draw_connector(ctx, x1, y1, x2, y2) { - ctx.lineWidth = 2; - ctx.strokeStyle = 'black'; - stroke_connector_to(ctx, x1, y1, x2, y2); - ctx.lineWidth = 1; + ctx.lineWidth = 2; + ctx.strokeStyle = "black"; + stroke_connector_to(ctx, x1, y1, x2, y2); + ctx.lineWidth = 1; } export { - draw_x_control, - draw_y_control, - draw_z_control, - draw_swap_control, - draw_iswap_control, - stroke_connector_to, - draw_connector, - draw_xswap_control, - draw_zswap_control, + draw_x_control, + draw_y_control, + draw_z_control, + draw_swap_control, + draw_iswap_control, + stroke_connector_to, + draw_connector, + draw_xswap_control, + draw_zswap_control, }; diff --git a/glue/crumble/main.js b/glue/crumble/main.js index 1d16f559d..d9d4dd42f 100644 --- a/glue/crumble/main.js +++ b/glue/crumble/main.js @@ -1,527 +1,105 @@ -import {Circuit} from "./circuit/circuit.js" -import {minXY} from "./circuit/layer.js" -import {pitch} from "./draw/config.js" -import {GATE_MAP} from "./gates/gateset.js" -import {EditorState} from "./editor/editor_state.js"; -import {initUrlCircuitSync} from "./editor/sync_url_to_state.js"; -import {draw} from "./draw/main_draw.js"; -import {drawToolbox} from "./keyboard/toolbox.js"; -import {Operation} from "./circuit/operation.js"; -import {make_mpp_gate} from './gates/gateset_mpp.js'; -import {PropagatedPauliFrames} from './circuit/propagated_pauli_frames.js'; - -const OFFSET_X = -pitch + Math.floor(pitch / 4) + 0.5; -const OFFSET_Y = -pitch + Math.floor(pitch / 4) + 0.5; - -const btnInsertLayer = /** @type{!HTMLButtonElement} */ document.getElementById('btnInsertLayer'); -const btnDeleteLayer = /** @type{!HTMLButtonElement} */ document.getElementById('btnDeleteLayer'); -const btnUndo = /** @type{!HTMLButtonElement} */ document.getElementById('btnUndo'); -const btnRedo = /** @type{!HTMLButtonElement} */ document.getElementById('btnRedo'); -const btnClearMarkers = /** @type{!HTMLButtonElement} */ document.getElementById('btnClearMarkers'); -const btnImportExport = /** @type{!HTMLButtonElement} */ document.getElementById('btnShowHideImportExport'); -const btnNextLayer = /** @type{!HTMLButtonElement} */ document.getElementById('btnNextLayer'); -const btnPrevLayer = /** @type{!HTMLButtonElement} */ document.getElementById('btnPrevLayer'); -const btnRotate45 = /** @type{!HTMLButtonElement} */ document.getElementById('btnRotate45'); -const btnRotate45Counter = /** @type{!HTMLButtonElement} */ document.getElementById('btnRotate45Counter'); -const btnExport = /** @type {!HTMLButtonElement} */ document.getElementById('btnExport'); -const btnImport = /** @type {!HTMLButtonElement} */ document.getElementById('btnImport'); -const btnClear = /** @type {!HTMLButtonElement} */ document.getElementById('clear'); -const txtStimCircuit = /** @type {!HTMLTextAreaElement} */ document.getElementById('txtStimCircuit'); -const btnTimelineFocus = /** @type{!HTMLButtonElement} */ document.getElementById('btnTimelineFocus'); -const btnClearTimelineFocus = /** @type{!HTMLButtonElement} */ document.getElementById('btnClearTimelineFocus'); -const btnClearSelectedMarkers = /** @type{!HTMLButtonElement} */ document.getElementById('btnClearSelectedMarkers'); -const btnShowExamples = /** @type {!HTMLButtonElement} */ document.getElementById('btnShowExamples'); -const divExamples = /** @type{!HTMLDivElement} */ document.getElementById('examples-div'); - -// Prevent typing in the import/export text editor from causing changes in the main circuit editor. -txtStimCircuit.addEventListener('keyup', ev => ev.stopPropagation()); -txtStimCircuit.addEventListener('keydown', ev => ev.stopPropagation()); +/** + * Copyright 2023 Craig Gidney + * Copyright 2025 Riverlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications: + * - Refactored for CrumPy + */ -let editorState = /** @type {!EditorState} */ new EditorState(document.getElementById('cvn')); +import { Circuit } from "./circuit/circuit.js"; +import { draw } from "./draw/main_draw.js"; +import { EditorState } from "./editor/editor_state.js"; +import { + setIndentCircuitLines, + setCurveConnectors, + setShowAnnotationRegions, +} from "./draw/config.js"; + +const CANVAS_W = 600; +const CANVAS_H = 300; + +function initCanvas(el) { + let scrollWrap = document.createElement("div"); + scrollWrap.setAttribute("style", "overflow-x: auto; overflow-y: hidden;"); + + let canvas = document.createElement("canvas"); + canvas.id = "cvn"; + canvas.setAttribute("style", `margin: 0; padding: 0;`); + canvas.tabIndex = 0; + canvas.width = CANVAS_W; + canvas.height = CANVAS_H; + + scrollWrap.appendChild(canvas); + el.appendChild(scrollWrap); + + return canvas; +} -btnExport.addEventListener('click', _ev => { - exportCurrentState(); -}); -btnImport.addEventListener('click', _ev => { - let text = txtStimCircuit.value; - let circuit = Circuit.fromStimCircuit(text); +function render({ model, el }) { + const traitlets = { + getStim: () => model.get("stim"), + getIndentCircuitLines: () => model.get("indentCircuitLines"), + getCurveConnectors: () => model.get("curveConnectors"), + getShowAnnotationRegions: () => model.get("showAnnotationRegions"), + }; + + const canvas = initCanvas(el); + + let editorState = /** @type {!EditorState} */ new EditorState(canvas); + + const exportCurrentState = () => + editorState + .copyOfCurCircuit() + .toStimCircuit() + .replaceAll("\nPOLYGON", "\n#!pragma POLYGON") + .replaceAll("\nERR", "\n#!pragma ERR") + .replaceAll("\nMARK", "\n#!pragma MARK"); + function commitStimCircuit(stim_str) { + let circuit = Circuit.fromStimCircuit(stim_str); editorState.commit(circuit); -}); - -btnImportExport.addEventListener('click', _ev => { - let div = /** @type{!HTMLDivElement} */ document.getElementById('divImportExport'); - if (div.style.display === 'none') { - div.style.display = 'block'; - btnImportExport.textContent = "Hide Import/Export"; - exportCurrentState(); - } else { - div.style.display = 'none'; - btnImportExport.textContent = "Show Import/Export"; - txtStimCircuit.value = ''; - } - setTimeout(() => { - window.scrollTo(0, 0); - }, 0); -}); - -btnClear.addEventListener('click', _ev => { - editorState.clearCircuit(); -}); - -btnUndo.addEventListener('click', _ev => { - editorState.undo(); -}); - -btnTimelineFocus.addEventListener('click', _ev => { - editorState.timelineSet = new Map(editorState.focusedSet.entries()); - editorState.force_redraw(); -}); - -btnClearSelectedMarkers.addEventListener('click', _ev => { - editorState.unmarkFocusInferBasis(false); - editorState.force_redraw(); -}); - -btnShowExamples.addEventListener('click', _ev => { - if (divExamples.style.display === 'none') { - divExamples.style.display = 'block'; - btnShowExamples.textContent = "Hide Example Circuits"; - } else { - divExamples.style.display = 'none'; - btnShowExamples.textContent = "Show Example Circuits"; - } -}); - -btnClearTimelineFocus.addEventListener('click', _ev => { - editorState.timelineSet = new Map(); - editorState.force_redraw(); -}); + } -btnRedo.addEventListener('click', _ev => { - editorState.redo(); -}); - -btnClearMarkers.addEventListener('click', _ev => { - editorState.clearMarkers(); -}); - -btnRotate45.addEventListener('click', _ev => { - editorState.rotate45(+1, false); -}); -btnRotate45Counter.addEventListener('click', _ev => { - editorState.rotate45(-1, false); -}); - -btnInsertLayer.addEventListener('click', _ev => { - editorState.insertLayer(false); -}); -btnDeleteLayer.addEventListener('click', _ev => { - editorState.deleteCurLayer(false); -}); - -btnNextLayer.addEventListener('click', _ev => { - editorState.changeCurLayerTo(editorState.curLayer + 1); -}); -btnPrevLayer.addEventListener('click', _ev => { - editorState.changeCurLayerTo(editorState.curLayer - 1); -}); - -window.addEventListener('resize', _ev => { - editorState.canvas.width = editorState.canvas.scrollWidth; - editorState.canvas.height = editorState.canvas.scrollHeight; + // Changes to circuit on the Python/notebook side update the JS + model.on("change:stim", () => commitStimCircuit(traitlets.getStim())); + model.on("change:indentCircuitLines", () => { + setIndentCircuitLines(traitlets.getIndentCircuitLines()); editorState.force_redraw(); -}); - -function exportCurrentState() { - let validStimCircuit = editorState.copyOfCurCircuit().toStimCircuit(). - replaceAll('\nPOLYGON', '\n#!pragma POLYGON'). - replaceAll('\nERR', '\n#!pragma ERR'). - replaceAll('\nMARK', '\n#!pragma MARK'); - let txt = txtStimCircuit; - txt.value = validStimCircuit + '\n'; - txt.focus(); - txt.select(); -} - -editorState.canvas.addEventListener('mousemove', ev => { - editorState.curMouseX = ev.offsetX + OFFSET_X; - editorState.curMouseY = ev.offsetY + OFFSET_Y; - - // Scrubber. - let w = editorState.canvas.width / 2; - if (isInScrubber && ev.buttons === 1) { - editorState.changeCurLayerTo(Math.floor((ev.offsetX - w) / 8)); - return; - } - + }); + model.on("change:curveConnectors", () => { + setCurveConnectors(traitlets.getCurveConnectors()); editorState.force_redraw(); -}); - -let isInScrubber = false; -editorState.canvas.addEventListener('mousedown', ev => { - editorState.curMouseX = ev.offsetX + OFFSET_X; - editorState.curMouseY = ev.offsetY + OFFSET_Y; - editorState.mouseDownX = ev.offsetX + OFFSET_X; - editorState.mouseDownY = ev.offsetY + OFFSET_Y; - - // Scrubber. - let w = editorState.canvas.width / 2; - isInScrubber = ev.offsetY < 20 && ev.offsetX > w && ev.buttons === 1; - if (isInScrubber) { - editorState.changeCurLayerTo(Math.floor((ev.offsetX - w) / 8)); - return; - } - + }); + model.on("change:showAnnotationRegions", () => { + setShowAnnotationRegions(traitlets.getShowAnnotationRegions()); editorState.force_redraw(); -}); - -editorState.canvas.addEventListener('mouseup', ev => { - let highlightedArea = editorState.currentPositionsBoxesByMouseDrag(ev.altKey); - editorState.mouseDownX = undefined; - editorState.mouseDownY = undefined; - editorState.curMouseX = ev.offsetX + OFFSET_X; - editorState.curMouseY = ev.offsetY + OFFSET_Y; - editorState.changeFocus(highlightedArea, ev.shiftKey, ev.ctrlKey); - if (ev.buttons === 1) { - isInScrubber = false; - } -}); - -/** - * @return {!Map} - */ -function makeChordHandlers() { - let res = /** @type {!Map} */ new Map(); - - res.set('shift+t', preview => editorState.rotate45(-1, preview)); - res.set('t', preview => editorState.rotate45(+1, preview)); - res.set('escape', () => editorState.clearFocus); - res.set('delete', preview => editorState.deleteAtFocus(preview)); - res.set('backspace', preview => editorState.deleteAtFocus(preview)); - res.set('ctrl+delete', preview => editorState.deleteCurLayer(preview)); - res.set('ctrl+insert', preview => editorState.insertLayer(preview)); - res.set('ctrl+backspace', () => editorState.deleteCurLayer); - res.set('ctrl+z', preview => { if (!preview) editorState.undo() }); - res.set('ctrl+y', preview => { if (!preview) editorState.redo() }); - res.set('ctrl+shift+z', preview => { if (!preview) editorState.redo() }); - res.set('ctrl+c', async preview => { await copyToClipboard(); }); - res.set('ctrl+v', pasteFromClipboard); - res.set('ctrl+x', async preview => { - await copyToClipboard(); - if (editorState.focusedSet.size === 0) { - let c = editorState.copyOfCurCircuit(); - c.layers[editorState.curLayer].id_ops.clear(); - c.layers[editorState.curLayer].markers.length = 0; - editorState.commit_or_preview(c, preview); - } else { - editorState.deleteAtFocus(preview); - } - }); - res.set('l', preview => { - if (!preview) { - editorState.timelineSet = new Map(editorState.focusedSet.entries()); - editorState.force_redraw(); - } - }); - res.set(' ', preview => editorState.unmarkFocusInferBasis(preview)); - - for (let [key, val] of [ - ['1', 0], - ['2', 1], - ['3', 2], - ['4', 3], - ['5', 4], - ['6', 5], - ['7', 6], - ['8', 7], - ['9', 8], - ['0', 9], - ['-', 10], - ['=', 11], - ['\\', 12], - ['`', 13], - ]) { - res.set(`${key}`, preview => editorState.markFocusInferBasis(preview, val)); - res.set(`${key}+x`, preview => editorState.writeGateToFocus(preview, GATE_MAP.get('MARKX').withDefaultArgument(val))); - res.set(`${key}+y`, preview => editorState.writeGateToFocus(preview, GATE_MAP.get('MARKY').withDefaultArgument(val))); - res.set(`${key}+z`, preview => editorState.writeGateToFocus(preview, GATE_MAP.get('MARKZ').withDefaultArgument(val))); - res.set(`${key}+d`, preview => editorState.writeMarkerToDetector(preview, val)); - res.set(`${key}+o`, preview => editorState.writeMarkerToObservable(preview, val)); - res.set(`${key}+j`, preview => editorState.moveDetOrObsAtFocusIntoMarker(preview, val)); - res.set(`${key}+k`, preview => editorState.addDissipativeOverlapToMarkers(preview, val)); - } - - let defaultPolygonAlpha = 0.25; - res.set('p', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 0, 0, defaultPolygonAlpha])); - res.set('alt+p', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 1, 0, defaultPolygonAlpha])); - res.set('shift+p', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 0, 1, defaultPolygonAlpha])); - res.set('p+x', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 0, 0, defaultPolygonAlpha])); - res.set('p+y', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 1, 0, defaultPolygonAlpha])); - res.set('p+z', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 0, 1, defaultPolygonAlpha])); - res.set('p+x+y', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 1, 0, defaultPolygonAlpha])); - res.set('p+x+z', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 0, 1, defaultPolygonAlpha])); - res.set('p+y+z', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 1, 1, defaultPolygonAlpha])); - res.set('p+x+y+z', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 1, 1, defaultPolygonAlpha])); - res.set('m+p+x', preview => editorState.writeGateToFocus(preview, make_mpp_gate("X".repeat(editorState.focusedSet.size)), [])); - res.set('m+p+y', preview => editorState.writeGateToFocus(preview, make_mpp_gate("Y".repeat(editorState.focusedSet.size)), [])); - res.set('m+p+z', preview => editorState.writeGateToFocus(preview, make_mpp_gate("Z".repeat(editorState.focusedSet.size)), [])); - res.set('f', preview => editorState.flipTwoQubitGateOrderAtFocus(preview)); - res.set('g', preview => editorState.reverseLayerOrderFromFocusToEmptyLayer(preview)); - res.set('shift+>', preview => editorState.applyCoordinateTransform((x, y) => [x + 1, y], preview, false)); - res.set('shift+<', preview => editorState.applyCoordinateTransform((x, y) => [x - 1, y], preview, false)); - res.set('shift+v', preview => editorState.applyCoordinateTransform((x, y) => [x, y + 1], preview, false)); - res.set('shift+^', preview => editorState.applyCoordinateTransform((x, y) => [x, y - 1], preview, false)); - res.set('>', preview => editorState.applyCoordinateTransform((x, y) => [x + 1, y], preview, false)); - res.set('<', preview => editorState.applyCoordinateTransform((x, y) => [x - 1, y], preview, false)); - res.set('v', preview => editorState.applyCoordinateTransform((x, y) => [x, y + 1], preview, false)); - res.set('^', preview => editorState.applyCoordinateTransform((x, y) => [x, y - 1], preview, false)); - res.set('.', preview => editorState.applyCoordinateTransform((x, y) => [x + 0.5, y + 0.5], preview, false)); - - /** - * @param {!Array} chords - * @param {!string} name - * @param {undefined|!string=}inverse_name - */ - function addGateChords(chords, name, inverse_name=undefined) { - for (let chord of chords) { - if (res.has(chord)) { - throw new Error("Chord collision: " + chord); - } - res.set(chord, preview => editorState.writeGateToFocus(preview, GATE_MAP.get(name))); - } - if (inverse_name !== undefined) { - addGateChords(chords.map(e => 'shift+' + e), inverse_name); - } - } - - addGateChords(['h', 'h+y', 'h+x+z'], "H", "H"); - addGateChords(['h+z', 'h+x+y'], "H_XY", "H_XY"); - addGateChords(['h+x', 'h+y+z'], "H_YZ", "H_YZ"); - addGateChords(['s+x', 's+y+z'], "SQRT_X", "SQRT_X_DAG"); - addGateChords(['s+y', 's+x+z'], "SQRT_Y", "SQRT_Y_DAG"); - addGateChords(['s', 's+z', 's+x+y'], "S", "S_DAG"); - addGateChords(['r+x', 'r+y+z'], "RX"); - addGateChords(['r+y', 'r+x+z'], "RY"); - addGateChords(['r', 'r+z', 'r+x+y'], "R"); - addGateChords(['m+x', 'm+y+z'], "MX"); - addGateChords(['m+y', 'm+x+z'], "MY"); - addGateChords(['m', 'm+z', 'm+x+y'], "M"); - addGateChords(['m+r+x', 'm+r+y+z'], "MRX"); - addGateChords(['m+r+y', 'm+r+x+z'], "MRY"); - addGateChords(['m+r', 'm+r+z', 'm+r+x+y'], "MR"); - addGateChords(['c'], "CX", "CX"); - addGateChords(['c+x'], "CX", "CX"); - addGateChords(['c+y'], "CY", "CY"); - addGateChords(['c+z'], "CZ", "CZ"); - addGateChords(['j+x'], "X", "X"); - addGateChords(['j+y'], "Y", "Y"); - addGateChords(['j+z'], "Z", "Z"); - addGateChords(['c+x+y'], "XCY", "XCY"); - addGateChords(['alt+c+x'], "XCX", "XCX"); - addGateChords(['alt+c+y'], "YCY", "YCY"); - - addGateChords(['w'], "SWAP", "SWAP"); - addGateChords(['w+x'], "CXSWAP", undefined); - addGateChords(['c+w+x'], "CXSWAP", undefined); - addGateChords(['i+w'], "ISWAP", "ISWAP_DAG"); - addGateChords(['w+z'], "CZSWAP", undefined); - addGateChords(['c+w+z'], "CZSWAP", undefined); - addGateChords(['c+w'], "CZSWAP", undefined); - - addGateChords(['c+t'], "C_XYZ", "C_ZYX"); - addGateChords(['c+s+x'], "SQRT_XX", "SQRT_XX_DAG"); - addGateChords(['c+s+y'], "SQRT_YY", "SQRT_YY_DAG"); - addGateChords(['c+s+z'], "SQRT_ZZ", "SQRT_ZZ_DAG"); - addGateChords(['c+s'], "SQRT_ZZ", "SQRT_ZZ_DAG"); - - addGateChords(['c+m+x'], "MXX", "MXX"); - addGateChords(['c+m+y'], "MYY", "MYY"); - addGateChords(['c+m+z'], "MZZ", "MZZ"); - addGateChords(['c+m'], "MZZ", "MZZ"); - - return res; -} - -let fallbackEmulatedClipboard = undefined; -async function copyToClipboard() { - let c = editorState.copyOfCurCircuit(); - c.layers = [c.layers[editorState.curLayer]] - if (editorState.focusedSet.size > 0) { - c.layers[0] = c.layers[0].id_filteredByQubit(q => { - let x = c.qubitCoordData[q * 2]; - let y = c.qubitCoordData[q * 2 + 1]; - return editorState.focusedSet.has(`${x},${y}`); - }); - let [x, y] = minXY(editorState.focusedSet.values()); - c = c.shifted(-x, -y); - } - - let content = c.toStimCircuit() - fallbackEmulatedClipboard = content; - try { - await navigator.clipboard.writeText(content); - } catch (ex) { - console.warn("Failed to write to clipboard. Using fallback emulated clipboard.", ex); - } -} - -/** - * @param {!boolean} preview - */ -async function pasteFromClipboard(preview) { - let text; - try { - text = await navigator.clipboard.readText(); - } catch (ex) { - console.warn("Failed to read from clipboard. Using fallback emulated clipboard.", ex); - text = fallbackEmulatedClipboard; - } - if (text === undefined) { - return; - } - - let pastedCircuit = Circuit.fromStimCircuit(text); - if (pastedCircuit.layers.length !== 1) { - throw new Error(text); - } - let newCircuit = editorState.copyOfCurCircuit(); - if (editorState.focusedSet.size > 0) { - let [x, y] = minXY(editorState.focusedSet.values()); - pastedCircuit = pastedCircuit.shifted(x, y); - } - - // Include new coordinates. - let usedCoords = []; - for (let q = 0; q < pastedCircuit.qubitCoordData.length; q += 2) { - usedCoords.push([pastedCircuit.qubitCoordData[q], pastedCircuit.qubitCoordData[q + 1]]); - } - newCircuit = newCircuit.withCoordsIncluded(usedCoords); - let c2q = newCircuit.coordToQubitMap(); - - // Remove existing content at paste location. - for (let key of editorState.focusedSet.keys()) { - let q = c2q.get(key); - if (q !== undefined) { - newCircuit.layers[editorState.curLayer].id_pop_at(q); - } - } - - // Add content to paste location. - for (let op of pastedCircuit.layers[0].iter_gates_and_markers()) { - let newTargets = []; - for (let q of op.id_targets) { - let x = pastedCircuit.qubitCoordData[2*q]; - let y = pastedCircuit.qubitCoordData[2*q+1]; - newTargets.push(c2q.get(`${x},${y}`)); - } - newCircuit.layers[editorState.curLayer].put(new Operation( - op.gate, - op.tag, - op.args, - new Uint32Array(newTargets), - )); - } - - editorState.commit_or_preview(newCircuit, preview); -} - -const CHORD_HANDLERS = makeChordHandlers(); -/** - * @param {!KeyboardEvent} ev - */ -function handleKeyboardEvent(ev) { - editorState.chorder.handleKeyEvent(ev); - if (ev.type === 'keydown') { - if (ev.key.toLowerCase() === 'q') { - let d = ev.shiftKey ? 5 : 1; - editorState.changeCurLayerTo(editorState.curLayer - d); - return; - } - if (ev.key.toLowerCase() === 'e') { - let d = ev.shiftKey ? 5 : 1; - editorState.changeCurLayerTo(editorState.curLayer + d); - return; - } - if (ev.key === 'Home') { - editorState.changeCurLayerTo(0); - ev.preventDefault(); - return; - } - if (ev.key === 'End') { - editorState.changeCurLayerTo(editorState.copyOfCurCircuit().layers.length - 1); - ev.preventDefault(); - return; - } - } - let evs = editorState.chorder.queuedEvents; - if (evs.length === 0) { - return; - } - let chord_ev = evs[evs.length - 1]; - while (evs.length > 0) { - evs.pop(); - } - - let pressed = [...chord_ev.chord]; - if (pressed.length === 0) { - return; - } - pressed.sort(); - let key = ''; - if (chord_ev.altKey) { - key += 'alt+'; - } - if (chord_ev.ctrlKey) { - key += 'ctrl+'; - } - if (chord_ev.metaKey) { - key += 'meta+'; - } - if (chord_ev.shiftKey) { - key += 'shift+'; - } - for (let e of pressed) { - key += `${e}+`; - } - key = key.substring(0, key.length - 1); - - let handler = CHORD_HANDLERS.get(key); - if (handler !== undefined) { - handler(chord_ev.inProgress); - ev.preventDefault(); - } else { - editorState.preview(editorState.copyOfCurCircuit()); - } -} - -document.addEventListener('keydown', handleKeyboardEvent); -document.addEventListener('keyup', handleKeyboardEvent); + }); -editorState.canvas.width = editorState.canvas.scrollWidth; -editorState.canvas.height = editorState.canvas.scrollHeight; -editorState.rev.changes().subscribe(() => { + // Listeners on editor state that trigger redraws + editorState.rev.changes().subscribe(() => { editorState.obs_val_draw_state.set(editorState.toSnapshot(undefined)); - drawToolbox(editorState.chorder.toEvent(false)); -}); -initUrlCircuitSync(editorState.rev); -editorState.obs_val_draw_state.observable().subscribe(ds => requestAnimationFrame(() => draw(editorState.canvas.getContext('2d'), ds))); -window.addEventListener('focus', () => { - editorState.chorder.handleFocusChanged(); -}); -window.addEventListener('blur', () => { - editorState.chorder.handleFocusChanged(); -}); - -// Intercept clicks on the example circuit links, and load them without actually reloading the page, to preserve undo history. -for (let anchor of document.getElementById('examples-div').querySelectorAll('a')) { - anchor.onclick = ev => { - // Don't stop the user from e.g. opening the example in a new tab using ctrl+click. - if (ev.shiftKey || ev.ctrlKey || ev.altKey || ev.button !== 0) { - return undefined; - } - let circuitText = anchor.href.split('#circuit=')[1]; - - editorState.rev.commit(circuitText); - return false; - }; + }); + editorState.obs_val_draw_state.observable().subscribe((ds) => + requestAnimationFrame(() => { + draw(editorState.canvas.getContext("2d"), ds); + }), + ); + + // Configure initial settings and stim + setIndentCircuitLines(traitlets.getIndentCircuitLines()); + setCurveConnectors(traitlets.getCurveConnectors()); + setShowAnnotationRegions(traitlets.getShowAnnotationRegions()); + commitStimCircuit(traitlets.getStim()); } +export default { render }; diff --git a/glue/crumble/package-lock.json b/glue/crumble/package-lock.json new file mode 100644 index 000000000..3ca99961a --- /dev/null +++ b/glue/crumble/package-lock.json @@ -0,0 +1,497 @@ +{ + "name": "crumpy", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "crumpy", + "devDependencies": { + "esbuild": "0.25.8" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" + } + } + } +} diff --git a/glue/crumble/package.json b/glue/crumble/package.json index 3dbc1ca59..142ff4801 100644 --- a/glue/crumble/package.json +++ b/glue/crumble/package.json @@ -1,3 +1,12 @@ { - "type": "module" -} + "name": "crumpy", + "type": "module", + "main": "crumble/main.js", + "scripts": { + "build": "esbuild --bundle --format=esm --outfile=./crumpy/bundle.js ./main.js", + "watch": "npm run build -- --watch" + }, + "devDependencies": { + "esbuild": "0.25.8" + } +} \ No newline at end of file diff --git a/glue/crumble/pyproject.toml b/glue/crumble/pyproject.toml new file mode 100644 index 000000000..b2a53c64e --- /dev/null +++ b/glue/crumble/pyproject.toml @@ -0,0 +1,176 @@ +[build-system] +requires = ["poetry-core>=2.0", "poetry-dynamic-versioning"] +build-backend = "poetry_dynamic_versioning.backend" + + +[project] +name = "crumpy" +authors = [ + { name = "Riverlane", email = "deltakit@riverlane.com" }, +] +description = "A python library for visualizing Crumble circuits in Jupyter" +readme = "README.md" +requires-python = ">=3.11" +classifiers = [ + "Development Status :: 1 - Planning", + "Intended Audience :: Science/Research", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering", + "Typing :: Typed", +] +dynamic = ["version"] +dependencies = [ + "anywidget>=0.9.18", + "cirq>=1.3.0", + "ipywidgets>=8.1.7", + "ply>=3.11", + "qiskit>=2.1.2", + "stim>=1.15.0", + "stimcirq>=1.15.0", + "traitlets>=5.14.3", +] + +[project.urls] +Homepage = "https://github.com/Deltakit/crumpy" +"Bug Tracker" = "https://github.com/Deltakit/crumpy/issues" +Discussions = "https://github.com/Deltakit/crumpy/discussions" +Changelog = "https://github.com/Deltakit/crumpy/releases" + + +[dependency-groups] +test = [ + "pytest >=6", + "pytest-cov >=3", +] +dev = [ + { include-group = "test" }, +] +docs = [ + "sphinx>=7.0", + "myst_parser>=0.13", + "sphinx_copybutton", + "sphinx_autodoc_typehints", + "furo>=2023.08.17", + "nbsphinx>=0.9.6", + "ipykernel>=6.30.1", + "stim>=1.15.0", + "pandoc>=2.4", + "cirq>=1.6.1", + "stimcirq>=1.15.0", +] +[tool.poetry] +packages = [{ include = "crumpy" }] +include = [ + { path = "crumpy/bundle.js", format = ["sdist", "wheel"] }, +] +version = "0.0.0" + +[tool.poetry.requires-plugins] +poetry-dynamic-versioning = { version = ">=1.0.0", extras = ["plugin"] } + +[tool.poetry-dynamic-versioning] +enable = true +substitution.files = ["crumpy/__init__.py"] + +[tool.poetry.group.test.dependencies] +pytest = ">= 6" +pytest-cov = ">= 3" + + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] +xfail_strict = true +filterwarnings = [ + "error", +] +log_cli_level = "INFO" +testpaths = [ + "tests", +] + + +[tool.coverage] +run.source = ["crumpy"] +report.exclude_also = [ + '\.\.\.', + 'if typing.TYPE_CHECKING:', +] + +[tool.mypy] +files = ["crumpy"] +python_version = "3.11" +warn_unused_configs = true +strict = true +enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] +warn_unreachable = true +disallow_untyped_defs = false +disallow_incomplete_defs = false + +[[tool.mypy.overrides]] +module = "crumpy.*" +disallow_untyped_defs = true +disallow_incomplete_defs = true + + +[tool.ruff] +target-version = "py39" +exclude = ["*.ipynb"] + +[tool.ruff.lint] +extend-select = [ + "ARG", # flake8-unused-arguments + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "EM", # flake8-errmsg + "EXE", # flake8-executable + "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + "NPY", # NumPy specific rules + "PD", # pandas-vet + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "RET", # flake8-return + "RUF", # Ruff-specific + "SIM", # flake8-simplify + "T20", # flake8-print + "UP", # pyupgrade + "YTT", # flake8-2020 +] +ignore = [ + "PLR09", # Too many <...> + "PLR2004", # Magic value used in comparison +] +isort.required-imports = ["from __future__ import annotations"] +# Uncomment if using a _compat.typing backport +# typing-modules = ["crumpy._compat.typing"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["T20"] +"noxfile.py" = ["T20"] + + +[tool.pylint] +py-version = "3.11" +ignore-paths = [".*/_version.py"] +reports.output-format = "colorized" +similarities.ignore-imports = "yes" +messages_control.disable = [ + "design", + "fixme", + "line-too-long", + "missing-module-docstring", + "missing-function-docstring", + "wrong-import-position", +] From 677e6503dbd05e4d29d03bb222ffde15f80f1281 Mon Sep 17 00:00:00 2001 From: Guen Prawiroatmodjo <4041805+guenp@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:11:14 -0700 Subject: [PATCH 2/4] Update pyproject.toml Add Sam as author, update links to point to Stim --- glue/crumble/pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/glue/crumble/pyproject.toml b/glue/crumble/pyproject.toml index b2a53c64e..4e37f7626 100644 --- a/glue/crumble/pyproject.toml +++ b/glue/crumble/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "poetry_dynamic_versioning.backend" [project] name = "crumpy" authors = [ - { name = "Riverlane", email = "deltakit@riverlane.com" }, + { name = "Sam Zappa (Riverlane)", email = "deltakit@riverlane.com" }, ] description = "A python library for visualizing Crumble circuits in Jupyter" readme = "README.md" @@ -38,10 +38,10 @@ dependencies = [ ] [project.urls] -Homepage = "https://github.com/Deltakit/crumpy" -"Bug Tracker" = "https://github.com/Deltakit/crumpy/issues" -Discussions = "https://github.com/Deltakit/crumpy/discussions" -Changelog = "https://github.com/Deltakit/crumpy/releases" +Homepage = "https://github.com/quantumlib/Stim" +"Bug Tracker" = "https://github.com/quantumlib/Stim/issues" +Discussions = "https://github.com/quantumlib/Stim/discussions" +Changelog = "https://github.com/quantumlib/Stim/releases" [dependency-groups] From 76f73e32903ff878670b6a18c0a424deeefba530 Mon Sep 17 00:00:00 2001 From: Guen Prawiroatmodjo <4041805+guenp@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:47:28 -0700 Subject: [PATCH 3/4] chore: Move crumpy package to separate folder (#1) --- .gitignore | 5 +- glue/crumble/draw/config.js | 56 +- glue/crumble/draw/main_draw.js | 637 +++++++++++------- glue/crumble/draw/timeline_viewer.js | 453 ++++++------- glue/crumble/gates/gate_draw_util.js | 347 +++++----- glue/crumble/main.js | 612 ++++++++++++++--- glue/crumble/package.json | 13 +- glue/crumpy/README.md | 177 +++++ glue/crumpy/getting_started.ipynb | 551 +++++++++++++++ glue/{crumble => crumpy}/pyproject.toml | 9 +- glue/crumpy/simple_example.ipynb | 119 ++++ .../src}/crumpy/__init__.py | 2 +- glue/{crumble => crumpy/src}/crumpy/py.typed | 0 glue/crumpy/src/js/draw/config.js | 63 ++ glue/crumpy/src/js/draw/main_draw.js | 273 ++++++++ glue/crumpy/src/js/draw/timeline_viewer.js | 282 ++++++++ glue/crumpy/src/js/main.js | 105 +++ .../src/js}/package-lock.json | 0 glue/crumpy/src/js/package.json | 15 + .../crumpy => crumpy/tests}/test_package.py | 0 .../crumpy => crumpy/tests}/test_widget.py | 0 21 files changed, 2876 insertions(+), 843 deletions(-) create mode 100644 glue/crumpy/README.md create mode 100644 glue/crumpy/getting_started.ipynb rename glue/{crumble => crumpy}/pyproject.toml (93%) create mode 100644 glue/crumpy/simple_example.ipynb rename glue/{crumble => crumpy/src}/crumpy/__init__.py (98%) rename glue/{crumble => crumpy/src}/crumpy/py.typed (100%) create mode 100644 glue/crumpy/src/js/draw/config.js create mode 100644 glue/crumpy/src/js/draw/main_draw.js create mode 100644 glue/crumpy/src/js/draw/timeline_viewer.js create mode 100644 glue/crumpy/src/js/main.js rename glue/{crumble => crumpy/src/js}/package-lock.json (100%) create mode 100644 glue/crumpy/src/js/package.json rename glue/{crumble/crumpy => crumpy/tests}/test_package.py (100%) rename glue/{crumble/crumpy => crumpy/tests}/test_widget.py (100%) diff --git a/.gitignore b/.gitignore index c7e8eaf63..a6f13f16a 100644 --- a/.gitignore +++ b/.gitignore @@ -44,5 +44,6 @@ build.ninja node_modules MODULE.bazel.lock .ninja_lock -glue/crumble/node_modules -glue/crumble/crumpy/bundle.js \ No newline at end of file +crumble_js +bundle.js +.ipynb_checkpoints \ No newline at end of file diff --git a/glue/crumble/draw/config.js b/glue/crumble/draw/config.js index 61bd5e866..d0b731d88 100644 --- a/glue/crumble/draw/config.js +++ b/glue/crumble/draw/config.js @@ -1,61 +1,7 @@ -/** - * Copyright 2023 Craig Gidney - * Copyright 2025 Riverlane - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Modifications: - * - Refactored for CrumPy - */ - const pitch = 50; const rad = 10; const OFFSET_X = -pitch + Math.floor(pitch / 4) + 0.5; const OFFSET_Y = -pitch + Math.floor(pitch / 4) + 0.5; -let indentCircuitLines = true; let curveConnectors = true; -let showAnnotationRegions = true; - -const setIndentCircuitLines = (newBool) => { - if (typeof newBool !== "boolean") { - throw new TypeError(`Expected a boolean, but got ${typeof newBool}`); - } - indentCircuitLines = newBool; -}; - -const setCurveConnectors = (newBool) => { - if (typeof newBool !== "boolean") { - throw new TypeError(`Expected a boolean, but got ${typeof newBool}`); - } - curveConnectors = newBool; -}; - -const setShowAnnotationRegions = (newBool) => { - if (typeof newBool !== "boolean") { - throw new TypeError(`Expected a boolean, but got ${typeof newBool}`); - } - showAnnotationRegions = newBool; -}; -export { - pitch, - rad, - OFFSET_X, - OFFSET_Y, - indentCircuitLines, - curveConnectors, - showAnnotationRegions, - setIndentCircuitLines, - setCurveConnectors, - setShowAnnotationRegions, -}; +export {pitch, rad, OFFSET_X, OFFSET_Y, curveConnectors}; diff --git a/glue/crumble/draw/main_draw.js b/glue/crumble/draw/main_draw.js index 22c7dfca1..4636133e2 100644 --- a/glue/crumble/draw/main_draw.js +++ b/glue/crumble/draw/main_draw.js @@ -1,35 +1,9 @@ -/** - * Copyright 2023 Craig Gidney - * Copyright 2025 Riverlane - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Modifications: - * - Refactored for CrumPy - */ - -import { - pitch, - rad, - showAnnotationRegions, - OFFSET_X, - OFFSET_Y, -} from "./config.js"; -import { marker_placement } from "../gates/gateset_markers.js"; -import { drawTimeline } from "./timeline_viewer.js"; -import { PropagatedPauliFrames } from "../circuit/propagated_pauli_frames.js"; -import { stroke_connector_to } from "../gates/gate_draw_util.js"; -import { beginPathPolygon } from "./draw_util.js"; +import {pitch, rad, OFFSET_X, OFFSET_Y} from "./config.js" +import {marker_placement} from "../gates/gateset_markers.js"; +import {drawTimeline} from "./timeline_viewer.js"; +import {PropagatedPauliFrames} from "../circuit/propagated_pauli_frames.js"; +import {stroke_connector_to} from "../gates/gate_draw_util.js" +import {beginPathPolygon} from './draw_util.js'; /** * @param {!number|undefined} x @@ -37,23 +11,19 @@ import { beginPathPolygon } from "./draw_util.js"; * @return {![undefined, undefined]|![!number, !number]} */ function xyToPos(x, y) { - if (x === undefined || y === undefined) { + if (x === undefined || y === undefined) { + return [undefined, undefined]; + } + let focusX = x / pitch; + let focusY = y / pitch; + let roundedX = Math.floor(focusX * 2 + 0.5) / 2; + let roundedY = Math.floor(focusY * 2 + 0.5) / 2; + let centerX = roundedX*pitch; + let centerY = roundedY*pitch; + if (Math.abs(centerX - x) <= rad && Math.abs(centerY - y) <= rad && roundedX % 1 === roundedY % 1) { + return [roundedX, roundedY]; + } return [undefined, undefined]; - } - let focusX = x / pitch; - let focusY = y / pitch; - let roundedX = Math.floor(focusX * 2 + 0.5) / 2; - let roundedY = Math.floor(focusY * 2 + 0.5) / 2; - let centerX = roundedX * pitch; - let centerY = roundedY * pitch; - if ( - Math.abs(centerX - x) <= rad && - Math.abs(centerY - y) <= rad && - roundedX % 1 === roundedY % 1 - ) { - return [roundedX, roundedY]; - } - return [undefined, undefined]; } /** @@ -64,25 +34,25 @@ function xyToPos(x, y) { * @param {!int} mi */ function drawCrossMarkers(ctx, snap, qubitCoordsFunc, propagatedMarkers, mi) { - let crossings = propagatedMarkers.atLayer(snap.curLayer).crossings; - if (crossings !== undefined) { - for (let { q1, q2, color } of crossings) { - let [x1, y1] = qubitCoordsFunc(q1); - let [x2, y2] = qubitCoordsFunc(q2); - if (color === "X") { - ctx.strokeStyle = "red"; - } else if (color === "Y") { - ctx.strokeStyle = "green"; - } else if (color === "Z") { - ctx.strokeStyle = "blue"; - } else { - ctx.strokeStyle = "purple"; - } - ctx.lineWidth = 8; - stroke_connector_to(ctx, x1, y1, x2, y2); - ctx.lineWidth = 1; + let crossings = propagatedMarkers.atLayer(snap.curLayer).crossings; + if (crossings !== undefined) { + for (let {q1, q2, color} of crossings) { + let [x1, y1] = qubitCoordsFunc(q1); + let [x2, y2] = qubitCoordsFunc(q2); + if (color === 'X') { + ctx.strokeStyle = 'red'; + } else if (color === 'Y') { + ctx.strokeStyle = 'green'; + } else if (color === 'Z') { + ctx.strokeStyle = 'blue'; + } else { + ctx.strokeStyle = 'purple' + } + ctx.lineWidth = 8; + stroke_connector_to(ctx, x1, y1, x2, y2); + ctx.lineWidth = 1; + } } - } } /** @@ -92,11 +62,11 @@ function drawCrossMarkers(ctx, snap, qubitCoordsFunc, propagatedMarkers, mi) { * @param {!Map} propagatedMarkerLayers */ function drawMarkers(ctx, snap, qubitCoordsFunc, propagatedMarkerLayers) { - let obsCount = new Map(); - let detCount = new Map(); - for (let [mi, p] of propagatedMarkerLayers.entries()) { - drawSingleMarker(ctx, snap, qubitCoordsFunc, p, mi, obsCount, detCount); - } + let obsCount = new Map(); + let detCount = new Map(); + for (let [mi, p] of propagatedMarkerLayers.entries()) { + drawSingleMarker(ctx, snap, qubitCoordsFunc, p, mi, obsCount, detCount); + } } /** @@ -107,96 +77,89 @@ function drawMarkers(ctx, snap, qubitCoordsFunc, propagatedMarkerLayers) { * @param {!int} mi * @param {!Map} hitCount */ -function drawSingleMarker( - ctx, - snap, - qubitCoordsFunc, - propagatedMarkers, - mi, - hitCount, -) { - let basesQubitMap = propagatedMarkers.atLayer(snap.curLayer + 0.5).bases; - - // Convert qubit indices to draw coordinates. - let basisCoords = []; - for (let [q, b] of basesQubitMap.entries()) { - basisCoords.push([b, qubitCoordsFunc(q)]); - } +function drawSingleMarker(ctx, snap, qubitCoordsFunc, propagatedMarkers, mi, hitCount) { + let basesQubitMap = propagatedMarkers.atLayer(snap.curLayer + 0.5).bases; - // Draw a polygon for the marker set. - if (mi >= 0 && basisCoords.length > 0) { - if (basisCoords.every((e) => e[0] === "X")) { - ctx.fillStyle = "red"; - } else if (basisCoords.every((e) => e[0] === "Y")) { - ctx.fillStyle = "green"; - } else if (basisCoords.every((e) => e[0] === "Z")) { - ctx.fillStyle = "blue"; - } else { - ctx.fillStyle = "black"; + // Convert qubit indices to draw coordinates. + let basisCoords = []; + for (let [q, b] of basesQubitMap.entries()) { + basisCoords.push([b, qubitCoordsFunc(q)]); } - ctx.strokeStyle = ctx.fillStyle; - let coords = basisCoords.map((e) => e[1]); - let cx = 0; - let cy = 0; - for (let [x, y] of coords) { - cx += x; - cy += y; + + // Draw a polygon for the marker set. + if (mi >= 0 && basisCoords.length > 0) { + if (basisCoords.every(e => e[0] === 'X')) { + ctx.fillStyle = 'red'; + } else if (basisCoords.every(e => e[0] === 'Y')) { + ctx.fillStyle = 'green'; + } else if (basisCoords.every(e => e[0] === 'Z')) { + ctx.fillStyle = 'blue'; + } else { + ctx.fillStyle = 'black'; + } + ctx.strokeStyle = ctx.fillStyle; + let coords = basisCoords.map(e => e[1]); + let cx = 0; + let cy = 0; + for (let [x, y] of coords) { + cx += x; + cy += y; + } + cx /= coords.length; + cy /= coords.length; + coords.sort((a, b) => { + let [ax, ay] = a; + let [bx, by] = b; + let av = Math.atan2(ay - cy, ax - cx); + let bv = Math.atan2(by - cy, bx - cx); + if (ax === cx && ay === cy) { + av = -100; + } + if (bx === cx && by === cy) { + bv = -100; + } + return av - bv; + }) + beginPathPolygon(ctx, coords); + ctx.globalAlpha *= 0.25; + ctx.fill(); + ctx.globalAlpha *= 4; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.lineWidth = 1; } - cx /= coords.length; - cy /= coords.length; - coords.sort((a, b) => { - let [ax, ay] = a; - let [bx, by] = b; - let av = Math.atan2(ay - cy, ax - cx); - let bv = Math.atan2(by - cy, bx - cx); - if (ax === cx && ay === cy) { - av = -100; - } - if (bx === cx && by === cy) { - bv = -100; - } - return av - bv; - }); - beginPathPolygon(ctx, coords); - ctx.globalAlpha *= 0.25; - ctx.fill(); - ctx.globalAlpha *= 4; - ctx.lineWidth = 2; - ctx.stroke(); - ctx.lineWidth = 1; - } - // Draw individual qubit markers. - for (let [b, [x, y]] of basisCoords) { - let { dx, dy, wx, wy } = marker_placement(mi, `${x}:${y}`, hitCount); - if (b === "X") { - ctx.fillStyle = "red"; - } else if (b === "Y") { - ctx.fillStyle = "green"; - } else if (b === "Z") { - ctx.fillStyle = "blue"; - } else { - throw new Error("Not a pauli: " + b); + // Draw individual qubit markers. + for (let [b, [x, y]] of basisCoords) { + let {dx, dy, wx, wy} = marker_placement(mi, `${x}:${y}`, hitCount); + if (b === 'X') { + ctx.fillStyle = 'red' + } else if (b === 'Y') { + ctx.fillStyle = 'green' + } else if (b === 'Z') { + ctx.fillStyle = 'blue' + } else { + throw new Error('Not a pauli: ' + b); + } + ctx.fillRect(x - dx, y - dy, wx, wy); } - ctx.fillRect(x - dx, y - dy, wx, wy); - } - // Show error highlights. - let errorsQubitSet = propagatedMarkers.atLayer(snap.curLayer).errors; - for (let q of errorsQubitSet) { - let [x, y] = qubitCoordsFunc(q); - let { dx, dy, wx, wy } = marker_placement(mi, `${x}:${y}`, hitCount); - if (mi < 0) { - ctx.lineWidth = 2; - } else { - ctx.lineWidth = 8; + // Show error highlights. + let errorsQubitSet = propagatedMarkers.atLayer(snap.curLayer).errors; + for (let q of errorsQubitSet) { + let [x, y] = qubitCoordsFunc(q); + let {dx, dy, wx, wy} = marker_placement(mi, `${x}:${y}`, hitCount); + if (mi < 0) { + ctx.lineWidth = 2; + } else { + ctx.lineWidth = 8; + } + ctx.strokeStyle = 'magenta' + ctx.strokeRect(x - dx, y - dy, wx, wy); + ctx.lineWidth = 1; + ctx.fillStyle = 'black' + ctx.fillRect(x - dx, y - dy, wx, wy); } - ctx.strokeStyle = "magenta"; - ctx.strokeRect(x - dx, y - dy, wx, wy); - ctx.lineWidth = 1; - ctx.fillStyle = "black"; - ctx.fillRect(x - dx, y - dy, wx, wy); - } } let _defensive_draw_enabled = true; @@ -205,7 +168,7 @@ let _defensive_draw_enabled = true; * @param {!boolean} val */ function setDefensiveDrawEnabled(val) { - _defensive_draw_enabled = val; + _defensive_draw_enabled = val; } /** @@ -213,20 +176,20 @@ function setDefensiveDrawEnabled(val) { * @param {!function} body */ function defensiveDraw(ctx, body) { - ctx.save(); - try { - if (_defensive_draw_enabled) { - body(); - } else { - try { - body(); - } catch (ex) { - console.error(ex); - } + ctx.save(); + try { + if (_defensive_draw_enabled) { + body(); + } else { + try { + body(); + } catch (ex) { + console.error(ex); + } + } + } finally { + ctx.restore(); } - } finally { - ctx.restore(); - } } /** @@ -234,72 +197,294 @@ function defensiveDraw(ctx, body) { * @param {!StateSnapshot} snap */ function draw(ctx, snap) { - let circuit = snap.circuit; + let circuit = snap.circuit; - let numPropagatedLayers = 0; - for (let layer of circuit.layers) { - for (let op of layer.markers) { - let gate = op.gate; - if ( - gate.name === "MARKX" || - gate.name === "MARKY" || - gate.name === "MARKZ" - ) { - numPropagatedLayers = Math.max(numPropagatedLayers, op.args[0] + 1); - } + let numPropagatedLayers = 0; + for (let layer of circuit.layers) { + for (let op of layer.markers) { + let gate = op.gate; + if (gate.name === "MARKX" || gate.name === "MARKY" || gate.name === "MARKZ") { + numPropagatedLayers = Math.max(numPropagatedLayers, op.args[0] + 1); + } + } } - } - let c2dCoordTransform = (x, y) => [ - x * pitch - OFFSET_X, - y * pitch - OFFSET_Y, - ]; - let qubitDrawCoords = (q) => { - let x = circuit.qubitCoordData[2 * q]; - let y = circuit.qubitCoordData[2 * q + 1]; - return c2dCoordTransform(x, y); - }; - let propagatedMarkerLayers = - /** @type {!Map} */ new Map(); - for (let mi = 0; mi < numPropagatedLayers; mi++) { - propagatedMarkerLayers.set( - mi, - PropagatedPauliFrames.fromCircuit(circuit, mi), - ); - } - - let { dets: dets, obs: obs } = circuit.collectDetectorsAndObservables(false); - let batch_input = []; - for (let mi = 0; mi < dets.length; mi++) { - batch_input.push(dets[mi].mids); - } - for (let mi of obs.keys()) { - batch_input.push(obs.get(mi)); - } - let batch_output = PropagatedPauliFrames.batchFromMeasurements( - circuit, - batch_input, - ); - let batch_index = 0; - - if (showAnnotationRegions) { + let c2dCoordTransform = (x, y) => [x*pitch - OFFSET_X, y*pitch - OFFSET_Y]; + let qubitDrawCoords = q => { + let x = circuit.qubitCoordData[2 * q]; + let y = circuit.qubitCoordData[2 * q + 1]; + return c2dCoordTransform(x, y); + }; + let propagatedMarkerLayers = /** @type {!Map} */ new Map(); + for (let mi = 0; mi < numPropagatedLayers; mi++) { + propagatedMarkerLayers.set(mi, PropagatedPauliFrames.fromCircuit(circuit, mi)); + } + let {dets: dets, obs: obs} = circuit.collectDetectorsAndObservables(false); + let batch_input = []; + for (let mi = 0; mi < dets.length; mi++) { + batch_input.push(dets[mi].mids); + } + for (let mi of obs.keys()) { + batch_input.push(obs.get(mi)); + } + let batch_output = PropagatedPauliFrames.batchFromMeasurements(circuit, batch_input); + let batch_index = 0; for (let mi = 0; mi < dets.length; mi++) { - propagatedMarkerLayers.set(~mi, batch_output[batch_index++]); + propagatedMarkerLayers.set(~mi, batch_output[batch_index++]); } for (let mi of obs.keys()) { - propagatedMarkerLayers.set(~mi ^ (1 << 30), batch_output[batch_index++]); + propagatedMarkerLayers.set(~mi ^ (1 << 30), batch_output[batch_index++]); } - } - drawTimeline( - ctx, - snap, - propagatedMarkerLayers, - qubitDrawCoords, - circuit.layers.length, - ); + let operatedOnQubits = new Set(); + for (let layer of circuit.layers) { + for (let t of layer.id_ops.keys()) { + operatedOnQubits.add(t); + } + } + let usedQubitCoordSet = new Set(); + let operatedOnQubitSet = new Set(); + for (let q of circuit.allQubits()) { + let qx = circuit.qubitCoordData[q * 2]; + let qy = circuit.qubitCoordData[q * 2 + 1]; + usedQubitCoordSet.add(`${qx},${qy}`); + if (operatedOnQubits.has(q)) { + operatedOnQubitSet.add(`${qx},${qy}`); + } + } + + defensiveDraw(ctx, () => { + ctx.fillStyle = 'white'; + ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); + let [focusX, focusY] = xyToPos(snap.curMouseX, snap.curMouseY); + + // Draw the background polygons. + let lastPolygonLayer = snap.curLayer; + for (let r = 0; r <= snap.curLayer; r++) { + for (let op of circuit.layers[r].markers) { + if (op.gate.name === 'POLYGON') { + lastPolygonLayer = r; + break; + } + } + } + let polygonMarkers = [...circuit.layers[lastPolygonLayer].markers]; + polygonMarkers.sort((a, b) => b.id_targets.length - a.id_targets.length); + for (let op of polygonMarkers) { + if (op.gate.name === 'POLYGON') { + op.id_draw(qubitDrawCoords, ctx); + } + } + + // Draw the grid of qubits. + defensiveDraw(ctx, () => { + for (let qx = 0; qx < 100; qx += 0.5) { + let [x, _] = c2dCoordTransform(qx, 0); + let s = `${qx}`; + ctx.fillStyle = 'black'; + ctx.fillText(s, x - ctx.measureText(s).width / 2, 15); + } + for (let qy = 0; qy < 100; qy += 0.5) { + let [_, y] = c2dCoordTransform(0, qy); + let s = `${qy}`; + ctx.fillStyle = 'black'; + ctx.fillText(s, 18 - ctx.measureText(s).width, y); + } + + ctx.strokeStyle = 'black'; + for (let qx = 0; qx < 100; qx += 0.5) { + let [x, _] = c2dCoordTransform(qx, 0); + let s = `${qx}`; + ctx.fillStyle = 'black'; + ctx.fillText(s, x - ctx.measureText(s).width / 2, 15); + for (let qy = qx % 1; qy < 100; qy += 1) { + let [x, y] = c2dCoordTransform(qx, qy); + ctx.fillStyle = 'white'; + let isUnused = !usedQubitCoordSet.has(`${qx},${qy}`); + let isVeryUnused = !operatedOnQubitSet.has(`${qx},${qy}`); + if (isUnused) { + ctx.globalAlpha *= 0.25; + } + if (isVeryUnused) { + ctx.globalAlpha *= 0.25; + } + ctx.fillRect(x - rad, y - rad, 2*rad, 2*rad); + ctx.strokeRect(x - rad, y - rad, 2*rad, 2*rad); + if (isUnused) { + ctx.globalAlpha *= 4; + } + if (isVeryUnused) { + ctx.globalAlpha *= 4; + } + } + } + }); + + for (let [mi, p] of propagatedMarkerLayers.entries()) { + drawCrossMarkers(ctx, snap, qubitDrawCoords, p, mi); + } + + for (let op of circuit.layers[snap.curLayer].iter_gates_and_markers()) { + if (op.gate.name !== 'POLYGON') { + op.id_draw(qubitDrawCoords, ctx); + } + } + + defensiveDraw(ctx, () => { + ctx.globalAlpha *= 0.25 + for (let [qx, qy] of snap.timelineSet.values()) { + let [x, y] = c2dCoordTransform(qx, qy); + ctx.fillStyle = 'yellow'; + ctx.fillRect(x - rad * 1.25, y - rad * 1.25, 2.5*rad, 2.5*rad); + } + }); - ctx.save(); + defensiveDraw(ctx, () => { + ctx.globalAlpha *= 0.5 + for (let [qx, qy] of snap.focusedSet.values()) { + let [x, y] = c2dCoordTransform(qx, qy); + ctx.fillStyle = 'blue'; + ctx.fillRect(x - rad * 1.25, y - rad * 1.25, 2.5*rad, 2.5*rad); + } + }); + + drawMarkers(ctx, snap, qubitDrawCoords, propagatedMarkerLayers); + + if (focusX !== undefined) { + ctx.save(); + ctx.globalAlpha *= 0.5; + let [x, y] = c2dCoordTransform(focusX, focusY); + ctx.fillStyle = 'red'; + ctx.fillRect(x - rad, y - rad, 2*rad, 2*rad); + ctx.restore(); + } + + defensiveDraw(ctx, () => { + ctx.globalAlpha *= 0.25; + ctx.fillStyle = 'blue'; + if (snap.mouseDownX !== undefined && snap.curMouseX !== undefined) { + let x1 = Math.min(snap.curMouseX, snap.mouseDownX); + let x2 = Math.max(snap.curMouseX, snap.mouseDownX); + let y1 = Math.min(snap.curMouseY, snap.mouseDownY); + let y2 = Math.max(snap.curMouseY, snap.mouseDownY); + x1 -= 1; + x2 += 1; + y1 -= 1; + y2 += 1; + x1 -= OFFSET_X; + x2 -= OFFSET_X; + y1 -= OFFSET_Y; + y2 -= OFFSET_Y; + ctx.fillRect(x1, y1, x2 - x1, y2 - y1); + } + for (let [qx, qy] of snap.boxHighlightPreview) { + let [x, y] = c2dCoordTransform(qx, qy); + ctx.fillRect(x - rad, y - rad, rad*2, rad*2); + } + }); + }); + + drawTimeline(ctx, snap, propagatedMarkerLayers, qubitDrawCoords, circuit.layers.length); + + // Draw scrubber. + ctx.save(); + try { + ctx.strokeStyle = 'black'; + ctx.translate(Math.floor(ctx.canvas.width / 2), 0); + for (let k = 0; k < circuit.layers.length; k++) { + let hasPolygons = false; + let hasXMarker = false; + let hasYMarker = false; + let hasZMarker = false; + let hasResetOperations = circuit.layers[k].hasResetOperations(); + let hasMeasurements = circuit.layers[k].hasMeasurementOperations(); + let hasTwoQubitGate = false; + let hasMultiQubitGate = false; + let hasSingleQubitClifford = circuit.layers[k].hasSingleQubitCliffords(); + for (let op of circuit.layers[k].markers) { + hasPolygons |= op.gate.name === "POLYGON"; + hasXMarker |= op.gate.name === "MARKX"; + hasYMarker |= op.gate.name === "MARKY"; + hasZMarker |= op.gate.name === "MARKZ"; + } + for (let op of circuit.layers[k].id_ops.values()) { + hasTwoQubitGate |= op.id_targets.length === 2; + hasMultiQubitGate |= op.id_targets.length > 2; + } + ctx.fillStyle = 'white'; + ctx.fillRect(k * 8, 0, 8, 20); + if (hasSingleQubitClifford) { + ctx.fillStyle = '#FF0'; + ctx.fillRect(k * 8, 0, 8, 20); + } else if (hasPolygons) { + ctx.fillStyle = '#FBB'; + ctx.fillRect(k * 8, 0, 8, 7); + ctx.fillStyle = '#BFB'; + ctx.fillRect(k * 8, 7, 8, 7); + ctx.fillStyle = '#BBF'; + ctx.fillRect(k * 8, 14, 8, 6); + } + if (hasMeasurements) { + ctx.fillStyle = '#DDD'; + ctx.fillRect(k * 8, 0, 8, 20); + } else if (hasResetOperations) { + ctx.fillStyle = '#DDD'; + ctx.fillRect(k * 8, 0, 4, 20); + } + if (hasXMarker) { + ctx.fillStyle = 'red'; + ctx.fillRect(k * 8 + 3, 14, 3, 3); + } + if (hasYMarker) { + ctx.fillStyle = 'green'; + ctx.fillRect(k * 8 + 3, 9, 3, 3); + } + if (hasZMarker) { + ctx.fillStyle = 'blue'; + ctx.fillRect(k * 8 + 3, 3, 3, 3); + } + if (hasMultiQubitGate) { + ctx.strokeStyle = 'black'; + ctx.beginPath(); + let x = k * 8 + 0.5; + for (let dx of [3, 5]) { + ctx.moveTo(x + dx, 6); + ctx.lineTo(x + dx, 15); + } + ctx.stroke(); + } + if (hasTwoQubitGate) { + ctx.strokeStyle = 'black'; + ctx.beginPath(); + ctx.moveTo(k * 8 + 0.5 + 4, 6); + ctx.lineTo(k * 8 + 0.5 + 4, 15); + ctx.stroke(); + } + } + ctx.fillStyle = 'black'; + ctx.beginPath(); + ctx.moveTo(snap.curLayer * 8 + 0.5 + 4, 16); + ctx.lineTo(snap.curLayer * 8 + 0.5 - 2, 28); + ctx.lineTo(snap.curLayer * 8 + 0.5 + 10, 28); + ctx.closePath(); + ctx.fill(); + + for (let k = 0; k < circuit.layers.length; k++) { + let has_errors = ![...propagatedMarkerLayers.values()].every(p => p.atLayer(k).errors.size === 0); + let hasOps = circuit.layers[k].id_ops.size > 0 || circuit.layers[k].markers.length > 0; + if (has_errors) { + ctx.strokeStyle = 'magenta'; + ctx.lineWidth = 4; + ctx.strokeRect(k*8 + 0.5 - 1, 0.5 - 1, 7 + 2, 20 + 2); + ctx.lineWidth = 1; + } else { + ctx.strokeStyle = '#000'; + ctx.strokeRect(k*8 + 0.5, 0.5, 8, 20); + } + } + } finally { + ctx.restore(); + } } -export { xyToPos, draw, setDefensiveDrawEnabled, OFFSET_X, OFFSET_Y }; +export {xyToPos, draw, setDefensiveDrawEnabled, OFFSET_X, OFFSET_Y} diff --git a/glue/crumble/draw/timeline_viewer.js b/glue/crumble/draw/timeline_viewer.js index 2087ec369..2bab08cfb 100644 --- a/glue/crumble/draw/timeline_viewer.js +++ b/glue/crumble/draw/timeline_viewer.js @@ -1,30 +1,8 @@ -/** - * Copyright 2023 Craig Gidney - * Copyright 2025 Riverlane - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Modifications: - * - Refactored for CrumPy - */ - -import { indentCircuitLines, OFFSET_Y, rad } from "./config.js"; -import { stroke_connector_to } from "../gates/gate_draw_util.js"; -import { marker_placement } from "../gates/gateset_markers.js"; +import {OFFSET_Y, rad} from "./config.js"; +import {stroke_connector_to} from "../gates/gate_draw_util.js" +import {marker_placement} from '../gates/gateset_markers.js'; let TIMELINE_PITCH = 32; -let PADDING_VERTICAL = rad; -let MAX_CANVAS_WIDTH = 4096; /** * @param {!CanvasRenderingContext2D} ctx @@ -37,89 +15,79 @@ let MAX_CANVAS_WIDTH = 4096; * @param {!number} x_pitch * @param {!Map} hitCounts */ -function drawTimelineMarkers( - ctx, - ds, - qubitTimeCoordFunc, - propagatedMarkers, - mi, - min_t, - max_t, - x_pitch, - hitCounts, -) { - for (let t = min_t - 1; t <= max_t; t++) { - if (!hitCounts.has(t)) { - hitCounts.set(t, new Map()); - } - let hitCount = hitCounts.get(t); - let p1 = propagatedMarkers.atLayer(t + 0.5); - let p0 = propagatedMarkers.atLayer(t); - for (let [q, b] of p1.bases.entries()) { - let { dx, dy, wx, wy } = marker_placement(mi, q, hitCount); - if (mi >= 0 && mi < 4) { - dx = 0; - wx = x_pitch; - wy = 5; - if (mi === 0) { - dy = 10; - } else if (mi === 1) { - dy = 5; - } else if (mi === 2) { - dy = 0; - } else if (mi === 3) { - dy = -5; +function drawTimelineMarkers(ctx, ds, qubitTimeCoordFunc, propagatedMarkers, mi, min_t, max_t, x_pitch, hitCounts) { + for (let t = min_t - 1; t <= max_t; t++) { + if (!hitCounts.has(t)) { + hitCounts.set(t, new Map()); + } + let hitCount = hitCounts.get(t); + let p1 = propagatedMarkers.atLayer(t + 0.5); + let p0 = propagatedMarkers.atLayer(t); + for (let [q, b] of p1.bases.entries()) { + let {dx, dy, wx, wy} = marker_placement(mi, q, hitCount); + if (mi >= 0 && mi < 4) { + dx = 0; + wx = x_pitch; + wy = 5; + if (mi === 0) { + dy = 10; + } else if (mi === 1) { + dy = 5; + } else if (mi === 2) { + dy = 0; + } else if (mi === 3) { + dy = -5; + } + } else { + dx -= x_pitch / 2; + } + let [x, y] = qubitTimeCoordFunc(q, t); + if (x === undefined || y === undefined) { + continue; + } + if (b === 'X') { + ctx.fillStyle = 'red' + } else if (b === 'Y') { + ctx.fillStyle = 'green' + } else if (b === 'Z') { + ctx.fillStyle = 'blue' + } else { + throw new Error('Not a pauli: ' + b); + } + ctx.fillRect(x - dx, y - dy, wx, wy); + } + for (let q of p0.errors) { + let {dx, dy, wx, wy} = marker_placement(mi, q, hitCount); + dx -= x_pitch / 2; + + let [x, y] = qubitTimeCoordFunc(q, t - 0.5); + if (x === undefined || y === undefined) { + continue; + } + ctx.strokeStyle = 'magenta'; + ctx.lineWidth = 8; + ctx.strokeRect(x - dx, y - dy, wx, wy); + ctx.lineWidth = 1; + ctx.fillStyle = 'black'; + ctx.fillRect(x - dx, y - dy, wx, wy); + } + for (let {q1, q2, color} of p0.crossings) { + let [x1, y1] = qubitTimeCoordFunc(q1, t); + let [x2, y2] = qubitTimeCoordFunc(q2, t); + if (color === 'X') { + ctx.strokeStyle = 'red'; + } else if (color === 'Y') { + ctx.strokeStyle = 'green'; + } else if (color === 'Z') { + ctx.strokeStyle = 'blue'; + } else { + ctx.strokeStyle = 'purple' + } + ctx.lineWidth = 8; + stroke_connector_to(ctx, x1, y1, x2, y2); + ctx.lineWidth = 1; } - } else { - dx -= x_pitch / 2; - } - let [x, y] = qubitTimeCoordFunc(q, t); - if (x === undefined || y === undefined) { - continue; - } - if (b === "X") { - ctx.fillStyle = "red"; - } else if (b === "Y") { - ctx.fillStyle = "green"; - } else if (b === "Z") { - ctx.fillStyle = "blue"; - } else { - throw new Error("Not a pauli: " + b); - } - ctx.fillRect(x - dx, y - dy, wx, wy); - } - for (let q of p0.errors) { - let { dx, dy, wx, wy } = marker_placement(mi, q, hitCount); - dx -= x_pitch / 2; - - let [x, y] = qubitTimeCoordFunc(q, t - 0.5); - if (x === undefined || y === undefined) { - continue; - } - ctx.strokeStyle = "magenta"; - ctx.lineWidth = 8; - ctx.strokeRect(x - dx, y - dy, wx, wy); - ctx.lineWidth = 1; - ctx.fillStyle = "black"; - ctx.fillRect(x - dx, y - dy, wx, wy); - } - for (let { q1, q2, color } of p0.crossings) { - let [x1, y1] = qubitTimeCoordFunc(q1, t); - let [x2, y2] = qubitTimeCoordFunc(q2, t); - if (color === "X") { - ctx.strokeStyle = "red"; - } else if (color === "Y") { - ctx.strokeStyle = "green"; - } else if (color === "Z") { - ctx.strokeStyle = "blue"; - } else { - ctx.strokeStyle = "purple"; - } - ctx.lineWidth = 8; - stroke_connector_to(ctx, x1, y1, x2, y2); - ctx.lineWidth = 1; } - } } /** @@ -129,169 +97,136 @@ function drawTimelineMarkers( * @param {!function(!int): ![!number, !number]} timesliceQubitCoordsFunc * @param {!int} numLayers */ -function drawTimeline( - ctx, - snap, - propagatedMarkerLayers, - timesliceQubitCoordsFunc, - numLayers, -) { - let w = MAX_CANVAS_WIDTH; - - let qubits = snap.timelineQubits(); - qubits.sort((a, b) => { - let [x1, y1] = timesliceQubitCoordsFunc(a); - let [x2, y2] = timesliceQubitCoordsFunc(b); - if (y1 !== y2) { - return y1 - y2; +function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFunc, numLayers) { + let w = Math.floor(ctx.canvas.width / 2); + + let qubits = snap.timelineQubits(); + qubits.sort((a, b) => { + let [x1, y1] = timesliceQubitCoordsFunc(a); + let [x2, y2] = timesliceQubitCoordsFunc(b); + if (y1 !== y2) { + return y1 - y2; + } + return x1 - x2; + }); + + let base_y2xy = new Map(); + let prev_y = undefined; + let cur_x = 0; + let cur_y = 0; + let max_run = 0; + let cur_run = 0; + for (let q of qubits) { + let [x, y] = timesliceQubitCoordsFunc(q); + cur_y += TIMELINE_PITCH; + if (prev_y !== y) { + prev_y = y; + cur_x = w * 1.5; + max_run = Math.max(max_run, cur_run); + cur_run = 0; + cur_y += TIMELINE_PITCH * 0.25; + } else { + cur_x += rad * 0.25; + cur_run++; + } + base_y2xy.set(`${x},${y}`, [Math.round(cur_x) + 0.5, Math.round(cur_y) + 0.5]); } - return x1 - x2; - }); - let base_y2xy = new Map(); - let prev_y = undefined; - let cur_x = 0; - let cur_y = PADDING_VERTICAL + rad; - let max_run = 0; - let cur_run = 0; - for (let q of qubits) { - let [x, y] = timesliceQubitCoordsFunc(q); - if (prev_y !== y) { - cur_x = w / 2; - max_run = Math.max(max_run, cur_run); - cur_run = 0; - if (prev_y !== undefined) { - // first qubit's y is at initial cur_y value - cur_y += TIMELINE_PITCH * 0.25; - } - prev_y = y; - } else { - if (indentCircuitLines) { - cur_x += rad * 0.25; // slight x offset between qubits in a row - } - cur_run++; + let x_pitch = TIMELINE_PITCH + Math.ceil(rad*max_run*0.25); + let num_cols_half = Math.floor(ctx.canvas.width / 4 / x_pitch); + let min_t_free = snap.curLayer - num_cols_half + 1; + let min_t_clamp = Math.max(0, Math.min(min_t_free, numLayers - num_cols_half*2 + 1)); + let max_t = Math.min(min_t_clamp + num_cols_half*2 + 2, numLayers); + let t2t = t => { + let dt = t - snap.curLayer; + dt -= min_t_clamp - min_t_free; + return dt*x_pitch; } - base_y2xy.set(`${x},${y}`, [ - Math.round(cur_x) + 0.5, - Math.round(cur_y) + 0.5, - ]); - cur_y += TIMELINE_PITCH; - } - - let x_pitch = TIMELINE_PITCH + Math.ceil(rad * max_run * 0.25); - let num_cols_half = Math.floor(w / 2 / x_pitch); - let min_t_free = snap.curLayer - num_cols_half + 1; - let min_t_clamp = Math.max( - 0, - Math.min(min_t_free, numLayers - num_cols_half * 2 + 1), - ); - let max_t = Math.min(min_t_clamp + num_cols_half * 2 + 2, numLayers); - let t2t = (t) => { - let dt = t - snap.curLayer; - dt -= min_t_clamp - min_t_free; - return dt * x_pitch; - }; - let coordTransform_t = ([x, y, t]) => { - let key = `${x},${y}`; - if (!base_y2xy.has(key)) { - return [undefined, undefined]; + let coordTransform_t = ([x, y, t]) => { + let key = `${x},${y}`; + if (!base_y2xy.has(key)) { + return [undefined, undefined]; + } + let [xb, yb] = base_y2xy.get(key); + return [xb + t2t(t), yb]; + }; + let qubitTimeCoords = (q, t) => { + let [x, y] = timesliceQubitCoordsFunc(q); + return coordTransform_t([x, y, t]); } - let [xb, yb] = base_y2xy.get(key); - return [xb + t2t(t), yb]; - }; - let qubitTimeCoords = (q, t) => { - let [x, y] = timesliceQubitCoordsFunc(q); - return coordTransform_t([x, y, t]); - }; - - ctx.save(); - - // Using coords function, see if any qubit labels would get cut off - let maxLabelWidth = 0; - let topLeftX = qubitTimeCoords(qubits[0], min_t_clamp - 1)[0]; - for (let q of qubits) { - let [x, y] = qubitTimeCoords(q, min_t_clamp - 1); - let qx = snap.circuit.qubitCoordData[q * 2]; - let qy = snap.circuit.qubitCoordData[q * 2 + 1]; - let label = `${qx},${qy}:`; - let labelWidth = ctx.measureText(label).width; - let labelWidthFromTop = labelWidth - (x - topLeftX); - maxLabelWidth = Math.max(maxLabelWidth, labelWidthFromTop); - } - let textOverflowLen = Math.max(0, maxLabelWidth - topLeftX); - - // Adjust coords function to ensure all qubit labels fit on canvas (+ small pad) - let labelShiftedQTC = (q, t) => { - let [x, y] = qubitTimeCoords(q, t); - return [x + Math.ceil(textOverflowLen) + 3, y]; - }; - // Resize canvas to fit circuit - let timelineHeight = - labelShiftedQTC(qubits.at(-1), max_t + 1)[1] + rad + PADDING_VERTICAL; // y of lowest qubit line + padding - let timelineWidth = Math.max( - ...qubits.map((q) => labelShiftedQTC(q, max_t + 1)[0]), - ); // max x of any qubit line's endpoint - ctx.canvas.width = Math.floor(timelineWidth); - ctx.canvas.height = Math.floor(timelineHeight); + ctx.save(); + try { + ctx.clearRect(w, 0, w, ctx.canvas.height); - try { - ctx.fillStyle = "white"; - ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + // Draw colored indicators showing Pauli propagation. + let hitCounts = new Map(); + for (let [mi, p] of propagatedMarkerLayers.entries()) { + drawTimelineMarkers(ctx, snap, qubitTimeCoords, p, mi, min_t_clamp, max_t, x_pitch, hitCounts); + } - // Draw colored indicators showing Pauli propagation. - let hitCounts = new Map(); - for (let [mi, p] of propagatedMarkerLayers.entries()) { - drawTimelineMarkers( - ctx, - snap, - labelShiftedQTC, - p, - mi, - min_t_clamp, - max_t, - x_pitch, - hitCounts, - ); - } + // Draw highlight of current layer. + ctx.globalAlpha *= 0.5; + ctx.fillStyle = 'black'; + { + let x1 = t2t(snap.curLayer) + w * 1.5 - x_pitch / 2; + ctx.fillRect(x1, 0, x_pitch, ctx.canvas.height); + } + ctx.globalAlpha *= 2; + + ctx.strokeStyle = 'black'; + ctx.fillStyle = 'black'; + + // Draw wire lines. + for (let q of qubits) { + let [x0, y0] = qubitTimeCoords(q, min_t_clamp - 1); + let [x1, y1] = qubitTimeCoords(q, max_t + 1); + ctx.beginPath(); + ctx.moveTo(x0, y0); + ctx.lineTo(x1, y1); + ctx.stroke(); + } - // Draw wire lines. - ctx.strokeStyle = "black"; - ctx.fillStyle = "black"; - for (let q of qubits) { - let [x0, y0] = labelShiftedQTC(q, min_t_clamp - 1); - let [x1, y1] = labelShiftedQTC(q, max_t + 1); - ctx.beginPath(); - ctx.moveTo(x0, y0); - ctx.lineTo(x1, y1); - ctx.stroke(); - } + // Draw wire labels. + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + for (let q of qubits) { + let [x, y] = qubitTimeCoords(q, min_t_clamp - 1); + let qx = snap.circuit.qubitCoordData[q * 2]; + let qy = snap.circuit.qubitCoordData[q * 2 + 1]; + ctx.fillText(`${qx},${qy}:`, x, y); + } - // Draw wire labels. - ctx.textAlign = "right"; - ctx.textBaseline = "middle"; - for (let q of qubits) { - let [x, y] = labelShiftedQTC(q, min_t_clamp - 1); - let qx = snap.circuit.qubitCoordData[q * 2]; - let qy = snap.circuit.qubitCoordData[q * 2 + 1]; - let label = `${qx},${qy}:`; - ctx.fillText(label, x, y); - } + // Draw layers of gates. + for (let time = min_t_clamp; time <= max_t; time++) { + let qubitsCoordsFuncForLayer = q => qubitTimeCoords(q, time); + let layer = snap.circuit.layers[time]; + if (layer === undefined) { + continue; + } + for (let op of layer.iter_gates_and_markers()) { + op.id_draw(qubitsCoordsFuncForLayer, ctx); + } + } - // Draw layers of gates. - for (let time = min_t_clamp; time <= max_t; time++) { - let qubitsCoordsFuncForLayer = (q) => labelShiftedQTC(q, time); - let layer = snap.circuit.layers[time]; - if (layer === undefined) { - continue; - } - for (let op of layer.iter_gates_and_markers()) { - op.id_draw(qubitsCoordsFuncForLayer, ctx); - } + // Draw links to timeslice viewer. + ctx.globalAlpha = 0.5; + for (let q of qubits) { + let [x0, y0] = qubitTimeCoords(q, min_t_clamp - 1); + let [x1, y1] = timesliceQubitCoordsFunc(q); + if (snap.curMouseX > ctx.canvas.width / 2 && snap.curMouseY >= y0 + OFFSET_Y - TIMELINE_PITCH * 0.55 && snap.curMouseY <= y0 + TIMELINE_PITCH * 0.55 + OFFSET_Y) { + ctx.beginPath(); + ctx.moveTo(x0, y0); + ctx.lineTo(x1, y1); + ctx.stroke(); + ctx.fillStyle = 'black'; + ctx.fillRect(x1 - 20, y1 - 20, 40, 40); + ctx.fillRect(ctx.canvas.width / 2, y0 - TIMELINE_PITCH / 3, ctx.canvas.width / 2, TIMELINE_PITCH * 2 / 3); + } + } + } finally { + ctx.restore(); } - } finally { - ctx.restore(); - } } -export { drawTimeline }; +export {drawTimeline} \ No newline at end of file diff --git a/glue/crumble/gates/gate_draw_util.js b/glue/crumble/gates/gate_draw_util.js index 3384acff6..8d2081b01 100644 --- a/glue/crumble/gates/gate_draw_util.js +++ b/glue/crumble/gates/gate_draw_util.js @@ -1,23 +1,3 @@ -/** - * Copyright 2023 Craig Gidney - * Copyright 2025 Riverlane - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Modifications: - * - Refactored for CrumPy - */ - import { curveConnectors, pitch, rad } from "../draw/config.js"; /** @@ -26,25 +6,25 @@ import { curveConnectors, pitch, rad } from "../draw/config.js"; * @param {undefined|!number} y */ function draw_x_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - - ctx.strokeStyle = "black"; - ctx.fillStyle = "white"; - ctx.beginPath(); - ctx.arc(x, y, rad, 0, 2 * Math.PI); - ctx.fill(); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(x, y - rad); - ctx.lineTo(x, y + rad); - ctx.stroke(); - ctx.beginPath(); - ctx.moveTo(x - rad, y); - ctx.lineTo(x + rad, y); - ctx.stroke(); + if (x === undefined || y === undefined) { + return; + } + + ctx.strokeStyle = 'black'; + ctx.fillStyle = 'white'; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(x, y - rad); + ctx.lineTo(x, y + rad); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(x - rad, y); + ctx.lineTo(x + rad, y); + ctx.stroke(); } /** @@ -53,18 +33,18 @@ function draw_x_control(ctx, x, y) { * @param {undefined|!number} y */ function draw_y_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - ctx.strokeStyle = "black"; - ctx.fillStyle = "#AAA"; - ctx.beginPath(); - ctx.moveTo(x, y + rad); - ctx.lineTo(x + rad, y - rad); - ctx.lineTo(x - rad, y - rad); - ctx.lineTo(x, y + rad); - ctx.stroke(); - ctx.fill(); + if (x === undefined || y === undefined) { + return; + } + ctx.strokeStyle = 'black'; + ctx.fillStyle = '#AAA'; + ctx.beginPath(); + ctx.moveTo(x, y + rad); + ctx.lineTo(x + rad, y - rad); + ctx.lineTo(x - rad, y - rad); + ctx.lineTo(x, y + rad); + ctx.stroke(); + ctx.fill(); } /** @@ -73,13 +53,13 @@ function draw_y_control(ctx, x, y) { * @param {undefined|!number} y */ function draw_z_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - ctx.fillStyle = "black"; - ctx.beginPath(); - ctx.arc(x, y, rad, 0, 2 * Math.PI); - ctx.fill(); + if (x === undefined || y === undefined) { + return; + } + ctx.fillStyle = 'black'; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, 2 * Math.PI); + ctx.fill(); } /** @@ -88,27 +68,27 @@ function draw_z_control(ctx, x, y) { * @param {undefined|!number} y */ function draw_xswap_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - ctx.fillStyle = "white"; - ctx.strokeStyle = "black"; - ctx.beginPath(); - ctx.arc(x, y, rad, 0, 2 * Math.PI); - ctx.fill(); - ctx.stroke(); - - let r = rad * 0.4; - ctx.strokeStyle = "black"; - ctx.lineWidth = 3; - ctx.beginPath(); - ctx.moveTo(x - r, y - r); - ctx.lineTo(x + r, y + r); - ctx.stroke(); - ctx.moveTo(x - r, y + r); - ctx.lineTo(x + r, y - r); - ctx.stroke(); - ctx.lineWidth = 1; + if (x === undefined || y === undefined) { + return; + } + ctx.fillStyle = 'white'; + ctx.strokeStyle = 'black'; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + + let r = rad * 0.4; + ctx.strokeStyle = 'black'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(x - r, y - r); + ctx.lineTo(x + r, y + r); + ctx.stroke(); + ctx.moveTo(x - r, y + r); + ctx.lineTo(x + r, y - r); + ctx.stroke(); + ctx.lineWidth = 1; } /** @@ -117,27 +97,27 @@ function draw_xswap_control(ctx, x, y) { * @param {undefined|!number} y */ function draw_zswap_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - ctx.fillStyle = "black"; - ctx.strokeStyle = "black"; - ctx.beginPath(); - ctx.arc(x, y, rad, 0, 2 * Math.PI); - ctx.fill(); - ctx.stroke(); - - let r = rad * 0.4; - ctx.strokeStyle = "white"; - ctx.lineWidth = 3; - ctx.beginPath(); - ctx.moveTo(x - r, y - r); - ctx.lineTo(x + r, y + r); - ctx.stroke(); - ctx.moveTo(x - r, y + r); - ctx.lineTo(x + r, y - r); - ctx.stroke(); - ctx.lineWidth = 1; + if (x === undefined || y === undefined) { + return; + } + ctx.fillStyle = 'black'; + ctx.strokeStyle = 'black'; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + + let r = rad * 0.4; + ctx.strokeStyle = 'white'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.moveTo(x - r, y - r); + ctx.lineTo(x + r, y + r); + ctx.stroke(); + ctx.moveTo(x - r, y + r); + ctx.lineTo(x + r, y - r); + ctx.stroke(); + ctx.lineWidth = 1; } /** @@ -146,27 +126,27 @@ function draw_zswap_control(ctx, x, y) { * @param {undefined|!number} y */ function draw_iswap_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - ctx.fillStyle = "#888"; - ctx.strokeStyle = "#222"; - ctx.beginPath(); - ctx.arc(x, y, rad, 0, 2 * Math.PI); - ctx.fill(); - ctx.stroke(); - - let r = rad * 0.4; - ctx.lineWidth = 3; - ctx.strokeStyle = "black"; - ctx.beginPath(); - ctx.moveTo(x - r, y - r); - ctx.lineTo(x + r, y + r); - ctx.stroke(); - ctx.moveTo(x - r, y + r); - ctx.lineTo(x + r, y - r); - ctx.stroke(); - ctx.lineWidth = 1; + if (x === undefined || y === undefined) { + return; + } + ctx.fillStyle = '#888'; + ctx.strokeStyle = '#222'; + ctx.beginPath(); + ctx.arc(x, y, rad, 0, 2 * Math.PI); + ctx.fill(); + ctx.stroke(); + + let r = rad * 0.4; + ctx.lineWidth = 3; + ctx.strokeStyle = 'black'; + ctx.beginPath(); + ctx.moveTo(x - r, y - r); + ctx.lineTo(x + r, y + r); + ctx.stroke(); + ctx.moveTo(x - r, y + r); + ctx.lineTo(x + r, y - r); + ctx.stroke(); + ctx.lineWidth = 1; } /** @@ -175,18 +155,18 @@ function draw_iswap_control(ctx, x, y) { * @param {undefined|!number} y */ function draw_swap_control(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - let r = rad / 3; - ctx.strokeStyle = "black"; - ctx.beginPath(); - ctx.moveTo(x - r, y - r); - ctx.lineTo(x + r, y + r); - ctx.stroke(); - ctx.moveTo(x - r, y + r); - ctx.lineTo(x + r, y - r); - ctx.stroke(); + if (x === undefined || y === undefined) { + return; + } + let r = rad / 3; + ctx.strokeStyle = 'black'; + ctx.beginPath(); + ctx.moveTo(x - r, y - r); + ctx.lineTo(x + r, y + r); + ctx.stroke(); + ctx.moveTo(x - r, y + r); + ctx.lineTo(x + r, y - r); + ctx.stroke(); } /** @@ -195,11 +175,11 @@ function draw_swap_control(ctx, x, y) { * @param {undefined|!number} y */ function stroke_degenerate_connector(ctx, x, y) { - if (x === undefined || y === undefined) { - return; - } - let r = rad * 1.1; - ctx.strokeRect(x - r, y - r, r * 2, r * 2); + if (x === undefined || y === undefined) { + return; + } + let r = rad * 1.1; + ctx.strokeRect(x - r, y - r, r * 2, r * 2); } /** @@ -210,45 +190,32 @@ function stroke_degenerate_connector(ctx, x, y) { * @param {undefined|!number} y2 */ function stroke_connector_to(ctx, x1, y1, x2, y2) { - if ( - x1 === undefined || - y1 === undefined || - x2 === undefined || - y2 === undefined - ) { - stroke_degenerate_connector(ctx, x1, y1); - stroke_degenerate_connector(ctx, x2, y2); - return; - } - if (x2 < x1 || (x2 === x1 && y2 < y1)) { - stroke_connector_to(ctx, x2, y2, x1, y1); - return; - } - - let dx = x2 - x1; - let dy = y2 - y1; - let d = Math.sqrt(dx * dx + dy * dy); - let ux = (dx / d) * 14; - let uy = (dy / d) * 14; - let px = uy; - let py = -ux; - - ctx.beginPath(); - ctx.moveTo(x1, y1); - if (!curveConnectors || d < pitch * 1.1) { - ctx.lineTo(x2, y2); - } else { - // curve connectors between far lines - ctx.bezierCurveTo( - x1 + ux + px, - y1 + uy + py, - x2 - ux + px, - y2 - uy + py, - x2, - y2, - ); - } - ctx.stroke(); + if (x1 === undefined || y1 === undefined || x2 === undefined || y2 === undefined) { + stroke_degenerate_connector(ctx, x1, y1); + stroke_degenerate_connector(ctx, x2, y2); + return; + } + if (x2 < x1 || (x2 === x1 && y2 < y1)) { + stroke_connector_to(ctx, x2, y2, x1, y1); + return; + } + + let dx = x2 - x1; + let dy = y2 - y1; + let d = Math.sqrt(dx*dx + dy*dy); + let ux = dx / d * 14; + let uy = dy / d * 14; + let px = uy; + let py = -ux; + + ctx.beginPath(); + ctx.moveTo(x1, y1); + if (!curveConnectors || d < pitch * 1.1) { + ctx.lineTo(x2, y2); + } else { + ctx.bezierCurveTo(x1 + ux + px, y1 + uy + py, x2 - ux + px, y2 - uy + py, x2, y2); + } + ctx.stroke(); } /** @@ -259,20 +226,20 @@ function stroke_connector_to(ctx, x1, y1, x2, y2) { * @param {undefined|!number} y2 */ function draw_connector(ctx, x1, y1, x2, y2) { - ctx.lineWidth = 2; - ctx.strokeStyle = "black"; - stroke_connector_to(ctx, x1, y1, x2, y2); - ctx.lineWidth = 1; + ctx.lineWidth = 2; + ctx.strokeStyle = 'black'; + stroke_connector_to(ctx, x1, y1, x2, y2); + ctx.lineWidth = 1; } export { - draw_x_control, - draw_y_control, - draw_z_control, - draw_swap_control, - draw_iswap_control, - stroke_connector_to, - draw_connector, - draw_xswap_control, - draw_zswap_control, + draw_x_control, + draw_y_control, + draw_z_control, + draw_swap_control, + draw_iswap_control, + stroke_connector_to, + draw_connector, + draw_xswap_control, + draw_zswap_control, }; diff --git a/glue/crumble/main.js b/glue/crumble/main.js index d9d4dd42f..1d16f559d 100644 --- a/glue/crumble/main.js +++ b/glue/crumble/main.js @@ -1,105 +1,527 @@ -/** - * Copyright 2023 Craig Gidney - * Copyright 2025 Riverlane - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * Modifications: - * - Refactored for CrumPy - */ +import {Circuit} from "./circuit/circuit.js" +import {minXY} from "./circuit/layer.js" +import {pitch} from "./draw/config.js" +import {GATE_MAP} from "./gates/gateset.js" +import {EditorState} from "./editor/editor_state.js"; +import {initUrlCircuitSync} from "./editor/sync_url_to_state.js"; +import {draw} from "./draw/main_draw.js"; +import {drawToolbox} from "./keyboard/toolbox.js"; +import {Operation} from "./circuit/operation.js"; +import {make_mpp_gate} from './gates/gateset_mpp.js'; +import {PropagatedPauliFrames} from './circuit/propagated_pauli_frames.js'; -import { Circuit } from "./circuit/circuit.js"; -import { draw } from "./draw/main_draw.js"; -import { EditorState } from "./editor/editor_state.js"; -import { - setIndentCircuitLines, - setCurveConnectors, - setShowAnnotationRegions, -} from "./draw/config.js"; - -const CANVAS_W = 600; -const CANVAS_H = 300; - -function initCanvas(el) { - let scrollWrap = document.createElement("div"); - scrollWrap.setAttribute("style", "overflow-x: auto; overflow-y: hidden;"); - - let canvas = document.createElement("canvas"); - canvas.id = "cvn"; - canvas.setAttribute("style", `margin: 0; padding: 0;`); - canvas.tabIndex = 0; - canvas.width = CANVAS_W; - canvas.height = CANVAS_H; - - scrollWrap.appendChild(canvas); - el.appendChild(scrollWrap); - - return canvas; -} +const OFFSET_X = -pitch + Math.floor(pitch / 4) + 0.5; +const OFFSET_Y = -pitch + Math.floor(pitch / 4) + 0.5; + +const btnInsertLayer = /** @type{!HTMLButtonElement} */ document.getElementById('btnInsertLayer'); +const btnDeleteLayer = /** @type{!HTMLButtonElement} */ document.getElementById('btnDeleteLayer'); +const btnUndo = /** @type{!HTMLButtonElement} */ document.getElementById('btnUndo'); +const btnRedo = /** @type{!HTMLButtonElement} */ document.getElementById('btnRedo'); +const btnClearMarkers = /** @type{!HTMLButtonElement} */ document.getElementById('btnClearMarkers'); +const btnImportExport = /** @type{!HTMLButtonElement} */ document.getElementById('btnShowHideImportExport'); +const btnNextLayer = /** @type{!HTMLButtonElement} */ document.getElementById('btnNextLayer'); +const btnPrevLayer = /** @type{!HTMLButtonElement} */ document.getElementById('btnPrevLayer'); +const btnRotate45 = /** @type{!HTMLButtonElement} */ document.getElementById('btnRotate45'); +const btnRotate45Counter = /** @type{!HTMLButtonElement} */ document.getElementById('btnRotate45Counter'); +const btnExport = /** @type {!HTMLButtonElement} */ document.getElementById('btnExport'); +const btnImport = /** @type {!HTMLButtonElement} */ document.getElementById('btnImport'); +const btnClear = /** @type {!HTMLButtonElement} */ document.getElementById('clear'); +const txtStimCircuit = /** @type {!HTMLTextAreaElement} */ document.getElementById('txtStimCircuit'); +const btnTimelineFocus = /** @type{!HTMLButtonElement} */ document.getElementById('btnTimelineFocus'); +const btnClearTimelineFocus = /** @type{!HTMLButtonElement} */ document.getElementById('btnClearTimelineFocus'); +const btnClearSelectedMarkers = /** @type{!HTMLButtonElement} */ document.getElementById('btnClearSelectedMarkers'); +const btnShowExamples = /** @type {!HTMLButtonElement} */ document.getElementById('btnShowExamples'); +const divExamples = /** @type{!HTMLDivElement} */ document.getElementById('examples-div'); -function render({ model, el }) { - const traitlets = { - getStim: () => model.get("stim"), - getIndentCircuitLines: () => model.get("indentCircuitLines"), - getCurveConnectors: () => model.get("curveConnectors"), - getShowAnnotationRegions: () => model.get("showAnnotationRegions"), - }; - - const canvas = initCanvas(el); - - let editorState = /** @type {!EditorState} */ new EditorState(canvas); - - const exportCurrentState = () => - editorState - .copyOfCurCircuit() - .toStimCircuit() - .replaceAll("\nPOLYGON", "\n#!pragma POLYGON") - .replaceAll("\nERR", "\n#!pragma ERR") - .replaceAll("\nMARK", "\n#!pragma MARK"); - function commitStimCircuit(stim_str) { - let circuit = Circuit.fromStimCircuit(stim_str); +// Prevent typing in the import/export text editor from causing changes in the main circuit editor. +txtStimCircuit.addEventListener('keyup', ev => ev.stopPropagation()); +txtStimCircuit.addEventListener('keydown', ev => ev.stopPropagation()); + +let editorState = /** @type {!EditorState} */ new EditorState(document.getElementById('cvn')); + +btnExport.addEventListener('click', _ev => { + exportCurrentState(); +}); +btnImport.addEventListener('click', _ev => { + let text = txtStimCircuit.value; + let circuit = Circuit.fromStimCircuit(text); editorState.commit(circuit); - } +}); + +btnImportExport.addEventListener('click', _ev => { + let div = /** @type{!HTMLDivElement} */ document.getElementById('divImportExport'); + if (div.style.display === 'none') { + div.style.display = 'block'; + btnImportExport.textContent = "Hide Import/Export"; + exportCurrentState(); + } else { + div.style.display = 'none'; + btnImportExport.textContent = "Show Import/Export"; + txtStimCircuit.value = ''; + } + setTimeout(() => { + window.scrollTo(0, 0); + }, 0); +}); + +btnClear.addEventListener('click', _ev => { + editorState.clearCircuit(); +}); + +btnUndo.addEventListener('click', _ev => { + editorState.undo(); +}); + +btnTimelineFocus.addEventListener('click', _ev => { + editorState.timelineSet = new Map(editorState.focusedSet.entries()); + editorState.force_redraw(); +}); + +btnClearSelectedMarkers.addEventListener('click', _ev => { + editorState.unmarkFocusInferBasis(false); + editorState.force_redraw(); +}); + +btnShowExamples.addEventListener('click', _ev => { + if (divExamples.style.display === 'none') { + divExamples.style.display = 'block'; + btnShowExamples.textContent = "Hide Example Circuits"; + } else { + divExamples.style.display = 'none'; + btnShowExamples.textContent = "Show Example Circuits"; + } +}); + +btnClearTimelineFocus.addEventListener('click', _ev => { + editorState.timelineSet = new Map(); + editorState.force_redraw(); +}); - // Changes to circuit on the Python/notebook side update the JS - model.on("change:stim", () => commitStimCircuit(traitlets.getStim())); - model.on("change:indentCircuitLines", () => { - setIndentCircuitLines(traitlets.getIndentCircuitLines()); +btnRedo.addEventListener('click', _ev => { + editorState.redo(); +}); + +btnClearMarkers.addEventListener('click', _ev => { + editorState.clearMarkers(); +}); + +btnRotate45.addEventListener('click', _ev => { + editorState.rotate45(+1, false); +}); +btnRotate45Counter.addEventListener('click', _ev => { + editorState.rotate45(-1, false); +}); + +btnInsertLayer.addEventListener('click', _ev => { + editorState.insertLayer(false); +}); +btnDeleteLayer.addEventListener('click', _ev => { + editorState.deleteCurLayer(false); +}); + +btnNextLayer.addEventListener('click', _ev => { + editorState.changeCurLayerTo(editorState.curLayer + 1); +}); +btnPrevLayer.addEventListener('click', _ev => { + editorState.changeCurLayerTo(editorState.curLayer - 1); +}); + +window.addEventListener('resize', _ev => { + editorState.canvas.width = editorState.canvas.scrollWidth; + editorState.canvas.height = editorState.canvas.scrollHeight; editorState.force_redraw(); - }); - model.on("change:curveConnectors", () => { - setCurveConnectors(traitlets.getCurveConnectors()); +}); + +function exportCurrentState() { + let validStimCircuit = editorState.copyOfCurCircuit().toStimCircuit(). + replaceAll('\nPOLYGON', '\n#!pragma POLYGON'). + replaceAll('\nERR', '\n#!pragma ERR'). + replaceAll('\nMARK', '\n#!pragma MARK'); + let txt = txtStimCircuit; + txt.value = validStimCircuit + '\n'; + txt.focus(); + txt.select(); +} + +editorState.canvas.addEventListener('mousemove', ev => { + editorState.curMouseX = ev.offsetX + OFFSET_X; + editorState.curMouseY = ev.offsetY + OFFSET_Y; + + // Scrubber. + let w = editorState.canvas.width / 2; + if (isInScrubber && ev.buttons === 1) { + editorState.changeCurLayerTo(Math.floor((ev.offsetX - w) / 8)); + return; + } + editorState.force_redraw(); - }); - model.on("change:showAnnotationRegions", () => { - setShowAnnotationRegions(traitlets.getShowAnnotationRegions()); +}); + +let isInScrubber = false; +editorState.canvas.addEventListener('mousedown', ev => { + editorState.curMouseX = ev.offsetX + OFFSET_X; + editorState.curMouseY = ev.offsetY + OFFSET_Y; + editorState.mouseDownX = ev.offsetX + OFFSET_X; + editorState.mouseDownY = ev.offsetY + OFFSET_Y; + + // Scrubber. + let w = editorState.canvas.width / 2; + isInScrubber = ev.offsetY < 20 && ev.offsetX > w && ev.buttons === 1; + if (isInScrubber) { + editorState.changeCurLayerTo(Math.floor((ev.offsetX - w) / 8)); + return; + } + editorState.force_redraw(); - }); +}); + +editorState.canvas.addEventListener('mouseup', ev => { + let highlightedArea = editorState.currentPositionsBoxesByMouseDrag(ev.altKey); + editorState.mouseDownX = undefined; + editorState.mouseDownY = undefined; + editorState.curMouseX = ev.offsetX + OFFSET_X; + editorState.curMouseY = ev.offsetY + OFFSET_Y; + editorState.changeFocus(highlightedArea, ev.shiftKey, ev.ctrlKey); + if (ev.buttons === 1) { + isInScrubber = false; + } +}); + +/** + * @return {!Map} + */ +function makeChordHandlers() { + let res = /** @type {!Map} */ new Map(); + + res.set('shift+t', preview => editorState.rotate45(-1, preview)); + res.set('t', preview => editorState.rotate45(+1, preview)); + res.set('escape', () => editorState.clearFocus); + res.set('delete', preview => editorState.deleteAtFocus(preview)); + res.set('backspace', preview => editorState.deleteAtFocus(preview)); + res.set('ctrl+delete', preview => editorState.deleteCurLayer(preview)); + res.set('ctrl+insert', preview => editorState.insertLayer(preview)); + res.set('ctrl+backspace', () => editorState.deleteCurLayer); + res.set('ctrl+z', preview => { if (!preview) editorState.undo() }); + res.set('ctrl+y', preview => { if (!preview) editorState.redo() }); + res.set('ctrl+shift+z', preview => { if (!preview) editorState.redo() }); + res.set('ctrl+c', async preview => { await copyToClipboard(); }); + res.set('ctrl+v', pasteFromClipboard); + res.set('ctrl+x', async preview => { + await copyToClipboard(); + if (editorState.focusedSet.size === 0) { + let c = editorState.copyOfCurCircuit(); + c.layers[editorState.curLayer].id_ops.clear(); + c.layers[editorState.curLayer].markers.length = 0; + editorState.commit_or_preview(c, preview); + } else { + editorState.deleteAtFocus(preview); + } + }); + res.set('l', preview => { + if (!preview) { + editorState.timelineSet = new Map(editorState.focusedSet.entries()); + editorState.force_redraw(); + } + }); + res.set(' ', preview => editorState.unmarkFocusInferBasis(preview)); + + for (let [key, val] of [ + ['1', 0], + ['2', 1], + ['3', 2], + ['4', 3], + ['5', 4], + ['6', 5], + ['7', 6], + ['8', 7], + ['9', 8], + ['0', 9], + ['-', 10], + ['=', 11], + ['\\', 12], + ['`', 13], + ]) { + res.set(`${key}`, preview => editorState.markFocusInferBasis(preview, val)); + res.set(`${key}+x`, preview => editorState.writeGateToFocus(preview, GATE_MAP.get('MARKX').withDefaultArgument(val))); + res.set(`${key}+y`, preview => editorState.writeGateToFocus(preview, GATE_MAP.get('MARKY').withDefaultArgument(val))); + res.set(`${key}+z`, preview => editorState.writeGateToFocus(preview, GATE_MAP.get('MARKZ').withDefaultArgument(val))); + res.set(`${key}+d`, preview => editorState.writeMarkerToDetector(preview, val)); + res.set(`${key}+o`, preview => editorState.writeMarkerToObservable(preview, val)); + res.set(`${key}+j`, preview => editorState.moveDetOrObsAtFocusIntoMarker(preview, val)); + res.set(`${key}+k`, preview => editorState.addDissipativeOverlapToMarkers(preview, val)); + } + + let defaultPolygonAlpha = 0.25; + res.set('p', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 0, 0, defaultPolygonAlpha])); + res.set('alt+p', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 1, 0, defaultPolygonAlpha])); + res.set('shift+p', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 0, 1, defaultPolygonAlpha])); + res.set('p+x', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 0, 0, defaultPolygonAlpha])); + res.set('p+y', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 1, 0, defaultPolygonAlpha])); + res.set('p+z', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 0, 1, defaultPolygonAlpha])); + res.set('p+x+y', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 1, 0, defaultPolygonAlpha])); + res.set('p+x+z', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 0, 1, defaultPolygonAlpha])); + res.set('p+y+z', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [0, 1, 1, defaultPolygonAlpha])); + res.set('p+x+y+z', preview => editorState.writeGateToFocus(preview, GATE_MAP.get("POLYGON"), [1, 1, 1, defaultPolygonAlpha])); + res.set('m+p+x', preview => editorState.writeGateToFocus(preview, make_mpp_gate("X".repeat(editorState.focusedSet.size)), [])); + res.set('m+p+y', preview => editorState.writeGateToFocus(preview, make_mpp_gate("Y".repeat(editorState.focusedSet.size)), [])); + res.set('m+p+z', preview => editorState.writeGateToFocus(preview, make_mpp_gate("Z".repeat(editorState.focusedSet.size)), [])); + res.set('f', preview => editorState.flipTwoQubitGateOrderAtFocus(preview)); + res.set('g', preview => editorState.reverseLayerOrderFromFocusToEmptyLayer(preview)); + res.set('shift+>', preview => editorState.applyCoordinateTransform((x, y) => [x + 1, y], preview, false)); + res.set('shift+<', preview => editorState.applyCoordinateTransform((x, y) => [x - 1, y], preview, false)); + res.set('shift+v', preview => editorState.applyCoordinateTransform((x, y) => [x, y + 1], preview, false)); + res.set('shift+^', preview => editorState.applyCoordinateTransform((x, y) => [x, y - 1], preview, false)); + res.set('>', preview => editorState.applyCoordinateTransform((x, y) => [x + 1, y], preview, false)); + res.set('<', preview => editorState.applyCoordinateTransform((x, y) => [x - 1, y], preview, false)); + res.set('v', preview => editorState.applyCoordinateTransform((x, y) => [x, y + 1], preview, false)); + res.set('^', preview => editorState.applyCoordinateTransform((x, y) => [x, y - 1], preview, false)); + res.set('.', preview => editorState.applyCoordinateTransform((x, y) => [x + 0.5, y + 0.5], preview, false)); + + /** + * @param {!Array} chords + * @param {!string} name + * @param {undefined|!string=}inverse_name + */ + function addGateChords(chords, name, inverse_name=undefined) { + for (let chord of chords) { + if (res.has(chord)) { + throw new Error("Chord collision: " + chord); + } + res.set(chord, preview => editorState.writeGateToFocus(preview, GATE_MAP.get(name))); + } + if (inverse_name !== undefined) { + addGateChords(chords.map(e => 'shift+' + e), inverse_name); + } + } + + addGateChords(['h', 'h+y', 'h+x+z'], "H", "H"); + addGateChords(['h+z', 'h+x+y'], "H_XY", "H_XY"); + addGateChords(['h+x', 'h+y+z'], "H_YZ", "H_YZ"); + addGateChords(['s+x', 's+y+z'], "SQRT_X", "SQRT_X_DAG"); + addGateChords(['s+y', 's+x+z'], "SQRT_Y", "SQRT_Y_DAG"); + addGateChords(['s', 's+z', 's+x+y'], "S", "S_DAG"); + addGateChords(['r+x', 'r+y+z'], "RX"); + addGateChords(['r+y', 'r+x+z'], "RY"); + addGateChords(['r', 'r+z', 'r+x+y'], "R"); + addGateChords(['m+x', 'm+y+z'], "MX"); + addGateChords(['m+y', 'm+x+z'], "MY"); + addGateChords(['m', 'm+z', 'm+x+y'], "M"); + addGateChords(['m+r+x', 'm+r+y+z'], "MRX"); + addGateChords(['m+r+y', 'm+r+x+z'], "MRY"); + addGateChords(['m+r', 'm+r+z', 'm+r+x+y'], "MR"); + addGateChords(['c'], "CX", "CX"); + addGateChords(['c+x'], "CX", "CX"); + addGateChords(['c+y'], "CY", "CY"); + addGateChords(['c+z'], "CZ", "CZ"); + addGateChords(['j+x'], "X", "X"); + addGateChords(['j+y'], "Y", "Y"); + addGateChords(['j+z'], "Z", "Z"); + addGateChords(['c+x+y'], "XCY", "XCY"); + addGateChords(['alt+c+x'], "XCX", "XCX"); + addGateChords(['alt+c+y'], "YCY", "YCY"); + + addGateChords(['w'], "SWAP", "SWAP"); + addGateChords(['w+x'], "CXSWAP", undefined); + addGateChords(['c+w+x'], "CXSWAP", undefined); + addGateChords(['i+w'], "ISWAP", "ISWAP_DAG"); + addGateChords(['w+z'], "CZSWAP", undefined); + addGateChords(['c+w+z'], "CZSWAP", undefined); + addGateChords(['c+w'], "CZSWAP", undefined); + + addGateChords(['c+t'], "C_XYZ", "C_ZYX"); + addGateChords(['c+s+x'], "SQRT_XX", "SQRT_XX_DAG"); + addGateChords(['c+s+y'], "SQRT_YY", "SQRT_YY_DAG"); + addGateChords(['c+s+z'], "SQRT_ZZ", "SQRT_ZZ_DAG"); + addGateChords(['c+s'], "SQRT_ZZ", "SQRT_ZZ_DAG"); + + addGateChords(['c+m+x'], "MXX", "MXX"); + addGateChords(['c+m+y'], "MYY", "MYY"); + addGateChords(['c+m+z'], "MZZ", "MZZ"); + addGateChords(['c+m'], "MZZ", "MZZ"); + + return res; +} + +let fallbackEmulatedClipboard = undefined; +async function copyToClipboard() { + let c = editorState.copyOfCurCircuit(); + c.layers = [c.layers[editorState.curLayer]] + if (editorState.focusedSet.size > 0) { + c.layers[0] = c.layers[0].id_filteredByQubit(q => { + let x = c.qubitCoordData[q * 2]; + let y = c.qubitCoordData[q * 2 + 1]; + return editorState.focusedSet.has(`${x},${y}`); + }); + let [x, y] = minXY(editorState.focusedSet.values()); + c = c.shifted(-x, -y); + } + + let content = c.toStimCircuit() + fallbackEmulatedClipboard = content; + try { + await navigator.clipboard.writeText(content); + } catch (ex) { + console.warn("Failed to write to clipboard. Using fallback emulated clipboard.", ex); + } +} + +/** + * @param {!boolean} preview + */ +async function pasteFromClipboard(preview) { + let text; + try { + text = await navigator.clipboard.readText(); + } catch (ex) { + console.warn("Failed to read from clipboard. Using fallback emulated clipboard.", ex); + text = fallbackEmulatedClipboard; + } + if (text === undefined) { + return; + } + + let pastedCircuit = Circuit.fromStimCircuit(text); + if (pastedCircuit.layers.length !== 1) { + throw new Error(text); + } + let newCircuit = editorState.copyOfCurCircuit(); + if (editorState.focusedSet.size > 0) { + let [x, y] = minXY(editorState.focusedSet.values()); + pastedCircuit = pastedCircuit.shifted(x, y); + } + + // Include new coordinates. + let usedCoords = []; + for (let q = 0; q < pastedCircuit.qubitCoordData.length; q += 2) { + usedCoords.push([pastedCircuit.qubitCoordData[q], pastedCircuit.qubitCoordData[q + 1]]); + } + newCircuit = newCircuit.withCoordsIncluded(usedCoords); + let c2q = newCircuit.coordToQubitMap(); + + // Remove existing content at paste location. + for (let key of editorState.focusedSet.keys()) { + let q = c2q.get(key); + if (q !== undefined) { + newCircuit.layers[editorState.curLayer].id_pop_at(q); + } + } + + // Add content to paste location. + for (let op of pastedCircuit.layers[0].iter_gates_and_markers()) { + let newTargets = []; + for (let q of op.id_targets) { + let x = pastedCircuit.qubitCoordData[2*q]; + let y = pastedCircuit.qubitCoordData[2*q+1]; + newTargets.push(c2q.get(`${x},${y}`)); + } + newCircuit.layers[editorState.curLayer].put(new Operation( + op.gate, + op.tag, + op.args, + new Uint32Array(newTargets), + )); + } + + editorState.commit_or_preview(newCircuit, preview); +} + +const CHORD_HANDLERS = makeChordHandlers(); +/** + * @param {!KeyboardEvent} ev + */ +function handleKeyboardEvent(ev) { + editorState.chorder.handleKeyEvent(ev); + if (ev.type === 'keydown') { + if (ev.key.toLowerCase() === 'q') { + let d = ev.shiftKey ? 5 : 1; + editorState.changeCurLayerTo(editorState.curLayer - d); + return; + } + if (ev.key.toLowerCase() === 'e') { + let d = ev.shiftKey ? 5 : 1; + editorState.changeCurLayerTo(editorState.curLayer + d); + return; + } + if (ev.key === 'Home') { + editorState.changeCurLayerTo(0); + ev.preventDefault(); + return; + } + if (ev.key === 'End') { + editorState.changeCurLayerTo(editorState.copyOfCurCircuit().layers.length - 1); + ev.preventDefault(); + return; + } + } + let evs = editorState.chorder.queuedEvents; + if (evs.length === 0) { + return; + } + let chord_ev = evs[evs.length - 1]; + while (evs.length > 0) { + evs.pop(); + } + + let pressed = [...chord_ev.chord]; + if (pressed.length === 0) { + return; + } + pressed.sort(); + let key = ''; + if (chord_ev.altKey) { + key += 'alt+'; + } + if (chord_ev.ctrlKey) { + key += 'ctrl+'; + } + if (chord_ev.metaKey) { + key += 'meta+'; + } + if (chord_ev.shiftKey) { + key += 'shift+'; + } + for (let e of pressed) { + key += `${e}+`; + } + key = key.substring(0, key.length - 1); + + let handler = CHORD_HANDLERS.get(key); + if (handler !== undefined) { + handler(chord_ev.inProgress); + ev.preventDefault(); + } else { + editorState.preview(editorState.copyOfCurCircuit()); + } +} + +document.addEventListener('keydown', handleKeyboardEvent); +document.addEventListener('keyup', handleKeyboardEvent); - // Listeners on editor state that trigger redraws - editorState.rev.changes().subscribe(() => { +editorState.canvas.width = editorState.canvas.scrollWidth; +editorState.canvas.height = editorState.canvas.scrollHeight; +editorState.rev.changes().subscribe(() => { editorState.obs_val_draw_state.set(editorState.toSnapshot(undefined)); - }); - editorState.obs_val_draw_state.observable().subscribe((ds) => - requestAnimationFrame(() => { - draw(editorState.canvas.getContext("2d"), ds); - }), - ); - - // Configure initial settings and stim - setIndentCircuitLines(traitlets.getIndentCircuitLines()); - setCurveConnectors(traitlets.getCurveConnectors()); - setShowAnnotationRegions(traitlets.getShowAnnotationRegions()); - commitStimCircuit(traitlets.getStim()); + drawToolbox(editorState.chorder.toEvent(false)); +}); +initUrlCircuitSync(editorState.rev); +editorState.obs_val_draw_state.observable().subscribe(ds => requestAnimationFrame(() => draw(editorState.canvas.getContext('2d'), ds))); +window.addEventListener('focus', () => { + editorState.chorder.handleFocusChanged(); +}); +window.addEventListener('blur', () => { + editorState.chorder.handleFocusChanged(); +}); + +// Intercept clicks on the example circuit links, and load them without actually reloading the page, to preserve undo history. +for (let anchor of document.getElementById('examples-div').querySelectorAll('a')) { + anchor.onclick = ev => { + // Don't stop the user from e.g. opening the example in a new tab using ctrl+click. + if (ev.shiftKey || ev.ctrlKey || ev.altKey || ev.button !== 0) { + return undefined; + } + let circuitText = anchor.href.split('#circuit=')[1]; + + editorState.rev.commit(circuitText); + return false; + }; } -export default { render }; diff --git a/glue/crumble/package.json b/glue/crumble/package.json index 142ff4801..3dbc1ca59 100644 --- a/glue/crumble/package.json +++ b/glue/crumble/package.json @@ -1,12 +1,3 @@ { - "name": "crumpy", - "type": "module", - "main": "crumble/main.js", - "scripts": { - "build": "esbuild --bundle --format=esm --outfile=./crumpy/bundle.js ./main.js", - "watch": "npm run build -- --watch" - }, - "devDependencies": { - "esbuild": "0.25.8" - } -} \ No newline at end of file + "type": "module" +} diff --git a/glue/crumpy/README.md b/glue/crumpy/README.md new file mode 100644 index 000000000..53e694102 --- /dev/null +++ b/glue/crumpy/README.md @@ -0,0 +1,177 @@ +
+ +# CrumPy + +Visualize quantum circuits with error propagation in a Jupyter widget. + +jupyter notebook cell with quantum circuit diagram output + +--- + +This package builds off of the existing circuit visualizations of +[Crumble](https://algassert.com/crumble), a stabilizer circuit editor and +sub-project of the open-source stabilizer circuit project +[Stim](https://github.com/quantumlib/Stim). + +
+ +--- + +## Installation + +**Requires:** Python 3.11+ + +```console +pip install crumpy +``` + +## Usage + +CrumPy provides a convenient Jupyter widget that makes use of Crumble's ability +to generate quantum circuit timeline visualizations with Pauli propagation from +[Stim circuit specifications](https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md#the-stim-circuit-file-format-stim). + +### Using `CircuitWidget` + +`CircuitWidget` takes a +[Stim circuit specification](https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md#the-stim-circuit-file-format-stim) +in the form of a Python `str`. To convert from the official Stim package's +`stim.Circuit`, use `str(stim_circuit)`. If coming from another circuit type, it +is recommended to first convert to a `stim.Circuit` (e.g., with Stim's +[`stimcirq`](https://github.com/quantumlib/Stim/tree/main/glue/cirq) package), +then to `str`. Note that not all circuits may be convertible to Stim. + +```python +from crumpy import CircuitWidget + +your_circuit = """ +H 0 +CNOT 0 1 +""" + +circuit = CircuitWidget(stim=your_circuit) +circuit +``` + +quantum circuit that creates EPR pair + +### Propagating Paulis + +A useful feature of Crumble (and CrumPy) is the ability to propagate Paulis +through a quantum circuit. Propagation is done automatically based on the +specified circuit and Pauli markers. From the +[Crumble docs](https://github.com/quantumlib/Stim/tree/main/glue/crumble#readme): + +> Propagating Paulis is done by placing markers to indicate where to add terms. +> Each marker has a type (X, Y, or Z) and an index (0-9) indicating which +> indexed Pauli product the marker is modifying. + +Define a Pauli X, Y, or Z marker with the `#!pragma MARK_` instruction. Note +that for compatibility with Stim, the '`#!pragma `' is included as `MARK_` is +not considered a standard Stim instruction: + +```python +z_error_on_qubit_2 = "#!pragma MARKZ(0) 2" +``` + +#### Legend + +red Pauli X, green Pauli Y, blue Pauli Z markers + +#### Example + +```python +circuit_with_error = """ +#!pragma MARKX(0) 0 +TICK +CNOT 0 1 +TICK +H 0 +""" + +CircuitWidget(stim=circuit_with_error) +``` + +quantum circuit with Pauli propagation markers + +Notice how the single specified Pauli X marker propagates both through the +control and across the connector of the CNOT, and gets transformed into a Pauli +Z error when it encounters an H gate. + +## Local Development + +See the [contribution guidelines](.github/CONTRIBUTING.md) for a quick start +guide and Python best practices. + +### Additional requirements + +[npm/Node.js](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) +(for bundling JavaScript); versions 11+/22+ recommended + +### Project Layout + +CrumPy makes use of the widget creation tool +[AnyWidget](https://anywidget.dev/). With AnyWidget, Python classes are able to +take JavaScript and display custom widgets in Jupyter environments. CrumPy +therefore includes both JavaScript and Python subparts: + +```text +glue/ +├── crumble/ # Modified Crumble code +│ ├── crumpy/ +│ │ ├── __init__.py # Python code for CircuitWidget +│ │ └── bundle.js # Bundled JavaScript will appear here +│ ├── main.js # Main circuit visualization/setup +│ └── ... +│ └── package.json # Bundling setup and scripts +├── tests/ # Python tests +└── ... +``` + +`glue/crumble/crumpy` contains the main Python package code. + +`glue/crumble/crumpy/__init__.py` contains the main class of the `crumpy` package, +`CircuitWidget`. + +`glue/crumble/` contains the modified Crumble circuit visualization code that +will be rendered in the `CircuitWidget` widget. + +`glue/crumble/main.js` contains the main circuit visualization and setup logic +in the form of the `render` function +[used by AnyWidget](https://anywidget.dev/en/afm/#anywidget-front-end-module-afm). + +### Bundling + +To create a Jupyter widget, AnyWidget takes in JavaScript as an ECMAScript +Module (ESM). **Any changes made to the JavaScript code that backs the circuit +visualization will require re-bundling** into one optimized ESM file. + +To bundle the JavaScript: + +1. Navigate to `glue/crumpy/src/js` +2. If you haven't yet, run `npm install` (this will install the + [esbuild](https://esbuild.github.io/) bundler) +3. Run `npm run build` (or `npm run watch` for watch mode) + +A new bundle should appear at `src/js/bundle.js`. + +**Note**: If you are working in a Jupyter notebook and re-bundle the JavaScript, +you may need to restart the notebook kernel and rerun any widget-displaying code +for the changes to take effect. + + + + +[pypi-link]: https://pypi.org/project/crumpy/ +[pypi-platforms]: https://img.shields.io/pypi/pyversions/crumpy +[pypi-version]: https://img.shields.io/pypi/v/crumpy + + + +## Attribution + +This package was created as part of [Sam Zappa](https://github.com/zzzappy)'s +internship at [Riverlane](https://github.com/riverlane) during the Summer +of 2025. Thanks to [Hyeok Kim](https://github.com/see-mike-out), +[Leilani Battle](https://github.com/leibatt), [Abe Asfaw](https://github.com/aasfaw) +and [Guen Prawiroatmodjo](https://github.com/guenp) for guidance and useful discussions. diff --git a/glue/crumpy/getting_started.ipynb b/glue/crumpy/getting_started.ipynb new file mode 100644 index 000000000..9e792c63d --- /dev/null +++ b/glue/crumpy/getting_started.ipynb @@ -0,0 +1,551 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0b6e4d5e-f8ab-4946-b950-65f909893b10", + "metadata": {}, + "source": [ + "# Getting Started with CrumPy" + ] + }, + { + "cell_type": "markdown", + "id": "514ab571-4824-4b1f-8c05-5c1b341403e2", + "metadata": {}, + "source": [ + "To start, install the `crumpy` package:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bd1bebe-37cb-400f-88f9-0db8b2934c15", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install crumpy" + ] + }, + { + "cell_type": "markdown", + "id": "3368a263-ff20-4cea-a758-e8310dbe98a5", + "metadata": {}, + "source": [ + "### Required Imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6ef6cbe-0611-489c-915f-f3fc42514dd5", + "metadata": {}, + "outputs": [], + "source": [ + "from crumpy import CircuitWidget\n", + "\n", + "import stim\n", + "import ipywidgets\n", + "import cirq\n", + "import qiskit" + ] + }, + { + "cell_type": "markdown", + "id": "947f9453-24b5-4a7f-9d18-2553730bc42c", + "metadata": {}, + "source": [ + "## Background: Crumble" + ] + }, + { + "cell_type": "markdown", + "id": "935f5eb6-68f8-4db8-82e3-fe1fa44e055e", + "metadata": {}, + "source": [ + "The goal of CrumPy's `CircuitWidget` is to utilize the visualization features of Crumble ([website](https://algassert.com/crumble), [docs](https://github.com/quantumlib/Stim/blob/main/glue/crumble/README.md)), a stabilizer circuit editor and sub-project of the open-source stabilizer circuit project [Stim](https://github.com/quantumlib/Stim), to provide a convenient way to display quantum circuit timelines with Pauli propagation in a Jupyter notebook. In particular, CrumPy takes advantage of Crumble's existing ability to generate circuit timeline visualizations from [Stim circuit specifications](https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md#the-stim-circuit-file-format-stim), which also includes support for Pauli propagation markers.\n", + "\n", + "If you aren't familiar with Crumble, try experimenting with building a circuit from scratch, adding Paulis, or check out some of Crumble's example circuits.\n", + "\n", + "The logic behind Crumble's Stim import textbox (shown on click of the **Show Import/Export** button in Crumble) provides the basis of CrumPy's circuit importing abilities. Try playing around with importing/exporting circuits to get familiar with the notation and instructions supported by Crumble and CrumPy." + ] + }, + { + "cell_type": "markdown", + "id": "099667d8-35a0-4ec1-80b9-76f9b5bf7755", + "metadata": {}, + "source": [ + "### Crumble ↔ CrumPy" + ] + }, + { + "cell_type": "markdown", + "id": "166a21e3-ebd9-4ce2-8381-cf2cc56b2551", + "metadata": {}, + "source": [ + "CrumPy uses the circuit compilation logic from Crumble, ensuring that circuits in Crumble and CrumPy are compatible with one another. Note that this means CrumPy supports Crumble-specific instructions not present in standard Stim circuit specification, like `MARKX` and `POLYGON`. This makes the transfer of a Crumble circuit to CrumPy and vice versa fairly straightforward:\n", + "\n", + "If you've already built a circuit in Crumble that you wish to display using CrumPy:\n", + "\n", + "- (in Crumble) Click **Show Import/Export → Export to Stim Circuit**\n", + "- Copy the Stim output from the textarea\n", + "- (in Python/notebook) Paste the output into the content of a Python string\n", + "- Use your circuit string with `CircuitWidget`!\n", + "\n", + "Going from CrumPy to Crumble:\n", + "\n", + "- (in Python/notebook) Copy your circuit string (tip: try copying the output of `print(your_circuit.stim)`)\n", + "- (in Crumble) Click **Show Import/Export**\n", + "- Paste your circuit into the textarea\n", + "- Click **Import from Stim Circuit** to see your circuit!" + ] + }, + { + "cell_type": "markdown", + "id": "174429da", + "metadata": {}, + "source": [ + "## Using `CircuitWidget`" + ] + }, + { + "cell_type": "markdown", + "id": "2acc809d-734e-477c-9f11-8915ca70419e", + "metadata": {}, + "source": [ + "At the core of `crumpy` is the `CircuitWidget`. To get started with `CircuitWidget`, import it from the `crumpy` package:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9327d695", + "metadata": {}, + "outputs": [], + "source": [ + "from crumpy import CircuitWidget" + ] + }, + { + "cell_type": "markdown", + "id": "d208dec4", + "metadata": {}, + "source": [ + "### Basic Usage" + ] + }, + { + "cell_type": "markdown", + "id": "d0d90c8d", + "metadata": {}, + "source": [ + "`CircuitWidget` takes a [Stim circuit specification](https://github.com/quantumlib/Stim/blob/main/doc/file_format_stim_circuit.md#the-stim-circuit-file-format-stim) in the form of a Python string:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67b624dc", + "metadata": {}, + "outputs": [], + "source": [ + "stim_circuit = \"\"\"\n", + "H 0\n", + "CNOT 0 2\n", + "SWAP 0 1\n", + "M 1\n", + "\"\"\"\n", + "\n", + "widg = CircuitWidget(stim=stim_circuit)\n", + "widg" + ] + }, + { + "cell_type": "markdown", + "id": "39dc6881-aaf6-4f69-be3c-6341c7a334d4", + "metadata": {}, + "source": [ + "It's also possible to update the circuit of an existing `CircuitWidget`. Running the cell below will update the output of the previous cell to reflect the new circuit:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10b0d536-de9d-4612-9303-5cef57834b63", + "metadata": {}, + "outputs": [], + "source": [ + "new_circuit = \"\"\"\n", + "CX 0 2\n", + "TICK\n", + "CY 1 2\n", + "TICK\n", + "CZ 0 1\n", + "\"\"\"\n", + "\n", + "widg.stim = new_circuit" + ] + }, + { + "cell_type": "markdown", + "id": "decdab71-dfae-4eed-b4f4-7124ab66bd32", + "metadata": {}, + "source": [ + "`CircuitWidget` also has configuration options that change the appearance of the circuit. Notice again how the above output is updated:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d45348e-464a-408e-8145-6c0302920570", + "metadata": {}, + "outputs": [], + "source": [ + "widg.curveConnectors = False\n", + "widg.indentCircuitLines = False\n", + "\n", + "# Can also specify within CircuitWidget constructor, e.g.:\n", + "# CircuitWidget(stim=..., curveConnectors=False)" + ] + }, + { + "cell_type": "markdown", + "id": "1752292c-621d-4b47-bddb-208cd1659766", + "metadata": {}, + "source": [ + "### Propagating Paulis" + ] + }, + { + "cell_type": "markdown", + "id": "c8e499f0-82a4-44a8-804d-c6f9c0ebfb09", + "metadata": {}, + "source": [ + "A useful feature of Crumble (and CrumPy) is the ability to propagate Paulis through a quantum circuit. From the [Crumble docs](https://github.com/quantumlib/Stim/tree/main/glue/crumble#readme):\n", + "\n", + "> Propagating Paulis is done by placing markers to indicate where to add terms.\n", + "> Each marker has a type (X, Y, or Z) and an index (0-9) indicating which\n", + "> indexed Pauli product the marker is modifying.\n", + "\n", + "Define a Pauli X, Y, or Z marker with the `#!pragma MARK_` instruction:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "966e029a-8f8d-4ebe-a8aa-4c6bdc6235e0", + "metadata": {}, + "outputs": [], + "source": [ + "x_error_on_qubit_3 = \"#!pragma MARKX(0) 3\"\n", + "z_error_on_qubit_2 = \"MARKZ(0) 2\" # Valid for use with CrumPy, but not with stim.Circuit - see below" + ] + }, + { + "cell_type": "markdown", + "id": "850b70b0-aaec-4686-b545-be839437cee1", + "metadata": {}, + "source": [ + "#### Legend\n", + "\n", + "\n", + "\n", + "#### Example: `MARKX` Propagation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8687d89f-0d39-4d75-928d-3a8d97d95560", + "metadata": {}, + "outputs": [], + "source": [ + "circuit_with_mark = \"\"\"\n", + "#!pragma MARKX(0) 0\n", + "TICK\n", + "CNOT 0 1\n", + "TICK\n", + "H 0\n", + "\"\"\"\n", + "\n", + "CircuitWidget(stim=circuit_with_mark)" + ] + }, + { + "cell_type": "markdown", + "id": "28643b60-9849-4953-9fd5-b3d43492c186", + "metadata": {}, + "source": [ + "Notice how the red Pauli X marker defined on qubit 0 propagates through the CNOT, resulting in X markers on both qubits. Then, when the X marker on qubit 0 encounters the H gate, it is converted into a blue Pauli Z marker. This propagation is done automatically based on the provided circuit and `MARKX`." + ] + }, + { + "cell_type": "markdown", + "id": "69b94d4a-4cbb-4ef6-9309-c224524f8da2", + "metadata": {}, + "source": [ + "#### Note: `stim.Circuit` Compatibility" + ] + }, + { + "cell_type": "markdown", + "id": "249d48d0-2e2f-444f-8657-8eb982f7e3d4", + "metadata": {}, + "source": [ + "Some instructions used in Crumble (and therefore CrumPy) are not considered standard Stim notation. This includes `MARK_`, `POLYGON`, and `ERR` instructions.\n", + "\n", + "For example, consider the circuit specification below which omits the '`#!pragma `' preceding its `MARKZ` instruction. Attempting to create a `stim.Circuit` will cause an error:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2057492f-74f1-4d5d-99fd-70a7a6a3f76c", + "metadata": {}, + "outputs": [], + "source": [ + "no_pragma = \"\"\"\n", + "MARKZ(0) 0\n", + "Y 1\n", + "TICK\n", + "CZ 0 1\n", + "\"\"\"\n", + "\n", + "try:\n", + " stim.Circuit(no_pragma)\n", + "except ValueError as e:\n", + " print(e)" + ] + }, + { + "cell_type": "markdown", + "id": "99c24004-d730-4973-87f2-a0abbfb3343b", + "metadata": {}, + "source": [ + "Including the '`#!pragma `' avoids this issue. The `MARKZ` instruction will not show up in a `stim.Circuit` (the '`#`' in '`#!pragma `' comments out the line):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba1f46f9-41d1-47e0-9985-2914820ef998", + "metadata": {}, + "outputs": [], + "source": [ + "with_pragma = \"\"\"\n", + "#!pragma MARKZ(0) 0\n", + "Y 1\n", + "TICK\n", + "CZ 0 1\n", + "\"\"\"\n", + "\n", + "ok_circuit = stim.Circuit(with_pragma)\n", + "ok_circuit" + ] + }, + { + "cell_type": "markdown", + "id": "3693989e-45de-419a-9450-fd112a5166bb", + "metadata": {}, + "source": [ + "Even though there is no `MARKZ`, we now have a valid `stim.Circuit` which can easily be used with `CircuitWidget`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "caa00306-bb17-4d7d-8f6b-d5cfaa117bad", + "metadata": {}, + "outputs": [], + "source": [ + "CircuitWidget(stim=str(ok_circuit))" + ] + }, + { + "cell_type": "markdown", + "id": "d1793d4c", + "metadata": {}, + "source": [ + "### Composing with Other Widgets" + ] + }, + { + "cell_type": "markdown", + "id": "738b5d9f-db4e-401e-be58-f2810c7b97ac", + "metadata": {}, + "source": [ + "The ability to programmatically update a `CircuitWidget`'s `stim` attribute (also its config options) provides an opportunity for more advanced interaction with the circuit visualization. In particular, we can utilize existing widgets from the `ipywidgets` package to create more interactive experiences.\n", + "\n", + "The `traitlets` package used behind the scenes of both `CircuitWidget` and the widgets of `ipywidgets` allows for the composition of multiple widgets through functions like `link` and `dlink`. See the `ipywidgets` [documentation](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html#linking-widgets) for more advanced uses." + ] + }, + { + "cell_type": "markdown", + "id": "d3f8883a-12d8-4594-aec3-e7b98873ebe2", + "metadata": {}, + "source": [ + "#### Example: `IntSlider` and `dlink`" + ] + }, + { + "cell_type": "markdown", + "id": "92016efe-eb20-4947-bf40-44c59e42416a", + "metadata": {}, + "source": [ + "In this example, an `IntSlider` widget is used in conjunction with a `CircuitWidget` to update the displayed circuit based on the slider's value. The `ipywidget.dlink` function is used to **d**irectionally **link** the slider's `value` to the circuit's `stim` (i.e. updates to the slider's `value` will update the circuit's `stim`). Here we can use the slider to see how a Pauli X marker acts when faced with consecutive H gates:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df1b4aee", + "metadata": {}, + "outputs": [], + "source": [ + "def sliderToCircuit(slider_val):\n", + " base_circuit = \"\"\"\n", + " #!pragma MARKX(0) 0\n", + " TICK\n", + " CX 0 1\n", + " \"\"\"\n", + " return base_circuit + \"H 0\\n\" * slider_val\n", + "\n", + "circuit = CircuitWidget()\n", + "slider = ipywidgets.IntSlider(description=\"# of H gates\", value=1, min=0, max=10)\n", + "\n", + "ipywidgets.dlink((slider, \"value\"), (circuit, \"stim\"), sliderToCircuit)\n", + "\n", + "display(slider, circuit)" + ] + }, + { + "cell_type": "markdown", + "id": "ed2c23e0", + "metadata": {}, + "source": [ + "### Converting Directly from a Circuit (`stim`, `cirq`, `qiskit`)" + ] + }, + { + "cell_type": "markdown", + "id": "ed4ffb4e", + "metadata": {}, + "source": [ + "CrumPy provides helper methods for creating a `CircuitWidget` directly from 3 popular quantum circuit packages: `stim`, `cirq`, and `qiskit`.\n", + "\n", + "Even if `stim` isn't your quantum circuit package of choice, it may still be possible to utilize CrumPy. Circuits from packages like `cirq` or `qiskit` may be convertible to a `stim` circuit. For circuit languages that don't have an existing transpiler that converts directly to Stim, it may also be possible to convert to [OpenQASM](https://en.wikipedia.org/wiki/OpenQASM), which can be [converted to Cirq](https://quantumai.google/cirq/build/interop#importing_from_openqasm) and finally [converted to Stim](https://github.com/quantumlib/Stim/tree/main/glue/cirq#readme).\n", + "\n", + "Note that it may not always be possible to convert a circuit between formats (notably, `stim` has a [limited gate set](https://github.com/quantumlib/Stim/tree/main?tab=readme-ov-file#what-is-stim:~:text=There%20is%20no%20support%20for%20non%2DClifford%20operations%2C%20such%20as%20T%20gates%20and%20Toffoli%20gates.%20Only%20stabilizer%20operations%20are%20supported.)).\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "b4dd3937", + "metadata": {}, + "source": [ + "#### Examples" + ] + }, + { + "cell_type": "markdown", + "id": "30d3991d", + "metadata": {}, + "source": [ + "`CircuitWidget.from_stim`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25852a22", + "metadata": {}, + "outputs": [], + "source": [ + "stim_circuit = stim.Circuit(\"\"\"\n", + "H 0\n", + "CX 0 1\n", + "M 0 1\n", + "\"\"\")\n", + "\n", + "CircuitWidget.from_stim(stim_circuit)" + ] + }, + { + "cell_type": "markdown", + "id": "d2ee059f", + "metadata": {}, + "source": [ + "`CircuitWidget.from_cirq`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0649f636", + "metadata": {}, + "outputs": [], + "source": [ + "q0 = cirq.LineQubit(0)\n", + "q1 = cirq.LineQubit(1)\n", + "cirq_circuit = cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1), cirq.measure(q0, q1))\n", + "\n", + "CircuitWidget.from_cirq(cirq_circuit)" + ] + }, + { + "cell_type": "markdown", + "id": "ff13af42", + "metadata": {}, + "source": [ + "`CircuitWidget.from_qiskit`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8bf6aa0b", + "metadata": {}, + "outputs": [], + "source": [ + "qiskit_circuit = qiskit.QuantumCircuit(2, 2)\n", + "qiskit_circuit.h(0)\n", + "qiskit_circuit.cx(0, 1)\n", + "qiskit_circuit.measure([0, 1], [0, 1])\n", + "\n", + "CircuitWidget.from_qiskit(qiskit_circuit)" + ] + }, + { + "cell_type": "markdown", + "id": "b3b2651f", + "metadata": {}, + "source": [ + "While it's not possible to directly add Pauli markers or other Crumble-specific instructions when creating `CircuitWidget`s with these methods, it is possible to read/write the resulting `CircuitWidget`'s `.stim` attribute to get the final converted Stim circuit (in string form), which can then be modified." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/glue/crumble/pyproject.toml b/glue/crumpy/pyproject.toml similarity index 93% rename from glue/crumble/pyproject.toml rename to glue/crumpy/pyproject.toml index 4e37f7626..ca3345229 100644 --- a/glue/crumble/pyproject.toml +++ b/glue/crumpy/pyproject.toml @@ -7,6 +7,7 @@ build-backend = "poetry_dynamic_versioning.backend" name = "crumpy" authors = [ { name = "Sam Zappa (Riverlane)", email = "deltakit@riverlane.com" }, + { name = "Guen Prawiroatmodjo (Riverlane)", email = "deltakit@riverlane.com" }, ] description = "A python library for visualizing Crumble circuits in Jupyter" readme = "README.md" @@ -66,9 +67,9 @@ docs = [ "stimcirq>=1.15.0", ] [tool.poetry] -packages = [{ include = "crumpy" }] +packages = [{ include = "crumpy", from = "src" }] include = [ - { path = "crumpy/bundle.js", format = ["sdist", "wheel"] }, + { path = "src/js/bundle.js", format = ["sdist", "wheel"] }, ] version = "0.0.0" @@ -77,7 +78,7 @@ poetry-dynamic-versioning = { version = ">=1.0.0", extras = ["plugin"] } [tool.poetry-dynamic-versioning] enable = true -substitution.files = ["crumpy/__init__.py"] +substitution.files = ["src/crumpy/__init__.py"] [tool.poetry.group.test.dependencies] pytest = ">= 6" @@ -105,7 +106,7 @@ report.exclude_also = [ ] [tool.mypy] -files = ["crumpy"] +files = ["src", "tests"] python_version = "3.11" warn_unused_configs = true strict = true diff --git a/glue/crumpy/simple_example.ipynb b/glue/crumpy/simple_example.ipynb new file mode 100644 index 000000000..f766628a7 --- /dev/null +++ b/glue/crumpy/simple_example.ipynb @@ -0,0 +1,119 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "6eb5b8eb-0dbe-4517-8935-30ced98c7eb5", + "metadata": { + "execution": { + "iopub.execute_input": "2025-09-25T00:30:26.471400Z", + "iopub.status.busy": "2025-09-25T00:30:26.471260Z", + "iopub.status.idle": "2025-09-25T00:30:27.728413Z", + "shell.execute_reply": "2025-09-25T00:30:27.728194Z", + "shell.execute_reply.started": "2025-09-25T00:30:26.471386Z" + } + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8791a3107353463b8790a9001b2a083b", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "CircuitWidget(stim='\\nH 0\\nCNOT 0 1\\n')" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from crumpy import CircuitWidget\n", + "\n", + "your_circuit = \"\"\"\n", + "H 0\n", + "CNOT 0 1\n", + "\"\"\"\n", + "\n", + "circuit = CircuitWidget(stim=your_circuit)\n", + "circuit" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f3ceb663-3dbf-44dd-aecb-a18bd0198ba7", + "metadata": { + "execution": { + "iopub.execute_input": "2025-09-25T00:30:27.728812Z", + "iopub.status.busy": "2025-09-25T00:30:27.728677Z", + "iopub.status.idle": "2025-09-25T00:30:27.732068Z", + "shell.execute_reply": "2025-09-25T00:30:27.731827Z", + "shell.execute_reply.started": "2025-09-25T00:30:27.728805Z" + } + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7677ee648e74420788ace92e7962e38a", + "version_major": 2, + "version_minor": 1 + }, + "text/plain": [ + "CircuitWidget(indentCircuitLines=False, stim='\\n#!pragma MARKX(0) 0\\nTICK\\nCNOT 0 1\\nTICK\\nH 0\\n')" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "circuit_with_error = \"\"\"\n", + "#!pragma MARKX(0) 0\n", + "TICK\n", + "CNOT 0 1\n", + "TICK\n", + "H 0\n", + "\"\"\"\n", + "\n", + "w = CircuitWidget(stim=circuit_with_error)\n", + "w.indentCircuitLines = False\n", + "w" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f106c05-0a51-410f-a3cc-d1c7af3277b7", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/glue/crumble/crumpy/__init__.py b/glue/crumpy/src/crumpy/__init__.py similarity index 98% rename from glue/crumble/crumpy/__init__.py rename to glue/crumpy/src/crumpy/__init__.py index 97cf93274..fd84d4572 100644 --- a/glue/crumble/crumpy/__init__.py +++ b/glue/crumpy/src/crumpy/__init__.py @@ -25,7 +25,7 @@ __all__ = ["__version__"] -bundler_output_dir = pathlib.Path(__file__).parent +bundler_output_dir = pathlib.Path(__file__).parent.parent / "js" class CircuitWidget(anywidget.AnyWidget): diff --git a/glue/crumble/crumpy/py.typed b/glue/crumpy/src/crumpy/py.typed similarity index 100% rename from glue/crumble/crumpy/py.typed rename to glue/crumpy/src/crumpy/py.typed diff --git a/glue/crumpy/src/js/draw/config.js b/glue/crumpy/src/js/draw/config.js new file mode 100644 index 000000000..7676b6baa --- /dev/null +++ b/glue/crumpy/src/js/draw/config.js @@ -0,0 +1,63 @@ +/** + * Copyright 2023 Craig Gidney + * Copyright 2025 Riverlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications: + * - Refactored for CrumPy + */ + +const pitch = 50; +const rad = 10; +const OFFSET_X = -pitch + Math.floor(pitch / 4) + 0.5; +const OFFSET_Y = -pitch + Math.floor(pitch / 4) + 0.5; +let indentCircuitLines = true; +let drawLinksToTimelineViewer = false; +let curveConnectors = true; +let showAnnotationRegions = true; + +const setIndentCircuitLines = (newBool) => { + if (typeof newBool !== "boolean") { + throw new TypeError(`Expected a boolean, but got ${typeof newBool}`); + } + indentCircuitLines = newBool; +}; + +const setCurveConnectors = (newBool) => { + if (typeof newBool !== "boolean") { + throw new TypeError(`Expected a boolean, but got ${typeof newBool}`); + } + curveConnectors = newBool; +}; + +const setShowAnnotationRegions = (newBool) => { + if (typeof newBool !== "boolean") { + throw new TypeError(`Expected a boolean, but got ${typeof newBool}`); + } + showAnnotationRegions = newBool; +}; + +export { + pitch, + rad, + OFFSET_X, + OFFSET_Y, + indentCircuitLines, + curveConnectors, + showAnnotationRegions, + setIndentCircuitLines, + drawLinksToTimelineViewer, + setCurveConnectors, + setShowAnnotationRegions, +}; diff --git a/glue/crumpy/src/js/draw/main_draw.js b/glue/crumpy/src/js/draw/main_draw.js new file mode 100644 index 000000000..cd2d48e85 --- /dev/null +++ b/glue/crumpy/src/js/draw/main_draw.js @@ -0,0 +1,273 @@ +/** + * Copyright 2023 Craig Gidney + * Copyright 2025 Riverlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications: + * - Refactored for CrumPy + */ + +import {pitch, rad, showAnnotationRegions, OFFSET_X, OFFSET_Y} from "./config.js" +import {marker_placement} from "../gates/gateset_markers.js"; +import {drawTimeline} from "./timeline_viewer.js"; +import {PropagatedPauliFrames} from "../circuit/propagated_pauli_frames.js"; +import {stroke_connector_to} from "../gates/gate_draw_util.js" +import {beginPathPolygon} from './draw_util.js'; + +/** + * @param {!number|undefined} x + * @param {!number|undefined} y + * @return {![undefined, undefined]|![!number, !number]} + */ +function xyToPos(x, y) { + if (x === undefined || y === undefined) { + return [undefined, undefined]; + } + let focusX = x / pitch; + let focusY = y / pitch; + let roundedX = Math.floor(focusX * 2 + 0.5) / 2; + let roundedY = Math.floor(focusY * 2 + 0.5) / 2; + let centerX = roundedX*pitch; + let centerY = roundedY*pitch; + if (Math.abs(centerX - x) <= rad && Math.abs(centerY - y) <= rad && roundedX % 1 === roundedY % 1) { + return [roundedX, roundedY]; + } + return [undefined, undefined]; +} + +/** + * @param {!CanvasRenderingContext2D} ctx + * @param {!StateSnapshot} snap + * @param {!function(q: !int): ![!number, !number]} qubitCoordsFunc + * @param {!PropagatedPauliFrames} propagatedMarkers + * @param {!int} mi + */ +function drawCrossMarkers(ctx, snap, qubitCoordsFunc, propagatedMarkers, mi) { + let crossings = propagatedMarkers.atLayer(snap.curLayer).crossings; + if (crossings !== undefined) { + for (let {q1, q2, color} of crossings) { + let [x1, y1] = qubitCoordsFunc(q1); + let [x2, y2] = qubitCoordsFunc(q2); + if (color === 'X') { + ctx.strokeStyle = 'red'; + } else if (color === 'Y') { + ctx.strokeStyle = 'green'; + } else if (color === 'Z') { + ctx.strokeStyle = 'blue'; + } else { + ctx.strokeStyle = 'purple' + } + ctx.lineWidth = 8; + stroke_connector_to(ctx, x1, y1, x2, y2); + ctx.lineWidth = 1; + } + } +} + +/** + * @param {!CanvasRenderingContext2D} ctx + * @param {!StateSnapshot} snap + * @param {!function(q: !int): ![!number, !number]} qubitCoordsFunc + * @param {!Map} propagatedMarkerLayers + */ +function drawMarkers(ctx, snap, qubitCoordsFunc, propagatedMarkerLayers) { + let obsCount = new Map(); + let detCount = new Map(); + for (let [mi, p] of propagatedMarkerLayers.entries()) { + drawSingleMarker(ctx, snap, qubitCoordsFunc, p, mi, obsCount, detCount); + } +} + +/** + * @param {!CanvasRenderingContext2D} ctx + * @param {!StateSnapshot} snap + * @param {!function(q: !int): ![!number, !number]} qubitCoordsFunc + * @param {!PropagatedPauliFrames} propagatedMarkers + * @param {!int} mi + * @param {!Map} hitCount + */ +function drawSingleMarker(ctx, snap, qubitCoordsFunc, propagatedMarkers, mi, hitCount) { + let basesQubitMap = propagatedMarkers.atLayer(snap.curLayer + 0.5).bases; + + // Convert qubit indices to draw coordinates. + let basisCoords = []; + for (let [q, b] of basesQubitMap.entries()) { + basisCoords.push([b, qubitCoordsFunc(q)]); + } + + // Draw a polygon for the marker set. + if (mi >= 0 && basisCoords.length > 0) { + if (basisCoords.every(e => e[0] === 'X')) { + ctx.fillStyle = 'red'; + } else if (basisCoords.every(e => e[0] === 'Y')) { + ctx.fillStyle = 'green'; + } else if (basisCoords.every(e => e[0] === 'Z')) { + ctx.fillStyle = 'blue'; + } else { + ctx.fillStyle = 'black'; + } + ctx.strokeStyle = ctx.fillStyle; + let coords = basisCoords.map(e => e[1]); + let cx = 0; + let cy = 0; + for (let [x, y] of coords) { + cx += x; + cy += y; + } + cx /= coords.length; + cy /= coords.length; + coords.sort((a, b) => { + let [ax, ay] = a; + let [bx, by] = b; + let av = Math.atan2(ay - cy, ax - cx); + let bv = Math.atan2(by - cy, bx - cx); + if (ax === cx && ay === cy) { + av = -100; + } + if (bx === cx && by === cy) { + bv = -100; + } + return av - bv; + }); + beginPathPolygon(ctx, coords); + ctx.globalAlpha *= 0.25; + ctx.fill(); + ctx.globalAlpha *= 4; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.lineWidth = 1; + } + + // Draw individual qubit markers. + for (let [b, [x, y]] of basisCoords) { + let {dx, dy, wx, wy} = marker_placement(mi, `${x}:${y}`, hitCount); + if (b === 'X') { + ctx.fillStyle = 'red' + } else if (b === 'Y') { + ctx.fillStyle = 'green' + } else if (b === 'Z') { + ctx.fillStyle = 'blue' + } else { + throw new Error('Not a pauli: ' + b); + } + ctx.fillRect(x - dx, y - dy, wx, wy); + } + + // Show error highlights. + let errorsQubitSet = propagatedMarkers.atLayer(snap.curLayer).errors; + for (let q of errorsQubitSet) { + let [x, y] = qubitCoordsFunc(q); + let {dx, dy, wx, wy} = marker_placement(mi, `${x}:${y}`, hitCount); + if (mi < 0) { + ctx.lineWidth = 2; + } else { + ctx.lineWidth = 8; + } + ctx.strokeStyle = 'magenta' + ctx.strokeRect(x - dx, y - dy, wx, wy); + ctx.lineWidth = 1; + ctx.fillStyle = 'black' + ctx.fillRect(x - dx, y - dy, wx, wy); + } +} + +let _defensive_draw_enabled = true; + +/** + * @param {!boolean} val + */ +function setDefensiveDrawEnabled(val) { + _defensive_draw_enabled = val; +} + +/** + * @param {!CanvasRenderingContext2D} ctx + * @param {!function} body + */ +function defensiveDraw(ctx, body) { + ctx.save(); + try { + if (_defensive_draw_enabled) { + body(); + } else { + try { + body(); + } catch (ex) { + console.error(ex); + } + } + } finally { + ctx.restore(); + } +} + +/** + * @param {!CanvasRenderingContext2D} ctx + * @param {!StateSnapshot} snap + */ +function draw(ctx, snap) { + let circuit = snap.circuit; + + let numPropagatedLayers = 0; + for (let layer of circuit.layers) { + for (let op of layer.markers) { + let gate = op.gate; + if (gate.name === "MARKX" || gate.name === "MARKY" || gate.name === "MARKZ") { + numPropagatedLayers = Math.max(numPropagatedLayers, op.args[0] + 1); + } + } + } + + let c2dCoordTransform = (x, y) => [x*pitch - OFFSET_X, y*pitch - OFFSET_Y]; + let qubitDrawCoords = q => { + let x = circuit.qubitCoordData[2 * q]; + let y = circuit.qubitCoordData[2 * q + 1]; + return c2dCoordTransform(x, y); + }; + let propagatedMarkerLayers = /** @type {!Map} */ new Map(); + for (let mi = 0; mi < numPropagatedLayers; mi++) { + propagatedMarkerLayers.set(mi, PropagatedPauliFrames.fromCircuit(circuit, mi)); + } + let {dets: dets, obs: obs} = circuit.collectDetectorsAndObservables(false); + let batch_input = []; + for (let mi = 0; mi < dets.length; mi++) { + batch_input.push(dets[mi].mids); + } + for (let mi of obs.keys()) { + batch_input.push(obs.get(mi)); + } + let batch_output = PropagatedPauliFrames.batchFromMeasurements(circuit, batch_input); + let batch_index = 0; + + if (showAnnotationRegions) { + for (let mi = 0; mi < dets.length; mi++) { + propagatedMarkerLayers.set(~mi, batch_output[batch_index++]); + } + for (let mi of obs.keys()) { + propagatedMarkerLayers.set(~mi ^ (1 << 30), batch_output[batch_index++]); + } + } + + drawTimeline( + ctx, + snap, + propagatedMarkerLayers, + qubitDrawCoords, + circuit.layers.length, + ); + + ctx.save(); +} + +export {xyToPos, draw, setDefensiveDrawEnabled, OFFSET_X, OFFSET_Y} diff --git a/glue/crumpy/src/js/draw/timeline_viewer.js b/glue/crumpy/src/js/draw/timeline_viewer.js new file mode 100644 index 000000000..712ccd91d --- /dev/null +++ b/glue/crumpy/src/js/draw/timeline_viewer.js @@ -0,0 +1,282 @@ +/** + * Copyright 2023 Craig Gidney + * Copyright 2025 Riverlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications: + * - Refactored for CrumPy + */ + +import { indentCircuitLines, drawLinksToTimelineViewer, OFFSET_Y, rad } from "./config.js"; +import { stroke_connector_to } from "../gates/gate_draw_util.js"; +import { marker_placement } from "../gates/gateset_markers.js"; + +let TIMELINE_PITCH = 32; +let PADDING_VERTICAL = rad; +let MAX_CANVAS_WIDTH = 4096; + +/** + * @param {!CanvasRenderingContext2D} ctx + * @param {!StateSnapshot} ds + * @param {!function(!int, !number): ![!number, !number]} qubitTimeCoordFunc + * @param {!PropagatedPauliFrames} propagatedMarkers + * @param {!int} mi + * @param {!int} min_t + * @param {!int} max_t + * @param {!number} x_pitch + * @param {!Map} hitCounts + */ +function drawTimelineMarkers(ctx, ds, qubitTimeCoordFunc, propagatedMarkers, mi, min_t, max_t, x_pitch, hitCounts) { + for (let t = min_t - 1; t <= max_t; t++) { + if (!hitCounts.has(t)) { + hitCounts.set(t, new Map()); + } + let hitCount = hitCounts.get(t); + let p1 = propagatedMarkers.atLayer(t + 0.5); + let p0 = propagatedMarkers.atLayer(t); + for (let [q, b] of p1.bases.entries()) { + let {dx, dy, wx, wy} = marker_placement(mi, q, hitCount); + if (mi >= 0 && mi < 4) { + dx = 0; + wx = x_pitch; + wy = 5; + if (mi === 0) { + dy = 10; + } else if (mi === 1) { + dy = 5; + } else if (mi === 2) { + dy = 0; + } else if (mi === 3) { + dy = -5; + } + } else { + dx -= x_pitch / 2; + } + let [x, y] = qubitTimeCoordFunc(q, t); + if (x === undefined || y === undefined) { + continue; + } + if (b === 'X') { + ctx.fillStyle = 'red' + } else if (b === 'Y') { + ctx.fillStyle = 'green' + } else if (b === 'Z') { + ctx.fillStyle = 'blue' + } else { + throw new Error('Not a pauli: ' + b); + } + ctx.fillRect(x - dx, y - dy, wx, wy); + } + for (let q of p0.errors) { + let {dx, dy, wx, wy} = marker_placement(mi, q, hitCount); + dx -= x_pitch / 2; + + let [x, y] = qubitTimeCoordFunc(q, t - 0.5); + if (x === undefined || y === undefined) { + continue; + } + ctx.strokeStyle = 'magenta'; + ctx.lineWidth = 8; + ctx.strokeRect(x - dx, y - dy, wx, wy); + ctx.lineWidth = 1; + ctx.fillStyle = 'black'; + ctx.fillRect(x - dx, y - dy, wx, wy); + } + for (let {q1, q2, color} of p0.crossings) { + let [x1, y1] = qubitTimeCoordFunc(q1, t); + let [x2, y2] = qubitTimeCoordFunc(q2, t); + if (color === 'X') { + ctx.strokeStyle = 'red'; + } else if (color === 'Y') { + ctx.strokeStyle = 'green'; + } else if (color === 'Z') { + ctx.strokeStyle = 'blue'; + } else { + ctx.strokeStyle = 'purple' + } + ctx.lineWidth = 8; + stroke_connector_to(ctx, x1, y1, x2, y2); + ctx.lineWidth = 1; + } + } +} + +/** + * @param {!CanvasRenderingContext2D} ctx + * @param {!StateSnapshot} snap + * @param {!Map} propagatedMarkerLayers + * @param {!function(!int): ![!number, !number]} timesliceQubitCoordsFunc + * @param {!int} numLayers + */ +function drawTimeline(ctx, snap, propagatedMarkerLayers, timesliceQubitCoordsFunc, numLayers) { + let w = MAX_CANVAS_WIDTH; + + let qubits = snap.timelineQubits(); + qubits.sort((a, b) => { + let [x1, y1] = timesliceQubitCoordsFunc(a); + let [x2, y2] = timesliceQubitCoordsFunc(b); + if (y1 !== y2) { + return y1 - y2; + } + return x1 - x2; + }); + + let base_y2xy = new Map(); + let prev_y = undefined; + let cur_x = 0; + let cur_y = PADDING_VERTICAL + rad; + let max_run = 0; + let cur_run = 0; + for (let q of qubits) { + let [x, y] = timesliceQubitCoordsFunc(q); + if (prev_y !== y) { + cur_x = w / 2; + max_run = Math.max(max_run, cur_run); + cur_run = 0; + if (prev_y !== undefined) { + // first qubit's y is at initial cur_y value + cur_y += TIMELINE_PITCH * 0.25; + } + prev_y = y; + } else { + if (indentCircuitLines) { + cur_x += rad * 0.25; // slight x offset between qubits in a row + } + cur_run++; + } + base_y2xy.set(`${x},${y}`, [Math.round(cur_x) + 0.5, Math.round(cur_y) + 0.5]); + cur_y += TIMELINE_PITCH; + } + + let x_pitch = TIMELINE_PITCH + Math.ceil(rad*max_run*0.25); + let num_cols_half = Math.floor(w / 2 / x_pitch); + let min_t_free = snap.curLayer - num_cols_half + 1; + let min_t_clamp = Math.max(0, Math.min(min_t_free, numLayers - num_cols_half*2 + 1)); + let max_t = Math.min(min_t_clamp + num_cols_half*2 + 2, numLayers); + let t2t = t => { + let dt = t - snap.curLayer; + dt -= min_t_clamp - min_t_free; + return dt*x_pitch; + } + let coordTransform_t = ([x, y, t]) => { + let key = `${x},${y}`; + if (!base_y2xy.has(key)) { + return [undefined, undefined]; + } + let [xb, yb] = base_y2xy.get(key); + return [xb + t2t(t), yb]; + }; + let qubitTimeCoords = (q, t) => { + let [x, y] = timesliceQubitCoordsFunc(q); + return coordTransform_t([x, y, t]); + }; + + ctx.save(); + + // Using coords function, see if any qubit labels would get cut off + let maxLabelWidth = 0; + let topLeftX = qubitTimeCoords(qubits[0], min_t_clamp - 1)[0]; + for (let q of qubits) { + let [x, y] = qubitTimeCoords(q, min_t_clamp - 1); + let qx = snap.circuit.qubitCoordData[q * 2]; + let qy = snap.circuit.qubitCoordData[q * 2 + 1]; + let label = `${qx},${qy}:`; + let labelWidth = ctx.measureText(label).width; + let labelWidthFromTop = labelWidth - (x - topLeftX); + maxLabelWidth = Math.max(maxLabelWidth, labelWidthFromTop); + } + let textOverflowLen = Math.max(0, maxLabelWidth - topLeftX); + + // Adjust coords function to ensure all qubit labels fit on canvas (+ small pad) + let labelShiftedQTC = (q, t) => { + let [x, y] = qubitTimeCoords(q, t); + return [x + Math.ceil(textOverflowLen) + 3, y]; + }; + + // Resize canvas to fit circuit + let timelineHeight = + labelShiftedQTC(qubits.at(-1), max_t + 1)[1] + rad + PADDING_VERTICAL; // y of lowest qubit line + padding + let timelineWidth = Math.max( + ...qubits.map((q) => labelShiftedQTC(q, max_t + 1)[0]), + ); // max x of any qubit line's endpoint + ctx.canvas.width = Math.floor(timelineWidth); + ctx.canvas.height = Math.floor(timelineHeight); + + try { + ctx.fillStyle = 'white'; + ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); + + // Draw colored indicators showing Pauli propagation. + let hitCounts = new Map(); + for (let [mi, p] of propagatedMarkerLayers.entries()) { + drawTimelineMarkers(ctx, snap, labelShiftedQTC, p, mi, min_t_clamp, max_t, x_pitch, hitCounts); + } + + // Draw wire lines. + ctx.strokeStyle = 'black'; + ctx.fillStyle = 'black'; + for (let q of qubits) { + let [x0, y0] = labelShiftedQTC(q, min_t_clamp - 1); + let [x1, y1] = labelShiftedQTC(q, max_t + 1); + ctx.beginPath(); + ctx.moveTo(x0, y0); + ctx.lineTo(x1, y1); + ctx.stroke(); + } + + // Draw wire labels. + ctx.textAlign = 'right'; + ctx.textBaseline = 'middle'; + for (let q of qubits) { + let [x, y] = labelShiftedQTC(q, min_t_clamp - 1); + let qx = snap.circuit.qubitCoordData[q * 2]; + let qy = snap.circuit.qubitCoordData[q * 2 + 1]; + let label = `${qx},${qy}:`; + ctx.fillText(label, x, y); + } + + // Draw layers of gates. + for (let time = min_t_clamp; time <= max_t; time++) { + let qubitsCoordsFuncForLayer = (q) => labelShiftedQTC(q, time); + let layer = snap.circuit.layers[time]; + if (layer === undefined) { + continue; + } + for (let op of layer.iter_gates_and_markers()) { + op.id_draw(qubitsCoordsFuncForLayer, ctx); + } + } + if (drawLinksToTimelineViewer) { + // Draw links to timeslice viewer. + ctx.globalAlpha = 0.5; + for (let q of qubits) { + let [x0, y0] = qubitTimeCoords(q, min_t_clamp - 1); + let [x1, y1] = timesliceQubitCoordsFunc(q); + if (snap.curMouseX > ctx.canvas.width / 2 && snap.curMouseY >= y0 + OFFSET_Y - TIMELINE_PITCH * 0.55 && snap.curMouseY <= y0 + TIMELINE_PITCH * 0.55 + OFFSET_Y) { + ctx.beginPath(); + ctx.moveTo(x0, y0); + ctx.lineTo(x1, y1); + ctx.stroke(); + ctx.fillStyle = 'black'; + ctx.fillRect(x1 - 20, y1 - 20, 40, 40); + ctx.fillRect(ctx.canvas.width / 2, y0 - TIMELINE_PITCH / 3, ctx.canvas.width / 2, TIMELINE_PITCH * 2 / 3); + } + } + } + } finally { + ctx.restore(); + } +} + +export { drawTimeline }; diff --git a/glue/crumpy/src/js/main.js b/glue/crumpy/src/js/main.js new file mode 100644 index 000000000..d9d4dd42f --- /dev/null +++ b/glue/crumpy/src/js/main.js @@ -0,0 +1,105 @@ +/** + * Copyright 2023 Craig Gidney + * Copyright 2025 Riverlane + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Modifications: + * - Refactored for CrumPy + */ + +import { Circuit } from "./circuit/circuit.js"; +import { draw } from "./draw/main_draw.js"; +import { EditorState } from "./editor/editor_state.js"; +import { + setIndentCircuitLines, + setCurveConnectors, + setShowAnnotationRegions, +} from "./draw/config.js"; + +const CANVAS_W = 600; +const CANVAS_H = 300; + +function initCanvas(el) { + let scrollWrap = document.createElement("div"); + scrollWrap.setAttribute("style", "overflow-x: auto; overflow-y: hidden;"); + + let canvas = document.createElement("canvas"); + canvas.id = "cvn"; + canvas.setAttribute("style", `margin: 0; padding: 0;`); + canvas.tabIndex = 0; + canvas.width = CANVAS_W; + canvas.height = CANVAS_H; + + scrollWrap.appendChild(canvas); + el.appendChild(scrollWrap); + + return canvas; +} + +function render({ model, el }) { + const traitlets = { + getStim: () => model.get("stim"), + getIndentCircuitLines: () => model.get("indentCircuitLines"), + getCurveConnectors: () => model.get("curveConnectors"), + getShowAnnotationRegions: () => model.get("showAnnotationRegions"), + }; + + const canvas = initCanvas(el); + + let editorState = /** @type {!EditorState} */ new EditorState(canvas); + + const exportCurrentState = () => + editorState + .copyOfCurCircuit() + .toStimCircuit() + .replaceAll("\nPOLYGON", "\n#!pragma POLYGON") + .replaceAll("\nERR", "\n#!pragma ERR") + .replaceAll("\nMARK", "\n#!pragma MARK"); + function commitStimCircuit(stim_str) { + let circuit = Circuit.fromStimCircuit(stim_str); + editorState.commit(circuit); + } + + // Changes to circuit on the Python/notebook side update the JS + model.on("change:stim", () => commitStimCircuit(traitlets.getStim())); + model.on("change:indentCircuitLines", () => { + setIndentCircuitLines(traitlets.getIndentCircuitLines()); + editorState.force_redraw(); + }); + model.on("change:curveConnectors", () => { + setCurveConnectors(traitlets.getCurveConnectors()); + editorState.force_redraw(); + }); + model.on("change:showAnnotationRegions", () => { + setShowAnnotationRegions(traitlets.getShowAnnotationRegions()); + editorState.force_redraw(); + }); + + // Listeners on editor state that trigger redraws + editorState.rev.changes().subscribe(() => { + editorState.obs_val_draw_state.set(editorState.toSnapshot(undefined)); + }); + editorState.obs_val_draw_state.observable().subscribe((ds) => + requestAnimationFrame(() => { + draw(editorState.canvas.getContext("2d"), ds); + }), + ); + + // Configure initial settings and stim + setIndentCircuitLines(traitlets.getIndentCircuitLines()); + setCurveConnectors(traitlets.getCurveConnectors()); + setShowAnnotationRegions(traitlets.getShowAnnotationRegions()); + commitStimCircuit(traitlets.getStim()); +} +export default { render }; diff --git a/glue/crumble/package-lock.json b/glue/crumpy/src/js/package-lock.json similarity index 100% rename from glue/crumble/package-lock.json rename to glue/crumpy/src/js/package-lock.json diff --git a/glue/crumpy/src/js/package.json b/glue/crumpy/src/js/package.json new file mode 100644 index 000000000..fb0f03b34 --- /dev/null +++ b/glue/crumpy/src/js/package.json @@ -0,0 +1,15 @@ +{ + "name": "crumpy", + "type": "module", + "main": "js/main.js", + "scripts": { + "copy": "cp -r ../../../crumble ./crumble_js/ && cp draw/* crumble_js/draw/ && cp main.js crumble_js/", + "bundle": "esbuild --bundle --format=esm --outfile=../bundle.js ./crumble_js/main.js", + "cleanup": "rm -r crumble_js", + "build": "npm run copy && npm run bundle && npm run cleanup", + "watch": "npm run copy && npm run bundle -- --watch" + }, + "devDependencies": { + "esbuild": "0.25.8" + } +} \ No newline at end of file diff --git a/glue/crumble/crumpy/test_package.py b/glue/crumpy/tests/test_package.py similarity index 100% rename from glue/crumble/crumpy/test_package.py rename to glue/crumpy/tests/test_package.py diff --git a/glue/crumble/crumpy/test_widget.py b/glue/crumpy/tests/test_widget.py similarity index 100% rename from glue/crumble/crumpy/test_widget.py rename to glue/crumpy/tests/test_widget.py From 188088e62f7337fa0396d282834581ee0aa42a4d Mon Sep 17 00:00:00 2001 From: Guen Prawiroatmodjo Date: Wed, 24 Sep 2025 18:02:17 -0700 Subject: [PATCH 4/4] update README --- glue/crumpy/README.md | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/glue/crumpy/README.md b/glue/crumpy/README.md index 53e694102..e2f87f7a9 100644 --- a/glue/crumpy/README.md +++ b/glue/crumpy/README.md @@ -112,31 +112,37 @@ guide and Python best practices. CrumPy makes use of the widget creation tool [AnyWidget](https://anywidget.dev/). With AnyWidget, Python classes are able to -take JavaScript and display custom widgets in Jupyter environments. CrumPy -therefore includes both JavaScript and Python subparts: +take JavaScript and display custom widgets in Jupyter environments. In order to +visualize Crumble within CrumPy, some modifications in the Crumble source code +were needed. CrumPy therefore includes both JavaScript and Python subparts: ```text glue/ -├── crumble/ # Modified Crumble code -│ ├── crumpy/ -│ │ ├── __init__.py # Python code for CircuitWidget -│ │ └── bundle.js # Bundled JavaScript will appear here -│ ├── main.js # Main circuit visualization/setup +├── crumpy/ # Modified Crumble code +│ ├── src/ +│ │ ├── crumpy/ +│ │ │ └── __init__.py # Python code for CircuitWidget +│ │ ├── js/ +│ │ │ ├── draw/ # Modified JS source code +│ │ │ ├── main.js # Main circuit visualization/setup +│ │ │ ├── bundle.js # Bundled JavaScript will appear here +│ │ │ └── package.json # Bundling setup and scripts │ └── ... -│ └── package.json # Bundling setup and scripts -├── tests/ # Python tests -└── ... +│ ├── tests/ # Python tests +│ ├── simple_example.ipynb # Example notebook +│ └── getting_started.ipynb # Getting started notebook ``` -`glue/crumble/crumpy` contains the main Python package code. +`glue/crumpy` contains the main Python package code. -`glue/crumble/crumpy/__init__.py` contains the main class of the `crumpy` package, +`glue/crumpy/src/crumpy/__init__.py` contains the main class of the `crumpy` package, `CircuitWidget`. -`glue/crumble/` contains the modified Crumble circuit visualization code that -will be rendered in the `CircuitWidget` widget. +`glue/crumpy/src/js/` contains the modified Crumble circuit visualization code that +will be rendered in the `CircuitWidget` widget. In the build step, we copy the Crumble +source code and modify it with the `.js` files in this folder. -`glue/crumble/main.js` contains the main circuit visualization and setup logic +`glue/crumpy/src/js/main.js` contains the main circuit visualization and setup logic in the form of the `render` function [used by AnyWidget](https://anywidget.dev/en/afm/#anywidget-front-end-module-afm). @@ -159,6 +165,13 @@ A new bundle should appear at `src/js/bundle.js`. you may need to restart the notebook kernel and rerun any widget-displaying code for the changes to take effect. +### Install from source + +After creating the bundle, you can install the package from source: + +1. Navigate to `glue/crumpy` +2. Run `pip install -e .` in your favorite Python environment +