qilisdk 0.1.4__py3-none-any.whl → 0.1.5__py3-none-any.whl
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.
- qilisdk/__init__.py +11 -2
- qilisdk/__init__.pyi +2 -3
- qilisdk/_logging.py +135 -0
- qilisdk/_optionals.py +5 -7
- qilisdk/analog/__init__.py +3 -18
- qilisdk/analog/exceptions.py +2 -4
- qilisdk/analog/hamiltonian.py +455 -110
- qilisdk/analog/linear_schedule.py +118 -0
- qilisdk/analog/schedule.py +272 -79
- qilisdk/backends/__init__.py +45 -0
- qilisdk/{digital/digital_algorithm.py → backends/__init__.pyi} +3 -5
- qilisdk/backends/backend.py +117 -0
- qilisdk/{extras/cuda → backends}/cuda_backend.py +152 -159
- qilisdk/backends/qutip_backend.py +492 -0
- qilisdk/common/__init__.py +48 -2
- qilisdk/common/algorithm.py +2 -1
- qilisdk/{extras/qaas/qaas_settings.py → common/exceptions.py} +12 -6
- qilisdk/common/model.py +1019 -1
- qilisdk/common/parameterizable.py +75 -0
- qilisdk/common/qtensor.py +666 -0
- qilisdk/common/result.py +2 -1
- qilisdk/common/variables.py +1931 -0
- qilisdk/{extras/cuda/cuda_analog_result.py → cost_functions/__init__.py} +3 -4
- qilisdk/cost_functions/cost_function.py +77 -0
- qilisdk/cost_functions/model_cost_function.py +145 -0
- qilisdk/cost_functions/observable_cost_function.py +109 -0
- qilisdk/digital/__init__.py +3 -22
- qilisdk/digital/ansatz.py +203 -160
- qilisdk/digital/circuit.py +81 -9
- qilisdk/digital/exceptions.py +12 -6
- qilisdk/digital/gates.py +228 -85
- qilisdk/{extras/qaas/qaas_analog_result.py → functionals/__init__.py} +14 -5
- qilisdk/functionals/functional.py +39 -0
- qilisdk/{extras/cuda/cuda_digital_result.py → functionals/functional_result.py} +3 -4
- qilisdk/functionals/sampling.py +81 -0
- qilisdk/functionals/sampling_result.py +92 -0
- qilisdk/functionals/time_evolution.py +98 -0
- qilisdk/functionals/time_evolution_result.py +84 -0
- qilisdk/functionals/variational_program.py +80 -0
- qilisdk/functionals/variational_program_result.py +69 -0
- qilisdk/logging_config.yaml +16 -0
- qilisdk/{common/backend.py → optimizers/__init__.py} +2 -1
- qilisdk/optimizers/optimizer.py +39 -0
- qilisdk/{common → optimizers}/optimizer_result.py +3 -12
- qilisdk/{common/optimizer.py → optimizers/scipy_optimizer.py} +10 -28
- qilisdk/settings.py +78 -0
- qilisdk/{extras → speqtrum}/__init__.py +7 -8
- qilisdk/{extras → speqtrum}/__init__.pyi +3 -3
- qilisdk/speqtrum/experiments/__init__.py +25 -0
- qilisdk/speqtrum/experiments/experiment_functional.py +124 -0
- qilisdk/speqtrum/experiments/experiment_result.py +231 -0
- qilisdk/{extras/qaas → speqtrum}/keyring.py +8 -4
- qilisdk/speqtrum/speqtrum.py +432 -0
- qilisdk/speqtrum/speqtrum_models.py +300 -0
- qilisdk/utils/__init__.py +0 -14
- qilisdk/utils/openqasm2.py +1 -1
- qilisdk/utils/serialization.py +1 -1
- qilisdk/utils/visualization/PlusJakartaSans-SemiBold.ttf +0 -0
- qilisdk/utils/visualization/__init__.py +24 -0
- qilisdk/utils/visualization/circuit_renderers.py +781 -0
- qilisdk/utils/visualization/schedule_renderers.py +161 -0
- qilisdk/utils/visualization/style.py +154 -0
- qilisdk/utils/visualization/themes.py +76 -0
- qilisdk/yaml.py +126 -0
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/METADATA +180 -134
- qilisdk-0.1.5.dist-info/RECORD +69 -0
- qilisdk/analog/algorithms.py +0 -111
- qilisdk/analog/analog_backend.py +0 -43
- qilisdk/analog/analog_result.py +0 -114
- qilisdk/analog/quantum_objects.py +0 -596
- qilisdk/digital/digital_backend.py +0 -90
- qilisdk/digital/digital_result.py +0 -145
- qilisdk/digital/vqe.py +0 -166
- qilisdk/extras/cuda/__init__.py +0 -13
- qilisdk/extras/qaas/__init__.py +0 -13
- qilisdk/extras/qaas/models.py +0 -132
- qilisdk/extras/qaas/qaas_backend.py +0 -255
- qilisdk/extras/qaas/qaas_digital_result.py +0 -20
- qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -20
- qilisdk/extras/qaas/qaas_vqe_result.py +0 -20
- qilisdk-0.1.4.dist-info/RECORD +0 -51
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/WHEEL +0 -0
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/licenses/LICENCE +0 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
# Copyright 2025 Qilimanjaro Quantum Tech
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from fractions import Fraction
|
|
18
|
+
from typing import TYPE_CHECKING, Final, Iterable
|
|
19
|
+
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
import numpy as np
|
|
22
|
+
from matplotlib.patches import Arc, Circle, FancyArrow, FancyBboxPatch
|
|
23
|
+
|
|
24
|
+
from qilisdk.digital.gates import Controlled, Gate, M, X
|
|
25
|
+
from qilisdk.utils.visualization.style import CircuitStyle
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from matplotlib.axes import Axes
|
|
29
|
+
|
|
30
|
+
from qilisdk.digital import Circuit
|
|
31
|
+
|
|
32
|
+
###############################################################################
|
|
33
|
+
# Matplotlib implementation
|
|
34
|
+
###############################################################################
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MatplotlibCircuitRenderer:
|
|
38
|
+
"""Render a :class:`~qilisdk.digital.Circuit` using *matplotlib*."""
|
|
39
|
+
|
|
40
|
+
# Z-order groups -------------------------------------------------------
|
|
41
|
+
_Z: Final = {
|
|
42
|
+
"wire": 1,
|
|
43
|
+
"wire_label": 1,
|
|
44
|
+
"gate": 3,
|
|
45
|
+
"node": 3,
|
|
46
|
+
"bridge": 2,
|
|
47
|
+
"connector": 4,
|
|
48
|
+
"gate_label": 4,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# ------------------------------------------------------------------
|
|
52
|
+
# Construction
|
|
53
|
+
# ------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def __init__(self, circuit: Circuit, ax: Axes | None = None, *, style: CircuitStyle = CircuitStyle()) -> None:
|
|
56
|
+
self.circuit = circuit
|
|
57
|
+
self.style = style
|
|
58
|
+
self._ax = ax or self._make_axes(style.dpi)
|
|
59
|
+
self._end_measure_qubits: set[int] = set()
|
|
60
|
+
self._wires = circuit.nqubits
|
|
61
|
+
# *layer_widths[w][l]* - width (inches) of layer *l* on wire *w*
|
|
62
|
+
self._layer_widths: dict[int, list[float]] = {w: [] for w in range(circuit.nqubits)}
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def axes(self) -> Axes:
|
|
66
|
+
return self._ax
|
|
67
|
+
|
|
68
|
+
def plot(self) -> None:
|
|
69
|
+
"""
|
|
70
|
+
Render the circuit on the current axes and show the figure.
|
|
71
|
+
|
|
72
|
+
Traverses the circuit gates once, placing and drawing each element,
|
|
73
|
+
deferring final-column measurements as needed, draws wires and finalizes
|
|
74
|
+
the figure.
|
|
75
|
+
"""
|
|
76
|
+
self._generate_layer_gate_mapping()
|
|
77
|
+
self._draw_wire_labels()
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
# 1. compute last-gate index for each qubit ------------------------
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
# done elswere
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
# 2. iterate through gates, drawing or deferring measurements -----
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
deferred_qubits: set[int] = set()
|
|
88
|
+
self._max_layer_width: list[float] = [self.style.start_pad]
|
|
89
|
+
for layer in self._layer_gate_mapping:
|
|
90
|
+
for _, gate in self._layer_gate_mapping[layer].items():
|
|
91
|
+
if isinstance(gate, M):
|
|
92
|
+
inline_qubits: list[int] = []
|
|
93
|
+
for q in gate.target_qubits:
|
|
94
|
+
if self.last_idx.get(q) == layer:
|
|
95
|
+
deferred_qubits.add(q)
|
|
96
|
+
self._end_measure_qubits.add(q)
|
|
97
|
+
else:
|
|
98
|
+
inline_qubits.append(q)
|
|
99
|
+
if inline_qubits:
|
|
100
|
+
self._draw_inline_measure(inline_qubits, layer=layer)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if isinstance(gate, Controlled):
|
|
104
|
+
self._draw_controlled_gate(gate, layer=layer)
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if gate.name == "SWAP":
|
|
108
|
+
self._draw_swap_gate(list(gate.target_qubits or []), layer=layer)
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
# Targets-only (single or multi-qubit) box
|
|
112
|
+
self._draw_targets_gate(
|
|
113
|
+
label=self._gate_label(gate), targets=list(gate.target_qubits or []), layer=layer
|
|
114
|
+
)
|
|
115
|
+
maxi = max(
|
|
116
|
+
[
|
|
117
|
+
0.0,
|
|
118
|
+
*(
|
|
119
|
+
self._layer_widths[wire][layer]
|
|
120
|
+
for wire in range(self._wires)
|
|
121
|
+
if len(self._layer_widths[wire]) - 1 >= layer
|
|
122
|
+
),
|
|
123
|
+
]
|
|
124
|
+
)
|
|
125
|
+
self._max_layer_width.append(maxi)
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
# 3. draw any deferred (final-column) measurements -----------------
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
if deferred_qubits:
|
|
131
|
+
self._draw_concurrent_measures(sorted(deferred_qubits), layer=layer)
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# final touches -----------------------------------------------------
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
self._draw_wires()
|
|
137
|
+
self._finalise_figure()
|
|
138
|
+
# plt.tight_layout()
|
|
139
|
+
plt.show()
|
|
140
|
+
|
|
141
|
+
def save(self, filename: str) -> None: # thin wrapper
|
|
142
|
+
"""Save current figure to disk.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
filename: Path to save the figure (e.g., 'circuit.png').
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
self.axes.figure.savefig(filename, bbox_inches="tight") # type: ignore[union-attr]
|
|
149
|
+
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
# Low-level drawing helpers (private)
|
|
152
|
+
# ------------------------------------------------------------------
|
|
153
|
+
def _generate_layer_gate_mapping(self) -> None:
|
|
154
|
+
self._layer_gate_mapping: dict[int, dict[int, Gate]] = {}
|
|
155
|
+
gate_maping: dict[int, list[Gate]] = {}
|
|
156
|
+
for qubit in range(self.circuit.nqubits):
|
|
157
|
+
gate_maping[qubit] = []
|
|
158
|
+
for gate in self.circuit.gates:
|
|
159
|
+
qubits = gate.qubits
|
|
160
|
+
if len(qubits) == 1:
|
|
161
|
+
gate_maping[qubits[0]].append(gate)
|
|
162
|
+
elif len(qubits) > 1:
|
|
163
|
+
if self.style.layout == "compact":
|
|
164
|
+
con_qubits = qubits
|
|
165
|
+
elif self.style.layout == "normal":
|
|
166
|
+
con_qubits = tuple(range(min(qubits), max(qubits) + 1))
|
|
167
|
+
for qubit in con_qubits:
|
|
168
|
+
gate_maping[qubit].append(gate)
|
|
169
|
+
|
|
170
|
+
layer: int = 0
|
|
171
|
+
waiting_list: dict[int, Gate] = {}
|
|
172
|
+
completed = [False] * self.circuit.nqubits
|
|
173
|
+
for q, l in gate_maping.items():
|
|
174
|
+
completed[q] = not bool(l)
|
|
175
|
+
ignore_q: list[int] = []
|
|
176
|
+
self.last_idx: dict[int, int] = {}
|
|
177
|
+
while not all(completed):
|
|
178
|
+
if layer not in self._layer_gate_mapping:
|
|
179
|
+
self._layer_gate_mapping[layer] = {}
|
|
180
|
+
for q in range(self.circuit.nqubits):
|
|
181
|
+
if q in ignore_q:
|
|
182
|
+
ignore_q.remove(q)
|
|
183
|
+
continue
|
|
184
|
+
if len(gate_maping[q]) == 0 and not completed[q]:
|
|
185
|
+
completed[q] = True
|
|
186
|
+
self.last_idx[q] = layer - 1
|
|
187
|
+
if q in waiting_list or completed[q]:
|
|
188
|
+
continue
|
|
189
|
+
gate = gate_maping[q][0]
|
|
190
|
+
if gate.nqubits == 1:
|
|
191
|
+
self._layer_gate_mapping[layer][q] = gate
|
|
192
|
+
gate_maping[q].pop(0)
|
|
193
|
+
if gate.nqubits > 1:
|
|
194
|
+
waiting_list[q] = gate
|
|
195
|
+
qubits = gate.qubits
|
|
196
|
+
if self.style.layout == "compact":
|
|
197
|
+
con_qubits = qubits
|
|
198
|
+
elif self.style.layout == "normal":
|
|
199
|
+
con_qubits = tuple(range(min(qubits), max(qubits) + 1))
|
|
200
|
+
if all(key in waiting_list for key in con_qubits) and all(
|
|
201
|
+
waiting_list[qr] == gate for qr in con_qubits
|
|
202
|
+
):
|
|
203
|
+
self._layer_gate_mapping[layer][q] = gate
|
|
204
|
+
for c_qubit in con_qubits:
|
|
205
|
+
gate_maping[c_qubit].pop(0)
|
|
206
|
+
del waiting_list[c_qubit]
|
|
207
|
+
|
|
208
|
+
if self.style.layout == "compact":
|
|
209
|
+
for m_qubit in range(min(qubits), q):
|
|
210
|
+
if m_qubit not in qubits and m_qubit in self._layer_gate_mapping[layer]:
|
|
211
|
+
ignore_q.append(m_qubit)
|
|
212
|
+
if layer + 1 not in self._layer_gate_mapping:
|
|
213
|
+
self._layer_gate_mapping[layer + 1] = {}
|
|
214
|
+
self._layer_gate_mapping[layer + 1][m_qubit] = self._layer_gate_mapping[layer][
|
|
215
|
+
m_qubit
|
|
216
|
+
]
|
|
217
|
+
del self._layer_gate_mapping[layer][m_qubit]
|
|
218
|
+
ignore_q += [*(m_qubit for m_qubit in range(q + 1, max(qubits) + 1))]
|
|
219
|
+
if len(gate_maping[q]) == 0 and not completed[q]:
|
|
220
|
+
completed[q] = True
|
|
221
|
+
self.last_idx[q] = layer
|
|
222
|
+
layer += 1
|
|
223
|
+
|
|
224
|
+
def _xpos(self, layer: int) -> float:
|
|
225
|
+
"""
|
|
226
|
+
Compute the left x of a given layer.
|
|
227
|
+
|
|
228
|
+
With align_layer=True, this ignores *wires* and aligns across all wires.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
layer: Column index (0 = the initial label pad entry).
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
The x-coordinate (inches) of the left edge of this column.
|
|
235
|
+
"""
|
|
236
|
+
return sum(self._max_layer_width[: layer + 1])
|
|
237
|
+
|
|
238
|
+
def _reserve(self, width: float, wires: Iterable[int], layer: int) -> None:
|
|
239
|
+
"""
|
|
240
|
+
Reserve width in a column for a set of wires.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
width: Box width (inches), *excluding* left/right gate margins.
|
|
244
|
+
wires: Wires to update.
|
|
245
|
+
layer: Column index to reserve.
|
|
246
|
+
"""
|
|
247
|
+
full_width = width + self.style.gate_margin * 2
|
|
248
|
+
for w in wires:
|
|
249
|
+
layers = self._layer_widths[w]
|
|
250
|
+
if len(layers) > layer:
|
|
251
|
+
layers[layer] = max(layers[layer], full_width)
|
|
252
|
+
else:
|
|
253
|
+
for _ in range(len(layers), layer):
|
|
254
|
+
layers.append(0.0)
|
|
255
|
+
layers.append(full_width)
|
|
256
|
+
|
|
257
|
+
def _place(self, wires: Iterable[int], layer: int, *, min_width: float = 0.0) -> float:
|
|
258
|
+
"""
|
|
259
|
+
Choose a column and compute its left x for a set of wires.
|
|
260
|
+
|
|
261
|
+
This does *not* draw anything; it optionally reserves a minimal width
|
|
262
|
+
for the selected column to create the column on those wires.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
wires: Wires that must participate in this column.
|
|
266
|
+
min_width: Optional minimal content width to reserve (inches).
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
A tuple ``(x, layer, wires_sorted)``:
|
|
270
|
+
- x: Left x of the column (including left margin).
|
|
271
|
+
- layer: Column index.
|
|
272
|
+
- wires_sorted: Sorted unique wires used for placement.
|
|
273
|
+
"""
|
|
274
|
+
wires = [*range(min(wires), max(wires) + 1)]
|
|
275
|
+
x = self._xpos(layer) + self.style.gate_margin
|
|
276
|
+
if min_width:
|
|
277
|
+
self._reserve(min_width, wires, layer)
|
|
278
|
+
return x
|
|
279
|
+
|
|
280
|
+
def _text_width(self, text: str) -> float:
|
|
281
|
+
"""
|
|
282
|
+
Measure rendered text width in inches for current DPI/style.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
text: Text to measure (mathtext is supported).
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
The rendered width in inches.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
t = plt.Text(
|
|
292
|
+
0,
|
|
293
|
+
0,
|
|
294
|
+
text,
|
|
295
|
+
fontproperties=self.style.font,
|
|
296
|
+
)
|
|
297
|
+
self.axes.add_artist(t)
|
|
298
|
+
renderer = self.axes.figure.canvas.get_renderer() # type: ignore[attr-defined]
|
|
299
|
+
width = t.get_window_extent(renderer=renderer).width / self.style.dpi
|
|
300
|
+
t.remove()
|
|
301
|
+
return width
|
|
302
|
+
|
|
303
|
+
# Basic primitives ----------------------------------------------------
|
|
304
|
+
|
|
305
|
+
def _draw_targets_gate(
|
|
306
|
+
self,
|
|
307
|
+
*,
|
|
308
|
+
label: str,
|
|
309
|
+
targets: list[int] | None,
|
|
310
|
+
x: float | None = None,
|
|
311
|
+
layer: int | None = None,
|
|
312
|
+
color: str | None = None,
|
|
313
|
+
) -> tuple[float, int, float]:
|
|
314
|
+
"""
|
|
315
|
+
Draw a box gate that touches only *targets* (no controls).
|
|
316
|
+
|
|
317
|
+
If ``x``/``layer`` are not provided, the earliest column that all
|
|
318
|
+
targets can share is used.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
label: Text label to show inside the box.
|
|
322
|
+
targets: Target qubit indices. If None, defaults to all wires.
|
|
323
|
+
x: Optional left x of the column (from a prior `_place`).
|
|
324
|
+
layer: Optional column index (from a prior `_place`).
|
|
325
|
+
color: Optional box fill/edge color.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
A tuple ``(x, layer, width)`` where:
|
|
329
|
+
- x: Left x used for this column.
|
|
330
|
+
- layer: Column index used.
|
|
331
|
+
- width: Content width (inches) of the box.
|
|
332
|
+
"""
|
|
333
|
+
targets = list(targets or range(self._wires))
|
|
334
|
+
t_sorted = sorted(targets)
|
|
335
|
+
a, b = t_sorted[0], t_sorted[-1]
|
|
336
|
+
|
|
337
|
+
# Decide layer/x if not given (no-controls: only target wires matter)
|
|
338
|
+
if layer is None or x is None:
|
|
339
|
+
layer = max(len(self._layer_widths[w]) for w in t_sorted)
|
|
340
|
+
x = self._xpos(layer) + self.style.gate_margin
|
|
341
|
+
|
|
342
|
+
# Measure and reserve the full width at the given x/layer
|
|
343
|
+
width = max(self._text_width(label) + self.style.gate_pad * 2, self.style.min_gate_w)
|
|
344
|
+
self._reserve(width, t_sorted, layer)
|
|
345
|
+
|
|
346
|
+
# Geometry
|
|
347
|
+
y_a = self._ypos(a, n_qubits=self._wires, sep=self.style.wire_sep)
|
|
348
|
+
y_b = self._ypos(b, n_qubits=self._wires, sep=self.style.wire_sep)
|
|
349
|
+
y_bottom = min(y_a, y_b) - self.style.min_gate_h / 2
|
|
350
|
+
height = abs(y_b - y_a) + self.style.min_gate_h
|
|
351
|
+
y_center = (y_a + y_b) / 2.0
|
|
352
|
+
|
|
353
|
+
gate_color = color or self.style.theme.primary
|
|
354
|
+
|
|
355
|
+
# Box
|
|
356
|
+
self.axes.add_patch(
|
|
357
|
+
FancyBboxPatch(
|
|
358
|
+
(x, y_bottom),
|
|
359
|
+
width,
|
|
360
|
+
height,
|
|
361
|
+
boxstyle=self.style.bulge,
|
|
362
|
+
mutation_scale=0.3,
|
|
363
|
+
facecolor=gate_color,
|
|
364
|
+
edgecolor=gate_color,
|
|
365
|
+
zorder=self._Z["gate"],
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
# Label
|
|
369
|
+
self.axes.text(
|
|
370
|
+
x + width / 2,
|
|
371
|
+
y_center,
|
|
372
|
+
label,
|
|
373
|
+
ha="center",
|
|
374
|
+
va="center",
|
|
375
|
+
color=self.style.theme.on_primary,
|
|
376
|
+
fontproperties=self.style.font,
|
|
377
|
+
zorder=self._Z["gate_label"],
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Visual connectors for multi-targets
|
|
381
|
+
if len(t_sorted) > 1:
|
|
382
|
+
for t in t_sorted:
|
|
383
|
+
y_t = self._ypos(t, n_qubits=self._wires, sep=self.style.wire_sep)
|
|
384
|
+
self.axes.add_patch(
|
|
385
|
+
Circle(
|
|
386
|
+
(x + self.style.connector_r, y_t),
|
|
387
|
+
self.style.connector_r,
|
|
388
|
+
color=self.style.theme.background,
|
|
389
|
+
zorder=self._Z["connector"],
|
|
390
|
+
)
|
|
391
|
+
)
|
|
392
|
+
self.axes.add_patch(
|
|
393
|
+
Circle(
|
|
394
|
+
(x + width - self.style.connector_r, y_t),
|
|
395
|
+
self.style.connector_r,
|
|
396
|
+
color=self.style.theme.background,
|
|
397
|
+
zorder=self._Z["connector"],
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return x, layer, width
|
|
402
|
+
|
|
403
|
+
def _draw_control_dot(self, wire: int, x: float) -> None:
|
|
404
|
+
"""
|
|
405
|
+
Draw a filled control dot at the given wire/x.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
wire: Qubit index.
|
|
409
|
+
x: Column anchor x coordinate.
|
|
410
|
+
"""
|
|
411
|
+
y = self._ypos(wire, n_qubits=self._wires, sep=self.style.wire_sep)
|
|
412
|
+
self.axes.add_patch(Circle((x, y), self.style.control_r, color=self.style.theme.accent, zorder=self._Z["node"]))
|
|
413
|
+
|
|
414
|
+
def _draw_plus_sign(self, wire: int, x: float) -> None:
|
|
415
|
+
"""
|
|
416
|
+
Draw a target ⊕ marker at the given wire/x.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
wire: Qubit index.
|
|
420
|
+
x: Column anchor x coordinate.
|
|
421
|
+
"""
|
|
422
|
+
y = self._ypos(wire, n_qubits=self._wires, sep=self.style.wire_sep)
|
|
423
|
+
self.axes.add_patch(Circle((x, y), self.style.target_r, color=self.style.theme.accent, zorder=self._Z["node"]))
|
|
424
|
+
self.axes.add_line(
|
|
425
|
+
plt.Line2D(
|
|
426
|
+
(x, x),
|
|
427
|
+
(y - self.style.target_r / 2, y + self.style.target_r / 2),
|
|
428
|
+
lw=1.5,
|
|
429
|
+
color=self.style.theme.background,
|
|
430
|
+
zorder=self._Z["gate_label"],
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
self.axes.add_line(
|
|
434
|
+
plt.Line2D(
|
|
435
|
+
(x - self.style.target_r / 2, x + self.style.target_r / 2),
|
|
436
|
+
(y, y),
|
|
437
|
+
lw=1.5,
|
|
438
|
+
color=self.style.theme.background,
|
|
439
|
+
zorder=self._Z["gate_label"],
|
|
440
|
+
)
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
def _draw_bridge(self, wire_a: int, wire_b: int, x: float) -> None:
|
|
444
|
+
"""
|
|
445
|
+
Draw a vertical bridge line between two wires at x.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
wire_a: First wire.
|
|
449
|
+
wire_b: Second wire.
|
|
450
|
+
x: Column x coordinate where the bridge is drawn.
|
|
451
|
+
"""
|
|
452
|
+
y1, y2 = (
|
|
453
|
+
self._ypos(wire_a, n_qubits=self._wires, sep=self.style.wire_sep),
|
|
454
|
+
self._ypos(wire_b, n_qubits=self._wires, sep=self.style.wire_sep),
|
|
455
|
+
)
|
|
456
|
+
self.axes.add_line(plt.Line2D([x, x], [y1, y2], color=self.style.theme.accent, zorder=self._Z["bridge"]))
|
|
457
|
+
|
|
458
|
+
def _draw_swap_mark(self, wire: int, x: float) -> None:
|
|
459
|
+
"""
|
|
460
|
+
Draw one X of a SWAP marker on a given wire at x.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
wire: Qubit index.
|
|
464
|
+
x: Column anchor x coordinate.
|
|
465
|
+
"""
|
|
466
|
+
y = self._ypos(wire, n_qubits=self._wires, sep=self.style.wire_sep)
|
|
467
|
+
offset = self.style.min_gate_w / 3
|
|
468
|
+
color = self.style.theme.accent
|
|
469
|
+
for xs, ys in (
|
|
470
|
+
([x + offset, x - offset], [y + self.style.min_gate_h / 4, y - self.style.min_gate_h / 4]),
|
|
471
|
+
([x - offset, x + offset], [y + self.style.min_gate_h / 4, y - self.style.min_gate_h / 4]),
|
|
472
|
+
):
|
|
473
|
+
self.axes.add_line(plt.Line2D(xs, ys, color=color, linewidth=2, zorder=self._Z["gate"]))
|
|
474
|
+
|
|
475
|
+
def _draw_swap_gate(
|
|
476
|
+
self,
|
|
477
|
+
targets: list[int],
|
|
478
|
+
layer: int,
|
|
479
|
+
*,
|
|
480
|
+
x: float | None = None,
|
|
481
|
+
) -> float:
|
|
482
|
+
"""
|
|
483
|
+
Draw a SWAP between two target wires.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
targets: Exactly two wires to swap.
|
|
487
|
+
x: Optional left x (from `_place`).
|
|
488
|
+
layer: Optional column index (from `_place`).
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
The anchor x within the column where the swap glyph is centered.
|
|
492
|
+
"""
|
|
493
|
+
t_sorted = sorted(targets)
|
|
494
|
+
|
|
495
|
+
if x is None:
|
|
496
|
+
x = self._place(t_sorted, layer, min_width=self.style.target_r * 2)
|
|
497
|
+
else:
|
|
498
|
+
self._reserve(self.style.target_r * 2, t_sorted, layer)
|
|
499
|
+
|
|
500
|
+
x_anchor = x + self.style.gate_pad
|
|
501
|
+
|
|
502
|
+
for t in t_sorted:
|
|
503
|
+
self._draw_swap_mark(t, x_anchor)
|
|
504
|
+
# vertical bridge between the two targets
|
|
505
|
+
self._draw_bridge(t_sorted[0], t_sorted[1], x_anchor)
|
|
506
|
+
return x_anchor
|
|
507
|
+
|
|
508
|
+
def _draw_controlled_gate(self, gate: Controlled, layer: int) -> None:
|
|
509
|
+
"""
|
|
510
|
+
Draw a controlled gate (controls + targets).
|
|
511
|
+
|
|
512
|
+
Handles:
|
|
513
|
+
- MCX family as control dots + ⊕ (no box),
|
|
514
|
+
- Controlled-SWAP by reusing swap glyphs,
|
|
515
|
+
- Generic controlled gates as a box over targets with control stems.
|
|
516
|
+
|
|
517
|
+
Args:
|
|
518
|
+
gate: Controlled gate instance.
|
|
519
|
+
"""
|
|
520
|
+
targets = list(gate.target_qubits or range(self._wires))
|
|
521
|
+
controls = list(gate.control_qubits or [])
|
|
522
|
+
all_wires = sorted(set(targets + controls))
|
|
523
|
+
|
|
524
|
+
# Place a column shared by all involved wires; reserve minimal node width
|
|
525
|
+
x = self._place(all_wires, layer, min_width=self.style.target_r * 2)
|
|
526
|
+
|
|
527
|
+
# Controlled-X family (CNOT / multi-controlled X): target glyph, not a box
|
|
528
|
+
if gate.is_modified_from(X):
|
|
529
|
+
x_anchor = x + self.style.gate_pad
|
|
530
|
+
for c in controls:
|
|
531
|
+
self._draw_control_dot(c, x_anchor)
|
|
532
|
+
self._draw_bridge(c, targets[0], x_anchor)
|
|
533
|
+
self._draw_plus_sign(targets[0], x_anchor)
|
|
534
|
+
return
|
|
535
|
+
|
|
536
|
+
# Controlled SWAP (Fredkin): reuse the SWAP primitive, then add controls
|
|
537
|
+
if getattr(gate.basic_gate, "name", "") == "SWAP":
|
|
538
|
+
x_anchor = self._draw_swap_gate(targets, x=x, layer=layer)
|
|
539
|
+
for c in controls:
|
|
540
|
+
self._draw_control_dot(c, x_anchor)
|
|
541
|
+
self._draw_bridge(c, targets[0], x_anchor)
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
# Generic controlled gate: draw the target box at this same column,
|
|
545
|
+
# then widen control wires for that layer and add stems to the center.
|
|
546
|
+
label = self._gate_label(gate.basic_gate)
|
|
547
|
+
gate_color = self.style.theme.accent
|
|
548
|
+
x_box, layer_box, width = self._draw_targets_gate(
|
|
549
|
+
label=label, targets=targets, x=x, layer=layer, color=gate_color
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
# Ensure control wires also reserve the same width for this layer
|
|
553
|
+
extra_controls = [c for c in controls if c not in targets]
|
|
554
|
+
if extra_controls:
|
|
555
|
+
self._reserve(width, extra_controls, layer_box)
|
|
556
|
+
|
|
557
|
+
x_center = x_box + width / 2.0
|
|
558
|
+
for c in controls:
|
|
559
|
+
self._draw_control_dot(c, x_center)
|
|
560
|
+
self._draw_bridge(c, targets[0], x_center)
|
|
561
|
+
|
|
562
|
+
# Measurements --------------------------------------------------------
|
|
563
|
+
|
|
564
|
+
def _draw_inline_measure(self, qubits: list[int], layer: int) -> None:
|
|
565
|
+
"""
|
|
566
|
+
Draw measurement boxes interleaved with gates (same column).
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
qubits: Wires to measure in this column.
|
|
570
|
+
"""
|
|
571
|
+
layer = max(len(self._layer_widths[q]) for q in qubits)
|
|
572
|
+
x = self._xpos(layer) + self.style.gate_margin
|
|
573
|
+
self._reserve(self.style.min_gate_w, qubits, layer)
|
|
574
|
+
for q in qubits:
|
|
575
|
+
self._draw_measure_symbol(q, x)
|
|
576
|
+
|
|
577
|
+
def _draw_concurrent_measures(self, qubits: list[int], layer: int) -> None:
|
|
578
|
+
"""
|
|
579
|
+
Draw a final column of measurements (one shared column).
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
qubits: Wires to measure concurrently.
|
|
583
|
+
"""
|
|
584
|
+
layer = max(len(v) for v in self._layer_widths.values())
|
|
585
|
+
x = self._xpos(layer) + self.style.gate_margin
|
|
586
|
+
self._max_layer_width.append(self.style.min_gate_w)
|
|
587
|
+
for q in qubits:
|
|
588
|
+
self._draw_measure_symbol(q, x)
|
|
589
|
+
|
|
590
|
+
def _draw_measure_symbol(self, wire: int, x: float) -> None:
|
|
591
|
+
"""
|
|
592
|
+
Draw a measurement glyph at the given wire/x.
|
|
593
|
+
|
|
594
|
+
Args:
|
|
595
|
+
wire: Qubit index.
|
|
596
|
+
x: Shared left x for this measurement column.
|
|
597
|
+
"""
|
|
598
|
+
y = self._ypos(wire, n_qubits=self._wires, sep=self.style.wire_sep)
|
|
599
|
+
self.axes.add_patch(
|
|
600
|
+
FancyBboxPatch(
|
|
601
|
+
(x, y - self.style.min_gate_h / 2),
|
|
602
|
+
self.style.min_gate_w,
|
|
603
|
+
self.style.min_gate_h,
|
|
604
|
+
boxstyle=self.style.bulge,
|
|
605
|
+
mutation_scale=0.3,
|
|
606
|
+
facecolor=self.style.theme.background,
|
|
607
|
+
edgecolor=self.style.theme.on_background,
|
|
608
|
+
linewidth=1.25,
|
|
609
|
+
zorder=self._Z["gate"],
|
|
610
|
+
)
|
|
611
|
+
)
|
|
612
|
+
self.axes.add_patch(
|
|
613
|
+
Arc(
|
|
614
|
+
(x + self.style.min_gate_w / 2, y - self.style.min_gate_h / 2),
|
|
615
|
+
self.style.min_gate_w * 1.5,
|
|
616
|
+
self.style.min_gate_h,
|
|
617
|
+
theta1=0,
|
|
618
|
+
theta2=180,
|
|
619
|
+
linewidth=1.25,
|
|
620
|
+
color=self.style.theme.on_background,
|
|
621
|
+
zorder=self._Z["gate_label"],
|
|
622
|
+
)
|
|
623
|
+
)
|
|
624
|
+
self.axes.add_patch(
|
|
625
|
+
FancyArrow(
|
|
626
|
+
x + self.style.min_gate_w / 2,
|
|
627
|
+
y - self.style.min_gate_h / 2,
|
|
628
|
+
dx=self.style.min_gate_w * 0.7,
|
|
629
|
+
dy=self.style.min_gate_h * 0.7,
|
|
630
|
+
length_includes_head=True,
|
|
631
|
+
width=0,
|
|
632
|
+
color=self.style.theme.on_background,
|
|
633
|
+
linewidth=1.25,
|
|
634
|
+
zorder=self._Z["gate_label"],
|
|
635
|
+
)
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
# Final decoration ----------------------------------------------------
|
|
639
|
+
|
|
640
|
+
def _draw_wires(self) -> None:
|
|
641
|
+
"""
|
|
642
|
+
Draw horizontal wires up to the last occupied x (plus tail).
|
|
643
|
+
|
|
644
|
+
For wires whose last operation is a measurement, the wire stops at the
|
|
645
|
+
measurement edge with no right-hand tail.
|
|
646
|
+
"""
|
|
647
|
+
# how far the drawing for this wire actually goes
|
|
648
|
+
x_end = sum(self._max_layer_width)
|
|
649
|
+
for q in range(self._wires):
|
|
650
|
+
y = self._ypos(q, n_qubits=self._wires, sep=self.style.wire_sep)
|
|
651
|
+
# keep the tail only for wires that KEEP going after their last gate
|
|
652
|
+
self.axes.add_line(
|
|
653
|
+
plt.Line2D([0, x_end], [y, y], lw=1, color=self.style.theme.surface_muted, zorder=self._Z["wire"])
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
def _draw_wire_labels(self) -> None:
|
|
657
|
+
"""Draw wire labels to the left of the drawing."""
|
|
658
|
+
labels = self.style.wire_label or [rf"$q_{{{i}}}$" for i in range(self._wires)]
|
|
659
|
+
widths = [self._text_width(lbl) for lbl in labels]
|
|
660
|
+
self._max_label_width = max(widths)
|
|
661
|
+
|
|
662
|
+
for i, label in enumerate(labels):
|
|
663
|
+
y = self._ypos(i, n_qubits=self._wires, sep=self.style.wire_sep)
|
|
664
|
+
self.axes.text(
|
|
665
|
+
-self.style.label_pad,
|
|
666
|
+
y,
|
|
667
|
+
label,
|
|
668
|
+
ha="right",
|
|
669
|
+
va="center",
|
|
670
|
+
fontproperties=self.style.font,
|
|
671
|
+
color=self.style.theme.on_background,
|
|
672
|
+
zorder=self._Z["wire_label"],
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
def _finalise_figure(self) -> None:
|
|
676
|
+
"""Finalize axes limits, aspect, background, and title."""
|
|
677
|
+
fig = self.axes.figure
|
|
678
|
+
fig.set_facecolor(self.style.theme.background)
|
|
679
|
+
|
|
680
|
+
total_length = sum(self._max_layer_width)
|
|
681
|
+
x_end = self.style.padding + total_length
|
|
682
|
+
|
|
683
|
+
y_end = self.style.padding + (self._wires - 1) * self.style.wire_sep
|
|
684
|
+
|
|
685
|
+
self.axes.set_xlim(
|
|
686
|
+
-self.style.padding - self._max_label_width - self.style.label_pad,
|
|
687
|
+
x_end,
|
|
688
|
+
)
|
|
689
|
+
self.axes.set_ylim(-self.style.padding, y_end)
|
|
690
|
+
|
|
691
|
+
if self.style.title:
|
|
692
|
+
self.axes.set_title(
|
|
693
|
+
self.style.title,
|
|
694
|
+
pad=10,
|
|
695
|
+
color=self.style.theme.surface_muted,
|
|
696
|
+
fontdict={"fontsize": self.style.fontsize},
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# In IPython keep figure square so equal aspect ratio does not shrink
|
|
700
|
+
try:
|
|
701
|
+
get_ipython() # type: ignore
|
|
702
|
+
size = max(self.axes.get_xlim()[1] - self.axes.get_xlim()[0], y_end + self.style.padding)
|
|
703
|
+
fig.set_size_inches(size, size, forward=True) # type: ignore[union-attr]
|
|
704
|
+
except NameError:
|
|
705
|
+
fig.set_size_inches( # type: ignore[union-attr]
|
|
706
|
+
self.axes.get_xlim()[1] - self.axes.get_xlim()[0], y_end + self.style.padding, forward=True
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
self.axes.set_aspect("equal", adjustable="box")
|
|
710
|
+
self.axes.axis("off")
|
|
711
|
+
|
|
712
|
+
# ------------------------------------------------------------------
|
|
713
|
+
# Helpers - human-readable gate labels & π-fractions
|
|
714
|
+
# ------------------------------------------------------------------
|
|
715
|
+
|
|
716
|
+
@staticmethod
|
|
717
|
+
def _ypos(index: int, *, n_qubits: int, sep: float) -> float:
|
|
718
|
+
return (n_qubits - 1 - index) * sep
|
|
719
|
+
|
|
720
|
+
@staticmethod
|
|
721
|
+
def _pi_fraction(value: float, /, tol: float = 1e-2) -> str:
|
|
722
|
+
"""
|
|
723
|
+
Format a float as a π-fraction (mathtext) when close to a rational.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
value: Angle value (radians).
|
|
727
|
+
tol: Tolerance for accepting the rational approximation.
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
Mathtext string like ``"\\pi/5"`` or fallback decimal.
|
|
731
|
+
"""
|
|
732
|
+
coeff = value / np.pi
|
|
733
|
+
frac = Fraction(coeff).limit_denominator(32)
|
|
734
|
+
n, d = frac.numerator, frac.denominator
|
|
735
|
+
if abs(frac - coeff) < tol:
|
|
736
|
+
if n == 0:
|
|
737
|
+
return "0"
|
|
738
|
+
if d == 1:
|
|
739
|
+
return r"\pi" if n == 1 else rf"{n}\pi"
|
|
740
|
+
return rf"\pi/{d}" if n == 1 else rf"{n}\pi/{d}"
|
|
741
|
+
return f"{value:.2f}"
|
|
742
|
+
|
|
743
|
+
@staticmethod
|
|
744
|
+
def _with_superscript_dagger(label: str) -> str:
|
|
745
|
+
# Convert trailing dagger to math superscript, e.g. "RX†" -> r"$\mathrm{RX}^{\dagger}$"
|
|
746
|
+
if label.endswith("†"):
|
|
747
|
+
base = label[:-1]
|
|
748
|
+
return rf"$\mathrm{{{base}}}^{{\dagger}}$"
|
|
749
|
+
return label
|
|
750
|
+
|
|
751
|
+
@staticmethod
|
|
752
|
+
def _gate_label(gate: Gate) -> str:
|
|
753
|
+
"""Build a display label for a (possibly parameterized) gate.
|
|
754
|
+
|
|
755
|
+
Args:
|
|
756
|
+
gate: Gate object.
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
Label text. Parameterized gates get ``name ( $args$ )``.
|
|
760
|
+
"""
|
|
761
|
+
name = MatplotlibCircuitRenderer._with_superscript_dagger(gate.name)
|
|
762
|
+
if gate.is_parameterized and gate.get_parameter_values():
|
|
763
|
+
parameters = ", ".join(
|
|
764
|
+
MatplotlibCircuitRenderer._pi_fraction(value) for value in gate.get_parameter_values()
|
|
765
|
+
)
|
|
766
|
+
return rf"{name} (${parameters}$)"
|
|
767
|
+
return gate.name
|
|
768
|
+
|
|
769
|
+
@staticmethod
|
|
770
|
+
def _make_axes(dpi: int) -> Axes:
|
|
771
|
+
"""
|
|
772
|
+
Create a new figure and axes with the given DPI.
|
|
773
|
+
|
|
774
|
+
Args:
|
|
775
|
+
dpi (int): The DPI of the figure
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
A newly created Matplotlib Axes.
|
|
779
|
+
"""
|
|
780
|
+
_, ax = plt.subplots(dpi=dpi, constrained_layout=True)
|
|
781
|
+
return ax
|