Skip to content

Commit e3b1564

Browse files
zzzappyGuen Prawiroatmodjo
authored andcommitted
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
1 parent c16f834 commit e3b1564

13 files changed

Lines changed: 1761 additions & 1283 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ build.ninja
4444
node_modules
4545
MODULE.bazel.lock
4646
.ninja_lock
47+
glue/crumble/node_modules
48+
glue/crumble/crumpy/bundle.js

glue/crumble/crumpy/__init__.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"""
2+
Copyright (c) 2025 Riverlane. All rights reserved.
3+
4+
crumpy: A python library for visualizing Crumble circuits in Jupyter
5+
"""
6+
7+
# mypy: ignore-errors
8+
# pylint: disable=abstract-method
9+
10+
from __future__ import annotations
11+
12+
import pathlib
13+
from importlib.metadata import version
14+
15+
import anywidget
16+
import cirq
17+
import qiskit
18+
import qiskit.qasm3
19+
import stim
20+
import stimcirq
21+
import traitlets
22+
from cirq.contrib.qasm_import import circuit_from_qasm
23+
24+
__version__ = version(__name__)
25+
26+
__all__ = ["__version__"]
27+
28+
bundler_output_dir = pathlib.Path(__file__).parent
29+
30+
31+
class CircuitWidget(anywidget.AnyWidget):
32+
"""A Jupyter widget for displaying Crumble circuit diagrams.
33+
34+
Attributes
35+
----------
36+
stim : str
37+
Stim circuit to be drawn.
38+
indentCircuitLines : bool
39+
If circuit lines for subsequent qubits in the same row get indented when drawn.
40+
Defaults to True (matching Crumble).
41+
curveConnectors : bool
42+
If connectors (e.g., the line between control and target of a CNOT) can be drawn curved.
43+
Defaults to True (matching Crumble).
44+
showAnnotationRegions : bool
45+
If detector region and observable marks are shown.
46+
Defaults to True (matching Crumble).
47+
"""
48+
49+
_esm = bundler_output_dir / "bundle.js"
50+
stim = traitlets.Unicode(default_value="", help="Stim circuit to be drawn").tag(
51+
sync=True
52+
)
53+
indentCircuitLines = traitlets.Bool(True).tag(sync=True)
54+
curveConnectors = traitlets.Bool(True).tag(sync=True)
55+
showAnnotationRegions = traitlets.Bool(True).tag(sync=True)
56+
57+
@staticmethod
58+
def from_cirq(cirq_circuit: cirq.Circuit):
59+
"""Create a `CircuitWidget` from a `cirq.Circuit`.
60+
61+
`cirq_circuit` will be transpiled to a `stim.Circuit` before use with `CircuitWidget`.
62+
`cirq_circuit` must be transpilable to a `stim.Circuit`.
63+
64+
Parameters
65+
----------
66+
cirq_circuit : cirq.Circuit
67+
cirq circuit to visualize
68+
69+
Raises
70+
------
71+
TypeError
72+
If `cirq_circuit` is unable to be converted to a `stim.Circuit`
73+
"""
74+
try:
75+
stim_circuit = stimcirq.cirq_circuit_to_stim_circuit(cirq_circuit)
76+
return CircuitWidget(stim=str(stim_circuit))
77+
except TypeError as ex:
78+
msg = "Unable to translate cirq circuit to stim."
79+
raise TypeError(msg) from ex
80+
81+
@staticmethod
82+
def from_qiskit(qiskit_circuit: qiskit.QuantumCircuit):
83+
"""Create a `CircuitWidget` from a `qiskit.QuantumCircuit`.
84+
85+
`qiskit_circuit` will be transpiled to a `stim.Circuit` before use with `CircuitWidget`.
86+
`qiskit_circuit` must be transpilable to a `stim.Circuit`.
87+
88+
Parameters
89+
----------
90+
qiskit_circuit : qiskit.QuantumCircuit
91+
qiskit circuit to visualize
92+
93+
Raises
94+
------
95+
TypeError
96+
If `qiskit_circuit` is unable to be converted to a `stim.Circuit`
97+
"""
98+
try:
99+
qasm_circuit = qiskit.qasm3.dumps(qiskit_circuit)
100+
cirq_circuit = circuit_from_qasm(qasm_circuit)
101+
return CircuitWidget.from_cirq(cirq_circuit)
102+
except TypeError as ex:
103+
msg = "Unable to translate qiskit circuit to stim."
104+
raise TypeError(msg) from ex
105+
106+
@staticmethod
107+
def from_stim(stim_circuit: stim.Circuit):
108+
"""Create a `CircuitWidget` from a `stim.Circuit`.
109+
110+
Parameters
111+
----------
112+
stim_circuit : stim.Circuit
113+
stim circuit to visualize
114+
"""
115+
return CircuitWidget(stim=str(stim_circuit))

