QuantumSheets 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantumsheets-0.1.0/PKG-INFO +34 -0
- quantumsheets-0.1.0/QuantumSheets/__init__.py +8 -0
- quantumsheets-0.1.0/QuantumSheets/__main__.py +3 -0
- quantumsheets-0.1.0/QuantumSheets/brace.py +24 -0
- quantumsheets-0.1.0/QuantumSheets/cli.py +76 -0
- quantumsheets-0.1.0/QuantumSheets/layout.py +126 -0
- quantumsheets-0.1.0/QuantumSheets/render.py +384 -0
- quantumsheets-0.1.0/QuantumSheets.egg-info/PKG-INFO +34 -0
- quantumsheets-0.1.0/QuantumSheets.egg-info/SOURCES.txt +15 -0
- quantumsheets-0.1.0/QuantumSheets.egg-info/dependency_links.txt +1 -0
- quantumsheets-0.1.0/QuantumSheets.egg-info/entry_points.txt +2 -0
- quantumsheets-0.1.0/QuantumSheets.egg-info/requires.txt +2 -0
- quantumsheets-0.1.0/QuantumSheets.egg-info/top_level.txt +1 -0
- quantumsheets-0.1.0/README.md +24 -0
- quantumsheets-0.1.0/pyproject.toml +20 -0
- quantumsheets-0.1.0/setup.cfg +4 -0
- quantumsheets-0.1.0/tests/test_quantumsheets.py +74 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: QuantumSheets
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Musical staff notation for quantum circuits
|
|
5
|
+
Author: Ben Bar
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: qiskit
|
|
9
|
+
Requires-Dist: matplotlib
|
|
10
|
+
|
|
11
|
+
# QuantumSheets
|
|
12
|
+
|
|
13
|
+
Renders Qiskit QuantumCircuits as sheet-music-style diagrams where each qubit is a five-line musical staff.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
You can install the package directly from this repository:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python -m QuantumSheets my_circuit.qasm -o output.png
|
|
27
|
+
python -m QuantumSheets my_circuit.py -o output.png --style clean
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or using the CLI directly:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
quantumsheets my_circuit.py -o output.png
|
|
34
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QuantumSheets — Musical staff notation for quantum circuits.
|
|
3
|
+
|
|
4
|
+
Renders Qiskit QuantumCircuits as sheet-music-style diagrams where each
|
|
5
|
+
qubit is a five-line musical staff.
|
|
6
|
+
"""
|
|
7
|
+
from .layout import circuit_to_moments, GateEvent
|
|
8
|
+
from .render import draw_circuit, StaffCircuitDrawer
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
brace.py
|
|
3
|
+
--------
|
|
4
|
+
Draws the left-edge bracket that groups the qubit staves into one
|
|
5
|
+
"system", the way an orchestral/ensemble score groups separate
|
|
6
|
+
instrument staves with a square bracket (a piano uses a curly brace
|
|
7
|
+
for ONE instrument with two staves; separate wires are more honestly
|
|
8
|
+
drawn as a bracketed group of separate staves).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def draw_vertical_brace(ax, x, y_top, y_bottom, depth=0.32, lw=2.4, color="black", zorder=5):
|
|
13
|
+
"""
|
|
14
|
+
Draws a '[' shaped bracket spanning from y_top down to y_bottom,
|
|
15
|
+
with its spine at x - depth and small serifs pointing right at
|
|
16
|
+
both ends.
|
|
17
|
+
"""
|
|
18
|
+
spine_x = x - depth
|
|
19
|
+
ax.plot([spine_x, spine_x], [y_top, y_bottom], color=color, lw=lw,
|
|
20
|
+
solid_capstyle="round", zorder=zorder)
|
|
21
|
+
ax.plot([spine_x, spine_x + depth], [y_top, y_top], color=color, lw=lw,
|
|
22
|
+
solid_capstyle="round", zorder=zorder)
|
|
23
|
+
ax.plot([spine_x, spine_x + depth], [y_bottom, y_bottom], color=color, lw=lw,
|
|
24
|
+
solid_capstyle="round", zorder=zorder)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli.py
|
|
3
|
+
------
|
|
4
|
+
Command-line interface for QuantumSheets. Run on a .qasm or .py file
|
|
5
|
+
that defines a QuantumCircuit to produce a musical-staff diagram.
|
|
6
|
+
|
|
7
|
+
Usage
|
|
8
|
+
-----
|
|
9
|
+
python -m QuantumSheets my_circuit.qasm -o output.png
|
|
10
|
+
python -m QuantumSheets my_circuit.py -o output.png --style clean
|
|
11
|
+
"""
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_circuit(path):
|
|
18
|
+
"""Load a QuantumCircuit from a .qasm or .py file."""
|
|
19
|
+
ext = os.path.splitext(path)[1].lower()
|
|
20
|
+
|
|
21
|
+
if ext in (".qasm",):
|
|
22
|
+
from qiskit import QuantumCircuit
|
|
23
|
+
return QuantumCircuit.from_qasm_file(path)
|
|
24
|
+
|
|
25
|
+
if ext == ".py":
|
|
26
|
+
# Execute the .py file; expect it to leave a 'qc' variable in its namespace.
|
|
27
|
+
ns = {}
|
|
28
|
+
with open(path) as f:
|
|
29
|
+
exec(compile(f.read(), path, "exec"), ns)
|
|
30
|
+
for name in ("qc", "circuit", "circ"):
|
|
31
|
+
if name in ns:
|
|
32
|
+
return ns[name]
|
|
33
|
+
raise RuntimeError(
|
|
34
|
+
f"Could not find a QuantumCircuit variable named 'qc', 'circuit', or 'circ' in {path}"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
raise ValueError(f"Unsupported file extension: {ext!r} (expected .qasm or .py)")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def main(argv=None):
|
|
41
|
+
parser = argparse.ArgumentParser(
|
|
42
|
+
prog="quantumsheets",
|
|
43
|
+
description="Render a quantum circuit as a musical-staff diagram.",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument("input", help="Path to a .qasm or .py file defining a QuantumCircuit")
|
|
46
|
+
parser.add_argument("-o", "--output", default=None,
|
|
47
|
+
help="Output filename (png/svg/pdf). Default: show in window.")
|
|
48
|
+
parser.add_argument("--style", choices=("ink", "clean"), default="ink",
|
|
49
|
+
help="Visual style (default: ink)")
|
|
50
|
+
parser.add_argument("--dpi", type=int, default=200, help="Output resolution")
|
|
51
|
+
parser.add_argument("--title", default=None, help="Optional title above the score")
|
|
52
|
+
parser.add_argument("--gates-per-measure", type=int, default=3,
|
|
53
|
+
help="How many gate columns between barlines")
|
|
54
|
+
|
|
55
|
+
args = parser.parse_args(argv)
|
|
56
|
+
|
|
57
|
+
qc = _load_circuit(args.input)
|
|
58
|
+
|
|
59
|
+
from .render import draw_circuit
|
|
60
|
+
fig = draw_circuit(
|
|
61
|
+
qc,
|
|
62
|
+
filename=args.output,
|
|
63
|
+
style=args.style,
|
|
64
|
+
dpi=args.dpi,
|
|
65
|
+
title=args.title,
|
|
66
|
+
gates_per_measure=args.gates_per_measure,
|
|
67
|
+
)
|
|
68
|
+
if args.output:
|
|
69
|
+
print(f"Saved to {args.output}")
|
|
70
|
+
else:
|
|
71
|
+
import matplotlib.pyplot as plt
|
|
72
|
+
plt.show()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
main()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
layout.py
|
|
3
|
+
---------
|
|
4
|
+
Turns a qiskit QuantumCircuit into a simple, drawing-agnostic list of
|
|
5
|
+
"moments" (columns). Each moment holds the operations that happen at
|
|
6
|
+
that horizontal position, exactly the way a textbook circuit diagram
|
|
7
|
+
(and our staff-notation diagram) lays gates out: every wire only ever
|
|
8
|
+
has ONE gate per column, and a gate's column is the first free column
|
|
9
|
+
after all of its wires' previous gates.
|
|
10
|
+
|
|
11
|
+
This file has no matplotlib / drawing code in it on purpose, so it can
|
|
12
|
+
be unit-tested or reused by a different renderer later.
|
|
13
|
+
"""
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import List, Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class GateEvent:
|
|
20
|
+
name: str # qiskit instruction name, e.g. "h", "cx", "measure"
|
|
21
|
+
label: str # human readable label to print, e.g. "H", "CNOT", "RZ(1.57)"
|
|
22
|
+
qubits: List[int] # ALL qubit rows touched (controls + targets), in circuit order
|
|
23
|
+
controls: List[int] # subset of qubits that act as controls (empty for non-controlled gates)
|
|
24
|
+
targets: List[int] # subset of qubits that are "acted upon" (targets)
|
|
25
|
+
clbits: List[int] = field(default_factory=list)
|
|
26
|
+
kind: str = "generic" # "single" | "control" | "swap" | "measure" | "barrier" | "generic"
|
|
27
|
+
column: int = 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# qiskit instruction name -> pretty label
|
|
31
|
+
_PRETTY = {
|
|
32
|
+
"h": "H", "x": "X", "y": "Y", "z": "Z", "s": "S", "sdg": "S†",
|
|
33
|
+
"t": "T", "tdg": "T†", "sx": "√X", "sxdg": "√X†", "id": "I",
|
|
34
|
+
"cx": "CNOT", "cy": "CY", "cz": "CZ", "ch": "CH", "swap": "SWAP",
|
|
35
|
+
"ccx": "TOFFOLI", "cswap": "CSWAP", "measure": "MEAS", "barrier": "BARRIER",
|
|
36
|
+
"reset": "RESET",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_PARAM_GATES = {"rx", "ry", "rz", "p", "u", "u1", "u2", "u3", "crx", "cry", "crz", "cp", "cu"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _fmt_params(params):
|
|
43
|
+
out = []
|
|
44
|
+
for p in params:
|
|
45
|
+
try:
|
|
46
|
+
out.append(f"{float(p):.2f}")
|
|
47
|
+
except (TypeError, ValueError):
|
|
48
|
+
out.append(str(p))
|
|
49
|
+
return ",".join(out)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _pretty_label(name, params):
|
|
53
|
+
base = _PRETTY.get(name, name.upper())
|
|
54
|
+
if name in _PARAM_GATES and params:
|
|
55
|
+
base = f"{base}({_fmt_params(params)})"
|
|
56
|
+
return base
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def circuit_to_moments(qc) -> List[List[GateEvent]]:
|
|
60
|
+
"""
|
|
61
|
+
Greedily packs qc.data into moments (columns), one gate per wire per
|
|
62
|
+
column, mirroring how standard circuit diagrams (and qiskit's own
|
|
63
|
+
drawer) lay things out. Returns a list of moments; each moment is a
|
|
64
|
+
list of GateEvent objects sharing that column.
|
|
65
|
+
"""
|
|
66
|
+
n_qubits = qc.num_qubits
|
|
67
|
+
next_free_col = [0] * n_qubits # next available column per qubit wire
|
|
68
|
+
events: List[GateEvent] = []
|
|
69
|
+
|
|
70
|
+
for instr in qc.data:
|
|
71
|
+
op = instr.operation
|
|
72
|
+
name = op.name
|
|
73
|
+
if name in ("delay",):
|
|
74
|
+
# timing-only instruction, irrelevant to the diagram
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
q_idx = [qc.find_bit(q).index for q in instr.qubits]
|
|
78
|
+
c_idx = [qc.find_bit(c).index for c in instr.clbits] if instr.clbits else []
|
|
79
|
+
|
|
80
|
+
if not q_idx:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
col = max(next_free_col[q] for q in q_idx)
|
|
84
|
+
|
|
85
|
+
if name == "barrier":
|
|
86
|
+
kind = "barrier"
|
|
87
|
+
controls, targets = [], q_idx
|
|
88
|
+
elif name == "measure":
|
|
89
|
+
kind = "measure"
|
|
90
|
+
controls, targets = [], q_idx
|
|
91
|
+
elif name == "swap":
|
|
92
|
+
kind = "swap"
|
|
93
|
+
controls, targets = [], q_idx
|
|
94
|
+
elif len(q_idx) >= 2 and name.startswith("c") and name not in ("cswap",):
|
|
95
|
+
# cx, cy, cz, ch, crx, cry, crz, cp, cu, ccx (toffoli), mcx...
|
|
96
|
+
kind = "control"
|
|
97
|
+
n_ctrl = len(q_idx) - 1
|
|
98
|
+
controls, targets = q_idx[:n_ctrl], q_idx[n_ctrl:]
|
|
99
|
+
elif name == "cswap":
|
|
100
|
+
kind = "control" # controlled-swap: draw control + swap pair as generic control group
|
|
101
|
+
controls, targets = [q_idx[0]], q_idx[1:]
|
|
102
|
+
elif len(q_idx) == 1:
|
|
103
|
+
kind = "single"
|
|
104
|
+
controls, targets = [], q_idx
|
|
105
|
+
else:
|
|
106
|
+
kind = "generic"
|
|
107
|
+
controls, targets = [], q_idx
|
|
108
|
+
|
|
109
|
+
label = _pretty_label(name, getattr(op, "params", []))
|
|
110
|
+
ev = GateEvent(
|
|
111
|
+
name=name, label=label, qubits=q_idx, controls=controls,
|
|
112
|
+
targets=targets, clbits=c_idx, kind=kind, column=col,
|
|
113
|
+
)
|
|
114
|
+
events.append(ev)
|
|
115
|
+
|
|
116
|
+
for q in q_idx:
|
|
117
|
+
next_free_col[q] = col + 1
|
|
118
|
+
|
|
119
|
+
if not events:
|
|
120
|
+
return []
|
|
121
|
+
|
|
122
|
+
n_cols = max(e.column for e in events) + 1
|
|
123
|
+
moments: List[List[GateEvent]] = [[] for _ in range(n_cols)]
|
|
124
|
+
for e in events:
|
|
125
|
+
moments[e.column].append(e)
|
|
126
|
+
return moments
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""
|
|
2
|
+
render.py
|
|
3
|
+
---------
|
|
4
|
+
Draws a qiskit QuantumCircuit as a "musical staff" diagram: one 5-line
|
|
5
|
+
staff per qubit, a real treble-clef glyph, filled/hollow noteheads with
|
|
6
|
+
stems for gates, an X-notehead for measurements, a proper circle-plus
|
|
7
|
+
target for CNOT, crossing lines for SWAP, and a text label under (or
|
|
8
|
+
beside) every symbol naming exactly which gate it is -- so it looks like
|
|
9
|
+
sheet music but reads like a circuit diagram.
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import numpy as np
|
|
13
|
+
import matplotlib
|
|
14
|
+
matplotlib.use("Agg")
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
import matplotlib.font_manager as fm
|
|
17
|
+
from matplotlib.patches import Circle, Ellipse, FancyBboxPatch
|
|
18
|
+
from matplotlib.path import Path
|
|
19
|
+
import matplotlib.patches as mpatches
|
|
20
|
+
|
|
21
|
+
from .layout import circuit_to_moments, GateEvent
|
|
22
|
+
from .brace import draw_vertical_brace
|
|
23
|
+
|
|
24
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
25
|
+
_FONT_PATH = os.path.join(_HERE, "Euterpe.ttf")
|
|
26
|
+
|
|
27
|
+
# Candidate music-symbol fonts in preference order
|
|
28
|
+
_FALLBACK_FONTS = [
|
|
29
|
+
"/System/Library/Fonts/Apple Symbols.ttf", # macOS
|
|
30
|
+
"/usr/share/fonts/truetype/noto/NotoMusic-Regular.ttf", # Linux (Noto)
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# Musical Symbols block -- see glyph catalogue.
|
|
34
|
+
GLYPH = {
|
|
35
|
+
"gclef": "\U0001D11E",
|
|
36
|
+
"barline": "\U0001D100",
|
|
37
|
+
"final_barline": "\U0001D102",
|
|
38
|
+
"notehead_black": "\U0001D158",
|
|
39
|
+
"notehead_white": "\U0001D157",
|
|
40
|
+
"x_notehead": "\U0001D143",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
STYLES = {
|
|
44
|
+
"ink": dict(bg="#efe4c9", ink="#1f4d36", accent="#c99a2e", paper_edge="#d8c79f"),
|
|
45
|
+
"clean": dict(bg="#ffffff", ink="#1a1a1a", accent="#b8860b", paper_edge="#ffffff"),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# ── Geometry ──────────────────────────────────────────────────────────
|
|
49
|
+
# All units are "staff units": 1 unit = distance between two staff lines
|
|
50
|
+
LINE_SPACING = 0.55 # ← tighter lines, like real sheet music
|
|
51
|
+
N_LINES = 5
|
|
52
|
+
STAFF_HEIGHT = (N_LINES - 1) * LINE_SPACING
|
|
53
|
+
STAFF_GAP = 1.8 # ← closer staves (was 3.4)
|
|
54
|
+
DX = 2.0 # horizontal step per moment column
|
|
55
|
+
MEASURE_GAP = 0.8
|
|
56
|
+
X_START = 4.0
|
|
57
|
+
LEFT_LABEL_X = 1.5
|
|
58
|
+
CLEF_X = 2.35
|
|
59
|
+
BRACE_X = 0.45
|
|
60
|
+
|
|
61
|
+
# Note stem length (in staff units)
|
|
62
|
+
STEM_LEN = 2.0
|
|
63
|
+
|
|
64
|
+
# deterministic small pitch variety for single-qubit gates (purely decorative;
|
|
65
|
+
# which STAFF a note sits on -- not its height -- is what carries meaning)
|
|
66
|
+
_PITCH_OFFSET = {
|
|
67
|
+
"h": 0.3, "x": -0.3, "y": 0.6, "z": -0.6, "s": 0.6, "sdg": -0.6,
|
|
68
|
+
"t": 0.0, "tdg": 0.0, "sx": 0.3, "sxdg": -0.3, "id": 0.0, "reset": -0.6,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_MUSIC_FONT_PROP = None # cached after first call
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _music_font():
|
|
75
|
+
"""Return a FontProperties that can render music-symbol glyphs, or None."""
|
|
76
|
+
global _MUSIC_FONT_PROP
|
|
77
|
+
if _MUSIC_FONT_PROP is not None:
|
|
78
|
+
return _MUSIC_FONT_PROP if _MUSIC_FONT_PROP != "NONE" else None
|
|
79
|
+
|
|
80
|
+
candidates = [_FONT_PATH] + _FALLBACK_FONTS
|
|
81
|
+
for path in candidates:
|
|
82
|
+
if os.path.isfile(path):
|
|
83
|
+
try:
|
|
84
|
+
fm.fontManager.addfont(path)
|
|
85
|
+
_MUSIC_FONT_PROP = fm.FontProperties(fname=path)
|
|
86
|
+
return _MUSIC_FONT_PROP
|
|
87
|
+
except Exception:
|
|
88
|
+
continue
|
|
89
|
+
# No music font found — callers will draw a text fallback.
|
|
90
|
+
_MUSIC_FONT_PROP = "NONE"
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _pitch_offset(ev: GateEvent) -> float:
|
|
95
|
+
if ev.name in _PITCH_OFFSET:
|
|
96
|
+
return _PITCH_OFFSET[ev.name]
|
|
97
|
+
return 0.4 if (hash(ev.name) % 2 == 0) else -0.4
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class StaffCircuitDrawer:
|
|
101
|
+
def __init__(self, qc, style="clean", gates_per_measure=3, title=None):
|
|
102
|
+
self.qc = qc
|
|
103
|
+
self.n_qubits = qc.num_qubits
|
|
104
|
+
self.style = STYLES[style]
|
|
105
|
+
self.gates_per_measure = max(1, gates_per_measure)
|
|
106
|
+
self.title = title
|
|
107
|
+
self.music_prop = _music_font()
|
|
108
|
+
self.moments = circuit_to_moments(qc)
|
|
109
|
+
self.n_cols = len(self.moments)
|
|
110
|
+
|
|
111
|
+
self.xs = self._compute_column_xs()
|
|
112
|
+
self.end_x = (self.xs[-1] if self.xs else X_START) + DX * 0.6 + 0.6
|
|
113
|
+
|
|
114
|
+
# ---------- geometry helpers ----------
|
|
115
|
+
def _compute_column_xs(self):
|
|
116
|
+
xs = []
|
|
117
|
+
x = X_START
|
|
118
|
+
for c in range(self.n_cols):
|
|
119
|
+
if c > 0 and c % self.gates_per_measure == 0:
|
|
120
|
+
x += MEASURE_GAP
|
|
121
|
+
xs.append(x)
|
|
122
|
+
x += DX
|
|
123
|
+
return xs
|
|
124
|
+
|
|
125
|
+
def _staff_top_y(self, qubit: int) -> float:
|
|
126
|
+
# qubit 0 = top staff
|
|
127
|
+
return -qubit * (STAFF_HEIGHT + STAFF_GAP)
|
|
128
|
+
|
|
129
|
+
def _mid_y(self, qubit: int) -> float:
|
|
130
|
+
return self._staff_top_y(qubit) - (N_LINES // 2) * LINE_SPACING
|
|
131
|
+
|
|
132
|
+
def _y_of(self, qubit: int, offset: float = 0.0) -> float:
|
|
133
|
+
return self._mid_y(qubit) + offset * LINE_SPACING
|
|
134
|
+
|
|
135
|
+
def _measure_barline_xs(self):
|
|
136
|
+
bl = []
|
|
137
|
+
for c in range(self.gates_per_measure, self.n_cols, self.gates_per_measure):
|
|
138
|
+
bl.append((self.xs[c - 1] + self.xs[c]) / 2.0)
|
|
139
|
+
return bl
|
|
140
|
+
|
|
141
|
+
# ---------- drawing primitives ----------
|
|
142
|
+
def _text(self, ax, x, y, s, size=10.5, weight="normal", style="normal",
|
|
143
|
+
color=None, ha="center", va="center", zorder=10):
|
|
144
|
+
ax.text(x, y, s, fontsize=size, fontweight=weight, fontstyle=style,
|
|
145
|
+
color=color or self.style["ink"], ha=ha, va=va,
|
|
146
|
+
family="DejaVu Serif", zorder=zorder)
|
|
147
|
+
|
|
148
|
+
def _music_glyph(self, ax, x, y, key, size=34, color=None, va="center", zorder=8):
|
|
149
|
+
c = color or self.style["ink"]
|
|
150
|
+
if self.music_prop is not None:
|
|
151
|
+
ax.text(x, y, GLYPH[key], fontsize=size, fontproperties=self.music_prop,
|
|
152
|
+
color=c, ha="center", va=va, zorder=zorder)
|
|
153
|
+
elif key == "gclef":
|
|
154
|
+
# Text fallback: draw a stylised "G" clef substitute
|
|
155
|
+
ax.text(x, y, "\U0001D11E", fontsize=size, color=c,
|
|
156
|
+
ha="center", va=va, zorder=zorder,
|
|
157
|
+
family="serif")
|
|
158
|
+
|
|
159
|
+
def _notehead(self, ax, x, y, filled=True, size=None, color=None, stem=True):
|
|
160
|
+
"""
|
|
161
|
+
Draw a proper notehead with an optional stem, like a real quarter
|
|
162
|
+
note (filled) or half note (hollow).
|
|
163
|
+
"""
|
|
164
|
+
c = color or self.style["ink"]
|
|
165
|
+
# Notehead ellipse — tilted like a real music note
|
|
166
|
+
w = LINE_SPACING * 1.15
|
|
167
|
+
h = LINE_SPACING * 0.80
|
|
168
|
+
face = c if filled else "none"
|
|
169
|
+
lw_head = 1.8 if filled else 1.6
|
|
170
|
+
ax.add_patch(Ellipse((x, y), width=w, height=h, angle=-16,
|
|
171
|
+
facecolor=face, edgecolor=c, lw=lw_head, zorder=9))
|
|
172
|
+
# Stem — straight vertical line going up from the right of the notehead
|
|
173
|
+
if stem:
|
|
174
|
+
stem_x = x + w * 0.42
|
|
175
|
+
stem_bottom = y
|
|
176
|
+
stem_top = y + STEM_LEN * LINE_SPACING
|
|
177
|
+
ax.plot([stem_x, stem_x], [stem_bottom, stem_top],
|
|
178
|
+
color=c, lw=1.6, solid_capstyle="round", zorder=9)
|
|
179
|
+
|
|
180
|
+
def _x_notehead(self, ax, x, y, size=None, color=None, r=None, lw=2.0):
|
|
181
|
+
c = color or self.style["ink"]
|
|
182
|
+
if r is None:
|
|
183
|
+
r = 0.22 * LINE_SPACING * 2
|
|
184
|
+
ax.plot([x - r, x + r], [y - r, y + r], color=c, lw=lw, zorder=9)
|
|
185
|
+
ax.plot([x - r, x + r], [y + r, y - r], color=c, lw=lw, zorder=9)
|
|
186
|
+
|
|
187
|
+
def _target_symbol(self, ax, x, y, r=None, color=None, lw=2.0):
|
|
188
|
+
c = color or self.style["ink"]
|
|
189
|
+
if r is None:
|
|
190
|
+
r = 0.28 * LINE_SPACING * 2
|
|
191
|
+
ax.add_patch(Circle((x, y), r, fill=False, edgecolor=c, lw=lw, zorder=9))
|
|
192
|
+
ax.plot([x - r, x + r], [y, y], color=c, lw=lw, zorder=9)
|
|
193
|
+
ax.plot([x, x], [y - r, y + r], color=c, lw=lw, zorder=9)
|
|
194
|
+
|
|
195
|
+
def _control_dot(self, ax, x, y, color=None, r=None):
|
|
196
|
+
c = color or self.style["ink"]
|
|
197
|
+
if r is None:
|
|
198
|
+
r = 0.13 * LINE_SPACING * 2
|
|
199
|
+
ax.add_patch(Circle((x, y), r, fill=True, facecolor=c, edgecolor=c, zorder=9))
|
|
200
|
+
|
|
201
|
+
# ---------- staves ----------
|
|
202
|
+
def _draw_staff_lines(self, ax, qubit):
|
|
203
|
+
top = self._staff_top_y(qubit)
|
|
204
|
+
for j in range(N_LINES):
|
|
205
|
+
y = top - j * LINE_SPACING
|
|
206
|
+
ax.plot([BRACE_X, self.end_x], [y, y], color=self.style["ink"],
|
|
207
|
+
lw=0.8, alpha=0.9, zorder=1)
|
|
208
|
+
|
|
209
|
+
def _draw_clef_and_label(self, ax, qubit):
|
|
210
|
+
mid = self._mid_y(qubit)
|
|
211
|
+
# Larger treble clef — should visually fill the staff
|
|
212
|
+
clef_size = max(44, int(80 * LINE_SPACING))
|
|
213
|
+
self._music_glyph(ax, CLEF_X, mid + 0.10 * LINE_SPACING, "gclef",
|
|
214
|
+
size=clef_size, va="center")
|
|
215
|
+
# Large qubit label styled like a time signature (e.g. "q" over "0")
|
|
216
|
+
self._text(ax, LEFT_LABEL_X, mid + 0.6 * LINE_SPACING,
|
|
217
|
+
"q", size=20, weight="bold", ha="right", va="center")
|
|
218
|
+
self._text(ax, LEFT_LABEL_X, mid - 0.6 * LINE_SPACING,
|
|
219
|
+
f"{qubit}", size=20, weight="bold", ha="right", va="center")
|
|
220
|
+
|
|
221
|
+
def _draw_initial_state(self, ax, qubit):
|
|
222
|
+
x = X_START - 1.05
|
|
223
|
+
y = self._mid_y(qubit)
|
|
224
|
+
self._notehead(ax, x, y, filled=False, color=self.style["accent"], stem=False)
|
|
225
|
+
self._text(ax, x, y - 1.1 * LINE_SPACING, "|0\u27e9", size=8,
|
|
226
|
+
style="italic", color=self.style["accent"])
|
|
227
|
+
|
|
228
|
+
def _draw_barlines(self, ax, y_top, y_bottom):
|
|
229
|
+
for bx in self._measure_barline_xs():
|
|
230
|
+
ax.plot([bx, bx], [y_top, y_bottom], color=self.style["ink"], lw=1.2, zorder=2)
|
|
231
|
+
# Initial barline after clef/init area
|
|
232
|
+
init_bar_x = (X_START - 1.05 + self.xs[0]) / 2.0 - 0.10 if self.xs else X_START
|
|
233
|
+
ax.plot([init_bar_x, init_bar_x], [y_top, y_bottom],
|
|
234
|
+
color=self.style["ink"], lw=1.0, zorder=2)
|
|
235
|
+
# Final double/thick barline
|
|
236
|
+
fx = self.end_x - 0.30
|
|
237
|
+
ax.plot([fx - 0.12, fx - 0.12], [y_top, y_bottom],
|
|
238
|
+
color=self.style["ink"], lw=1.0, zorder=2)
|
|
239
|
+
ax.plot([fx, fx], [y_top, y_bottom],
|
|
240
|
+
color=self.style["ink"], lw=2.8, zorder=2)
|
|
241
|
+
|
|
242
|
+
# ---------- gate rendering ----------
|
|
243
|
+
def _draw_event(self, ax, ev: GateEvent):
|
|
244
|
+
x = self.xs[ev.column]
|
|
245
|
+
ink = self.style["ink"]
|
|
246
|
+
|
|
247
|
+
if ev.kind == "barrier":
|
|
248
|
+
y0 = self._y_of(min(ev.qubits), -2.2)
|
|
249
|
+
y1 = self._y_of(max(ev.qubits), 2.2)
|
|
250
|
+
ax.plot([x, x], [y1, y0], color=ink, lw=1.2,
|
|
251
|
+
ls=(0, (4, 3)), alpha=0.55, zorder=6)
|
|
252
|
+
self._text(ax, x, y1 + 0.6 * LINE_SPACING, "barrier",
|
|
253
|
+
size=7, style="italic", color=ink)
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
if ev.kind == "measure":
|
|
257
|
+
q = ev.targets[0]
|
|
258
|
+
y = self._y_of(q, 0.0)
|
|
259
|
+
self._x_notehead(ax, x, y)
|
|
260
|
+
self._text(ax, x, y - 1.2 * LINE_SPACING, "M",
|
|
261
|
+
size=9, weight="bold", style="italic")
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
if ev.kind == "single":
|
|
265
|
+
q = ev.targets[0]
|
|
266
|
+
off = _pitch_offset(ev)
|
|
267
|
+
y = self._y_of(q, off)
|
|
268
|
+
filled = ev.name != "h"
|
|
269
|
+
self._notehead(ax, x, y, filled=filled, stem=True)
|
|
270
|
+
label_y = y - 1.1 * LINE_SPACING if off >= 0 else y + 1.4 * LINE_SPACING
|
|
271
|
+
self._text(ax, x, label_y, ev.label,
|
|
272
|
+
size=8, weight="bold", style="italic")
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
if ev.kind == "swap":
|
|
276
|
+
q0, q1 = ev.targets[0], ev.targets[1]
|
|
277
|
+
y0, y1 = self._y_of(q0, 0.0), self._y_of(q1, 0.0)
|
|
278
|
+
ax.plot([x, x], [y0, y1], color=ink, lw=1.5, zorder=6)
|
|
279
|
+
r_swap = 0.25 * LINE_SPACING * 2
|
|
280
|
+
self._x_notehead(ax, x, y0, r=r_swap, lw=2.0)
|
|
281
|
+
self._x_notehead(ax, x, y1, r=r_swap, lw=2.0)
|
|
282
|
+
mid_y = (y0 + y1) / 2.0
|
|
283
|
+
self._text(ax, x + 0.6, mid_y, "SWAP",
|
|
284
|
+
size=8.5, weight="bold", style="italic", ha="left")
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
if ev.kind == "control":
|
|
288
|
+
all_q = sorted(ev.qubits)
|
|
289
|
+
y_top = self._y_of(all_q[0], 0.0)
|
|
290
|
+
y_bot = self._y_of(all_q[-1], 0.0)
|
|
291
|
+
ax.plot([x, x], [y_top, y_bot], color=ink, lw=1.5, zorder=6)
|
|
292
|
+
|
|
293
|
+
for q in ev.controls:
|
|
294
|
+
self._control_dot(ax, x, self._y_of(q, 0.0))
|
|
295
|
+
|
|
296
|
+
for q in ev.targets:
|
|
297
|
+
yt = self._y_of(q, 0.0)
|
|
298
|
+
if ev.name == "cx":
|
|
299
|
+
self._target_symbol(ax, x, yt)
|
|
300
|
+
elif ev.name == "cz":
|
|
301
|
+
self._control_dot(ax, x, yt)
|
|
302
|
+
else:
|
|
303
|
+
self._notehead(ax, x, yt, filled=True, stem=False)
|
|
304
|
+
|
|
305
|
+
label_y = (y_top + y_bot) / 2.0
|
|
306
|
+
self._text(ax, x + 0.6, label_y, ev.label,
|
|
307
|
+
size=8.5, weight="bold", style="italic", ha="left")
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
# generic fallback: treat like a "control" block with plain noteheads
|
|
311
|
+
all_q = sorted(ev.qubits)
|
|
312
|
+
y_top = self._y_of(all_q[0], 0.0)
|
|
313
|
+
y_bot = self._y_of(all_q[-1], 0.0)
|
|
314
|
+
if len(all_q) > 1:
|
|
315
|
+
ax.plot([x, x], [y_top, y_bot], color=ink, lw=1.5, zorder=6)
|
|
316
|
+
for q in all_q:
|
|
317
|
+
self._notehead(ax, x, self._y_of(q, 0.0), filled=True, stem=False)
|
|
318
|
+
self._text(ax, x + 0.6, (y_top + y_bot) / 2.0, ev.label,
|
|
319
|
+
size=8.5, weight="bold", style="italic", ha="left")
|
|
320
|
+
|
|
321
|
+
# ---------- public entry point ----------
|
|
322
|
+
def draw(self, figsize=None, dpi=170):
|
|
323
|
+
n = self.n_qubits
|
|
324
|
+
top_y = self._staff_top_y(0) + 1.6
|
|
325
|
+
bottom_y = self._staff_top_y(n - 1) - STAFF_HEIGHT - 1.0
|
|
326
|
+
width = self.end_x + 1.0
|
|
327
|
+
height = top_y - bottom_y
|
|
328
|
+
|
|
329
|
+
if figsize is None:
|
|
330
|
+
figsize = (max(10.0, width * 0.72), max(3.0, height * 0.72))
|
|
331
|
+
|
|
332
|
+
fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
|
|
333
|
+
fig.patch.set_facecolor(self.style["bg"])
|
|
334
|
+
ax.set_facecolor(self.style["bg"])
|
|
335
|
+
|
|
336
|
+
for q in range(n):
|
|
337
|
+
self._draw_staff_lines(ax, q)
|
|
338
|
+
self._draw_clef_and_label(ax, q)
|
|
339
|
+
self._draw_initial_state(ax, q)
|
|
340
|
+
|
|
341
|
+
draw_vertical_brace(ax, BRACE_X, top_y - 0.9, bottom_y + 0.9,
|
|
342
|
+
color=self.style["ink"], lw=2.0)
|
|
343
|
+
|
|
344
|
+
self._draw_barlines(ax, top_y - 0.9, bottom_y + 0.9)
|
|
345
|
+
|
|
346
|
+
for moment in self.moments:
|
|
347
|
+
for ev in moment:
|
|
348
|
+
self._draw_event(ax, ev)
|
|
349
|
+
|
|
350
|
+
if self.title:
|
|
351
|
+
self._text(ax, width / 2.0, top_y + 0.6, self.title, size=15,
|
|
352
|
+
weight="bold", ha="center", va="bottom")
|
|
353
|
+
|
|
354
|
+
ax.set_xlim(-0.3, width)
|
|
355
|
+
ax.set_ylim(bottom_y, top_y + (1.2 if self.title else 0.3))
|
|
356
|
+
ax.set_aspect("equal")
|
|
357
|
+
ax.axis("off")
|
|
358
|
+
fig.tight_layout(pad=0.5)
|
|
359
|
+
return fig
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def draw_circuit(qc, filename=None, style="clean", gates_per_measure=3,
|
|
363
|
+
title=None, dpi=200):
|
|
364
|
+
"""
|
|
365
|
+
Renders a qiskit QuantumCircuit as a musical-staff circuit diagram.
|
|
366
|
+
|
|
367
|
+
Parameters
|
|
368
|
+
----------
|
|
369
|
+
qc : qiskit.QuantumCircuit
|
|
370
|
+
filename : str, optional -- if given, saves the figure (png/svg/pdf by extension)
|
|
371
|
+
style : "ink" (aged paper + green ink) or
|
|
372
|
+
"clean" (white background, black ink, good for printing) ← default
|
|
373
|
+
gates_per_measure : how many circuit "moments" to group between barlines
|
|
374
|
+
title : optional title text drawn above the score
|
|
375
|
+
|
|
376
|
+
Returns
|
|
377
|
+
-------
|
|
378
|
+
matplotlib.figure.Figure
|
|
379
|
+
"""
|
|
380
|
+
drawer = StaffCircuitDrawer(qc, style=style, gates_per_measure=gates_per_measure, title=title)
|
|
381
|
+
fig = drawer.draw(dpi=dpi)
|
|
382
|
+
if filename:
|
|
383
|
+
fig.savefig(filename, facecolor=fig.get_facecolor(), bbox_inches="tight")
|
|
384
|
+
return fig
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: QuantumSheets
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Musical staff notation for quantum circuits
|
|
5
|
+
Author: Ben Bar
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: qiskit
|
|
9
|
+
Requires-Dist: matplotlib
|
|
10
|
+
|
|
11
|
+
# QuantumSheets
|
|
12
|
+
|
|
13
|
+
Renders Qiskit QuantumCircuits as sheet-music-style diagrams where each qubit is a five-line musical staff.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
You can install the package directly from this repository:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install .
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python -m QuantumSheets my_circuit.qasm -o output.png
|
|
27
|
+
python -m QuantumSheets my_circuit.py -o output.png --style clean
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or using the CLI directly:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
quantumsheets my_circuit.py -o output.png
|
|
34
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
QuantumSheets/__init__.py
|
|
4
|
+
QuantumSheets/__main__.py
|
|
5
|
+
QuantumSheets/brace.py
|
|
6
|
+
QuantumSheets/cli.py
|
|
7
|
+
QuantumSheets/layout.py
|
|
8
|
+
QuantumSheets/render.py
|
|
9
|
+
QuantumSheets.egg-info/PKG-INFO
|
|
10
|
+
QuantumSheets.egg-info/SOURCES.txt
|
|
11
|
+
QuantumSheets.egg-info/dependency_links.txt
|
|
12
|
+
QuantumSheets.egg-info/entry_points.txt
|
|
13
|
+
QuantumSheets.egg-info/requires.txt
|
|
14
|
+
QuantumSheets.egg-info/top_level.txt
|
|
15
|
+
tests/test_quantumsheets.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
QuantumSheets
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# QuantumSheets
|
|
2
|
+
|
|
3
|
+
Renders Qiskit QuantumCircuits as sheet-music-style diagrams where each qubit is a five-line musical staff.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
You can install the package directly from this repository:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
python -m QuantumSheets my_circuit.qasm -o output.png
|
|
17
|
+
python -m QuantumSheets my_circuit.py -o output.png --style clean
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or using the CLI directly:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
quantumsheets my_circuit.py -o output.png
|
|
24
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "QuantumSheets"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Musical staff notation for quantum circuits"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Ben Bar" }
|
|
11
|
+
]
|
|
12
|
+
dependencies = [
|
|
13
|
+
"qiskit",
|
|
14
|
+
"matplotlib"
|
|
15
|
+
]
|
|
16
|
+
readme = "README.md"
|
|
17
|
+
requires-python = ">=3.8"
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
quantumsheets = "QuantumSheets.cli:main"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
test_quantumsheets.py
|
|
4
|
+
--------------------
|
|
5
|
+
Comprehensive test: builds a circuit with every gate type the QuantumSheets
|
|
6
|
+
package is designed to handle, renders it, and saves a PNG.
|
|
7
|
+
"""
|
|
8
|
+
import sys, os
|
|
9
|
+
|
|
10
|
+
# Allow importing QuantumSheets as a package from the parent directory
|
|
11
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
12
|
+
|
|
13
|
+
from qiskit import QuantumCircuit
|
|
14
|
+
from QuantumSheets.render import draw_circuit
|
|
15
|
+
|
|
16
|
+
# ---------- build a circuit with every gate type ----------
|
|
17
|
+
qc = QuantumCircuit(4, 2)
|
|
18
|
+
|
|
19
|
+
# Single-qubit gates
|
|
20
|
+
qc.h(0)
|
|
21
|
+
qc.x(1)
|
|
22
|
+
qc.z(2)
|
|
23
|
+
|
|
24
|
+
# Parametric gate
|
|
25
|
+
qc.rz(1.5708, 3) # ≈ π/2
|
|
26
|
+
|
|
27
|
+
# Barrier
|
|
28
|
+
qc.barrier()
|
|
29
|
+
|
|
30
|
+
# CNOT (cx) — control on q0, target on q1
|
|
31
|
+
qc.cx(0, 1)
|
|
32
|
+
|
|
33
|
+
# CZ — control on q2, target on q3
|
|
34
|
+
qc.cz(2, 3)
|
|
35
|
+
|
|
36
|
+
# Toffoli — controls on q0,q1, target on q2
|
|
37
|
+
qc.ccx(0, 1, 2)
|
|
38
|
+
|
|
39
|
+
# SWAP — swap q1 and q3
|
|
40
|
+
qc.swap(1, 3)
|
|
41
|
+
|
|
42
|
+
# Another barrier
|
|
43
|
+
qc.barrier()
|
|
44
|
+
|
|
45
|
+
# More single-qubit for variety
|
|
46
|
+
qc.h(2)
|
|
47
|
+
qc.x(0)
|
|
48
|
+
|
|
49
|
+
# Measurements
|
|
50
|
+
qc.measure(0, 0)
|
|
51
|
+
qc.measure(1, 1)
|
|
52
|
+
|
|
53
|
+
print(f"Circuit: {qc.num_qubits} qubits, {qc.num_clbits} classical bits")
|
|
54
|
+
print(f"Operations: {len(qc.data)}")
|
|
55
|
+
print()
|
|
56
|
+
print(qc.draw(output="text"))
|
|
57
|
+
print()
|
|
58
|
+
|
|
59
|
+
# ---------- render ----------
|
|
60
|
+
out_dir = os.path.dirname(os.path.abspath(__file__))
|
|
61
|
+
fname = os.path.join(out_dir, "test_clean.png")
|
|
62
|
+
|
|
63
|
+
fig = draw_circuit(
|
|
64
|
+
qc,
|
|
65
|
+
filename=fname,
|
|
66
|
+
style="clean",
|
|
67
|
+
title="Quantum Circuit — Staff Notation",
|
|
68
|
+
dpi=200,
|
|
69
|
+
gates_per_measure=3,
|
|
70
|
+
)
|
|
71
|
+
import matplotlib.pyplot as plt
|
|
72
|
+
plt.close(fig)
|
|
73
|
+
print(f"✓ Saved {fname}")
|
|
74
|
+
print("\nDone.")
|