diff --git a/.gitignore b/.gitignore index 76441ea0b..a6f13f16a 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ build.ninja node_modules MODULE.bazel.lock .ninja_lock +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 25c6c739b..d0b731d88 100644 --- a/glue/crumble/draw/config.js +++ b/glue/crumble/draw/config.js @@ -2,5 +2,6 @@ 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 curveConnectors = true; -export {pitch, rad, OFFSET_X, OFFSET_Y}; +export {pitch, rad, OFFSET_X, OFFSET_Y, curveConnectors}; diff --git a/glue/crumble/gates/gate_draw_util.js b/glue/crumble/gates/gate_draw_util.js index 54bb30bcc..8d2081b01 100644 --- a/glue/crumble/gates/gate_draw_util.js +++ b/glue/crumble/gates/gate_draw_util.js @@ -1,4 +1,4 @@ -import {pitch, rad} from "../draw/config.js" +import { curveConnectors, pitch, rad } from "../draw/config.js"; /** * @param {!CanvasRenderingContext2D} ctx @@ -210,7 +210,7 @@ function stroke_connector_to(ctx, x1, y1, x2, y2) { ctx.beginPath(); ctx.moveTo(x1, y1); - if (d < pitch * 1.1) { + 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); diff --git a/glue/crumpy/README.md b/glue/crumpy/README.md new file mode 100644 index 000000000..e2f87f7a9 --- /dev/null +++ b/glue/crumpy/README.md @@ -0,0 +1,190 @@ +
+ +# 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. 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/ +├── 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 +│ └── ... +│ ├── tests/ # Python tests +│ ├── simple_example.ipynb # Example notebook +│ └── getting_started.ipynb # Getting started notebook +``` + +`glue/crumpy` contains the main Python package code. + +`glue/crumpy/src/crumpy/__init__.py` contains the main class of the `crumpy` package, +`CircuitWidget`. + +`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/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). + +### 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. + +### 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 + + + + +[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/crumpy/pyproject.toml b/glue/crumpy/pyproject.toml new file mode 100644 index 000000000..ca3345229 --- /dev/null +++ b/glue/crumpy/pyproject.toml @@ -0,0 +1,177 @@ +[build-system] +requires = ["poetry-core>=2.0", "poetry-dynamic-versioning"] +build-backend = "poetry_dynamic_versioning.backend" + + +[project] +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" +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/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] +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", from = "src" }] +include = [ + { path = "src/js/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 = ["src/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 = ["src", "tests"] +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", +] 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/crumpy/src/crumpy/__init__.py b/glue/crumpy/src/crumpy/__init__.py new file mode 100644 index 000000000..fd84d4572 --- /dev/null +++ b/glue/crumpy/src/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.parent / "js" + + +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/crumpy/src/crumpy/py.typed b/glue/crumpy/src/crumpy/py.typed new file mode 100644 index 000000000..e69de29bb 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/crumpy/src/js/package-lock.json b/glue/crumpy/src/js/package-lock.json new file mode 100644 index 000000000..3ca99961a --- /dev/null +++ b/glue/crumpy/src/js/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/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/crumpy/tests/test_package.py b/glue/crumpy/tests/test_package.py new file mode 100644 index 000000000..73f7cd39f --- /dev/null +++ b/glue/crumpy/tests/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/crumpy/tests/test_widget.py b/glue/crumpy/tests/test_widget.py new file mode 100644 index 000000000..dfd9d3df4 --- /dev/null +++ b/glue/crumpy/tests/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