glue/crumble/crumpy/py.typed

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from __future__ import annotations
2+
3+
import importlib.metadata
4+
5+
import crumpy as m
6+
7+
8+
def test_version():
9+
assert m.__version__ in importlib.metadata.version("crumpy")

glue/crumble/crumpy/test_widget.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# mypy: ignore-errors
2+
3+
from __future__ import annotations
4+
5+
import cirq
6+
import ipywidgets
7+
import pytest
8+
import qiskit
9+
import stim
10+
import traitlets
11+
12+
from crumpy import CircuitWidget
13+
14+
15+
def test_basic_no_exception():
16+
"""CircuitWidget should initialize without errors."""
17+
CircuitWidget(stim="H 0;", indentCircuitLines=False, curveConnectors=False)
18+
19+
20+
def test_bad_stim_instruction_no_exception():
21+
"""CircuitWidget should not raise an error if given any invalid stim instructions."""
22+
CircuitWidget(stim="H 0;thisIsNotAnInstruction 123;CNOT 0 1;")
23+
24+
25+
def test_bad_trait_assignment_exception():
26+
"""Assigning a non-string value to a CircuitWidget's stim should raise a TraitError."""
27+
widg = CircuitWidget()
28+
with pytest.raises(traitlets.TraitError):
29+
widg.stim = 123
30+
31+
32+
def test_has_traits():
33+
"""CircuitWidget should have the expected traits and sync metadata."""
34+
widg = CircuitWidget()
35+
assert widg.has_trait("stim")
36+
assert widg.has_trait("indentCircuitLines")
37+
assert widg.has_trait("curveConnectors")
38+
assert widg.has_trait("showAnnotationRegions")
39+
assert widg.trait_metadata("stim", "sync")
40+
assert widg.trait_metadata("indentCircuitLines", "sync")
41+
assert widg.trait_metadata("curveConnectors", "sync")
42+
assert widg.trait_metadata("showAnnotationRegions", "sync")
43+
44+
45+
def test_use_case_dlink():
46+
"""CircuitWidget should update its stim when linked to an IntSlider via ipywidgets.dlink."""
47+
48+
def sliderToCircuit(slider_val):
49+
return "H 0;" * int(slider_val)
50+
51+
circuit = CircuitWidget()
52+
slider_default_value = 1
53+
slider = ipywidgets.IntSlider(
54+
description="# of H gates", value=slider_default_value, min=0, max=30
55+
)
56+
ipywidgets.dlink((slider, "value"), (circuit, "stim"), sliderToCircuit)
57+
assert circuit.stim == sliderToCircuit(slider_default_value)
58+
59+
new_slider_value = 5
60+
slider.value = new_slider_value
61+
assert circuit.stim == sliderToCircuit(new_slider_value)
62+
63+
new_stim = "Y 0;"
64+
circuit.stim = new_stim
65+
assert circuit.stim == new_stim
66+
67+
68+
class Test_CircuitImporting:
69+
def test_from_cirq_valid(self):
70+
"""CircuitWidget.from_cirq should create a CircuitWidget with the correct circuit when given a valid (stim-transpilable) cirq circuit."""
71+
72+
q0 = cirq.LineQubit(0)
73+
q1 = cirq.LineQubit(1)
74+
cirq_circuit = cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1))
75+
76+
widg = CircuitWidget.from_cirq(cirq_circuit)
77+
78+
assert "H 0" in widg.stim
79+
assert "CNOT 0 1" in widg.stim or "CX 0 1" in widg.stim
80+
81+
def test_from_cirq_invalid(self):
82+
"""CircuitWidget.from_cirq should raise an error when given an invalid (non-stim-transpilable) cirq circuit."""
83+
84+
q0 = cirq.LineQubit(0)
85+
q1 = cirq.LineQubit(1)
86+
cirq_circuit = cirq.Circuit(cirq.H(q0), cirq.CNOT(q0, q1), cirq.rx(2).on(q0))
87+
with pytest.raises(TypeError):
88+
CircuitWidget.from_cirq(cirq_circuit)
89+
90+
def test_from_qiskit_valid(self):
91+
"""CircuitWidget.from_qiskit should create a CircuitWidget with the correct circuit when given a valid (stim-transpilable) qiskit circuit."""
92+
93+
qc = qiskit.QuantumCircuit(2)
94+
qc.z(0)
95+
qc.cx(1, 0)
96+
97+
widg = CircuitWidget.from_qiskit(qc)
98+
99+
assert "Z 0" in widg.stim
100+
assert "CNOT 1 0" in widg.stim or "CX 1 0" in widg.stim
101+
102+
def test_from_qiskit_invalid(self):
103+
"""CircuitWidget.from_qiskit should raise an error when given an invalid (non-stim-transpilable) qiskit circuit."""
104+
105+
qc = qiskit.QuantumCircuit(2)
106+
qc.z(0)
107+
qc.cx(1, 0)
108+
qc.ry(1.234, 0)
109+
110+
with pytest.raises(TypeError):
111+
CircuitWidget.from_qiskit(qc)
112+
113+
def test_from_stim(self):
114+
"""CircuitWidget.from_stim should create a CircuitWidget with the given stim circuit."""
115+
116+
stim_circuit = stim.Circuit("""
117+
X 1
118+
CY 0 1
119+
""")
120+
121+
widg = CircuitWidget.from_stim(stim_circuit)
122+
123+
assert "X 1" in widg.stim
124+
assert "CY 0 1" in widg.stim # also covers alternate name ZCY

glue/crumble/draw/config.js

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,61 @@
1+
/**
2+
* Copyright 2023 Craig Gidney
3+
* Copyright 2025 Riverlane
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
* Modifications:
18+
* - Refactored for CrumPy
19+
*/
20+
121
const pitch = 50;
222
const rad = 10;
323
const OFFSET_X = -pitch + Math.floor(pitch / 4) + 0.5;
424
const OFFSET_Y = -pitch + Math.floor(pitch / 4) + 0.5;
25+
let indentCircuitLines = true;
26+
let curveConnectors = true;
27+
let showAnnotationRegions = true;
28+
29+
const setIndentCircuitLines = (newBool) => {
30+
if (typeof newBool !== "boolean") {
31+
throw new TypeError(`Expected a boolean, but got ${typeof newBool}`);
32+
}
33+
indentCircuitLines = newBool;
34+
};
35+
36+
const setCurveConnectors = (newBool) => {
37+
if (typeof newBool !== "boolean") {
38+
throw new TypeError(`Expected a boolean, but got ${typeof newBool}`);
39+
}
40+
curveConnectors = newBool;
41+
};
42+
43+
const setShowAnnotationRegions = (newBool) => {
44+
if (typeof newBool !== "boolean") {
45+
throw new TypeError(`Expected a boolean, but got ${typeof newBool}`);
46+
}
47+
showAnnotationRegions = newBool;
48+
};
549

6-
export {pitch, rad, OFFSET_X, OFFSET_Y};
50+
export {
51+
pitch,
52+
rad,
53+
OFFSET_X,
54+
OFFSET_Y,
55+
indentCircuitLines,
56+
curveConnectors,
57+
showAnnotationRegions,
58+
setIndentCircuitLines,
59+
setCurveConnectors,
60+
setShowAnnotationRegions,
61+
};

0 commit comments

Comments
 (0)