mibo-client 0.1.0__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.
Files changed (44) hide show
  1. mb_client/__init__.py +45 -0
  2. mb_client/circuit/__init__.py +28 -0
  3. mb_client/circuit/circuit.py +482 -0
  4. mb_client/circuit/gates.py +147 -0
  5. mb_client/circuit/operations.py +63 -0
  6. mb_client/client.py +210 -0
  7. mb_client/common/__init__.py +17 -0
  8. mb_client/common/parameters.py +182 -0
  9. mb_client/compilation/__init__.py +24 -0
  10. mb_client/compilation/basis_translation.py +333 -0
  11. mb_client/compilation/calibration.py +110 -0
  12. mb_client/compilation/pulse_lowering.py +494 -0
  13. mb_client/compilation/schedule_map.py +282 -0
  14. mb_client/compilation/target.py +203 -0
  15. mb_client/conf/__init__.py +19 -0
  16. mb_client/conf/settings.py +24 -0
  17. mb_client/exceptions.py +23 -0
  18. mb_client/execution/__init__.py +27 -0
  19. mb_client/execution/backends.py +58 -0
  20. mb_client/execution/jobs.py +61 -0
  21. mb_client/execution/options.py +67 -0
  22. mb_client/execution/readout.py +64 -0
  23. mb_client/execution/remote.py +123 -0
  24. mb_client/execution/results.py +74 -0
  25. mb_client/execution/sweep.py +373 -0
  26. mb_client/protocol.py +297 -0
  27. mb_client/pulse/__init__.py +32 -0
  28. mb_client/pulse/block.py +101 -0
  29. mb_client/pulse/channels.py +59 -0
  30. mb_client/pulse/envelopes.py +459 -0
  31. mb_client/pulse/schedule.py +259 -0
  32. mb_client/pulse/sequence.py +406 -0
  33. mb_client/utils/__init__.py +19 -0
  34. mb_client/utils/formatting.py +54 -0
  35. mb_client/visualization/__init__.py +15 -0
  36. mb_client/visualization/circuit/__init__.py +36 -0
  37. mb_client/visualization/circuit/drawer.py +420 -0
  38. mb_client/visualization/circuit/matplotlib.py +232 -0
  39. mb_client/visualization/circuit/styles/default.json +87 -0
  40. mb_client/visualization/pulse.py +402 -0
  41. mibo_client-0.1.0.dist-info/METADATA +90 -0
  42. mibo_client-0.1.0.dist-info/RECORD +44 -0
  43. mibo_client-0.1.0.dist-info/WHEEL +4 -0
  44. mibo_client-0.1.0.dist-info/licenses/LICENSE +201 -0
mb_client/__init__.py ADDED
@@ -0,0 +1,45 @@
1
+ # Copyright 2026 mibosuili
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
+ # https://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
+ """Public API for the MB Cloud client SDK."""
16
+
17
+ from .client import MBCloudClient
18
+ from .exceptions import ClientError, ServerConnectionError
19
+ from .execution import MeasurementLevel
20
+ from .execution import (ExecutionResult, Job, JobCancelledError,
21
+ JobExecutionError, JobStatus, ProgramResult,
22
+ RemoteBackend, RemoteJob, RunOptions)
23
+ from .circuit import QuantumCircuit
24
+ from .protocol import decode_circuit, encode_circuit, decode_result, encode_result
25
+
26
+ __all__ = [
27
+ "ClientError",
28
+ "decode_circuit",
29
+ "decode_result",
30
+ "encode_result",
31
+ "encode_circuit",
32
+ "ExecutionResult",
33
+ "Job",
34
+ "JobCancelledError",
35
+ "JobExecutionError",
36
+ "JobStatus",
37
+ "MBCloudClient",
38
+ "MeasurementLevel",
39
+ "ProgramResult",
40
+ "QuantumCircuit",
41
+ "RemoteBackend",
42
+ "RemoteJob",
43
+ "RunOptions",
44
+ "ServerConnectionError",
45
+ ]
@@ -0,0 +1,28 @@
1
+ # Copyright 2026 mibosuili
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
+ # https://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 .circuit import (CircuitInstruction, Clbit, Gate, Instruction,
16
+ QuantumCircuit, Qubit)
17
+ from .gates import (IGate, XGate, YGate, SYGate, SXGate, SXdgGate, RXGate,
18
+ RYGate, RXYGate, RZGate, HGate, CXGate, CZGate,
19
+ UnitaryGate, VirtualZ)
20
+ from .operations import (Barrier, Delay, Measure, Reset, Drive, Idle)
21
+
22
+ __all__ = [
23
+ 'CircuitInstruction', 'Clbit', 'Gate', 'Instruction', 'QuantumCircuit',
24
+ 'Qubit', 'IGate', 'XGate', 'YGate', 'SYGate', 'SXGate', 'SXdgGate',
25
+ 'RXGate', 'RYGate', 'RXYGate', 'RZGate', 'HGate', 'CXGate', 'CZGate',
26
+ 'UnitaryGate', 'VirtualZ', 'Barrier', 'Delay', 'Measure',
27
+ 'Reset', 'Drive', 'Idle'
28
+ ]
@@ -0,0 +1,482 @@
1
+ # Copyright 2026 mibosuili
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
+ # https://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
+ """MB Cloud's lightweight circuit intermediate representation.
16
+
17
+ The public shape is intentionally familiar to users of circuit SDKs, while the
18
+ implementation is focused on cloud transport and hardware compilation needs.
19
+ """
20
+
21
+ from __future__ import annotations
22
+ import copy
23
+ import math
24
+ from numbers import Real
25
+ from dataclasses import dataclass
26
+ from typing import Iterable, Mapping, Sequence
27
+ from mb_client.common.parameters import Parameter, ParameterExpression, bind_value, parameters_in
28
+
29
+
30
+ @dataclass(frozen=True, order=True)
31
+ class Qubit:
32
+ index: int
33
+
34
+
35
+ @dataclass(frozen=True, order=True)
36
+ class Clbit:
37
+ index: int
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class CircuitInstruction:
42
+ operation: "Instruction"
43
+ qubits: tuple[Qubit, ...]
44
+ clbits: tuple[Clbit, ...] = ()
45
+
46
+
47
+ class Instruction:
48
+ """A circuit operation with fixed quantum/classical arity."""
49
+
50
+ def __init__(self,
51
+ name: str,
52
+ num_qubits: int,
53
+ num_clbits: int = 0,
54
+ params: Sequence[object] = ()):
55
+ if not name or num_qubits < 0 or num_clbits < 0:
56
+ raise ValueError("Instruction name and bit counts must be valid")
57
+ self.name = name
58
+ self.num_qubits = num_qubits
59
+ self.num_clbits = num_clbits
60
+ self.params = list(params)
61
+ self._definition: QuantumCircuit | None = None
62
+
63
+ @property
64
+ def definition(self) -> "QuantumCircuit | None":
65
+ return self._definition
66
+
67
+ @definition.setter
68
+ def definition(self, circuit: "QuantumCircuit | None") -> None:
69
+ if circuit is not None and circuit.num_qubits != self.num_qubits:
70
+ raise ValueError(
71
+ "Instruction definition has the wrong number of qubits")
72
+ self._definition = circuit
73
+
74
+ def copy(self) -> "Instruction":
75
+ return copy.deepcopy(self)
76
+
77
+
78
+ class Gate(Instruction):
79
+ """An instruction without classical outputs."""
80
+
81
+ def __init__(self,
82
+ name: str,
83
+ num_qubits: int,
84
+ params: Sequence[object] = ()):
85
+ super().__init__(name, num_qubits, 0, params)
86
+
87
+
88
+ class QuantumCircuit:
89
+ """Ordered logical operations over indexed quantum and classical bits."""
90
+
91
+ def __init__(self,
92
+ num_qubits: int,
93
+ num_clbits: int = 0,
94
+ name: str | None = None,
95
+ *,
96
+ bias: Mapping[int, object] | None = None):
97
+ if num_qubits < 0 or num_clbits < 0:
98
+ raise ValueError("Circuit bit counts cannot be negative")
99
+ self.name = name or "circuit"
100
+ self.metadata: dict[str, object] = {}
101
+ self.physical_qubits = tuple(range(num_qubits))
102
+ self.qubits = tuple(Qubit(index) for index in range(num_qubits))
103
+ self.clbits = tuple(Clbit(index) for index in range(num_clbits))
104
+ self.data: list[CircuitInstruction] = []
105
+ self._bias = self._validate_bias(bias or {})
106
+
107
+ def _validate_bias(self, bias):
108
+ result = {}
109
+ for qubit, value in bias.items():
110
+ if type(qubit) is not int or not 0 <= qubit < self.num_qubits:
111
+ raise ValueError(f"invalid logical bias qubit {qubit!r}")
112
+ if not isinstance(value, (Parameter, ParameterExpression)):
113
+ if isinstance(value, bool) or not isinstance(value, Real):
114
+ raise TypeError(
115
+ "bias must be a finite real number or parameter")
116
+ if not math.isfinite(value):
117
+ raise ValueError("bias must be a finite real number")
118
+ value = float(value)
119
+ result[qubit] = copy.deepcopy(value)
120
+ return result
121
+
122
+ @property
123
+ def bias(self) -> dict[int, object]:
124
+ """A copy of whole-execution bias settings keyed by logical qubit.
125
+
126
+ Unspecified qubits use backend defaults. Bias has no timeline position.
127
+ """
128
+ return copy.deepcopy(self._bias)
129
+
130
+ def with_bias(self, bias: Mapping[int, object]) -> "QuantumCircuit":
131
+ """Return a copy with supplied settings replacing matching logical entries."""
132
+ overrides = self._validate_bias(bias)
133
+ target = copy.deepcopy(self)
134
+ target._bias.update(overrides)
135
+ return target
136
+
137
+ @property
138
+ def num_qubits(self) -> int:
139
+ return len(self.qubits)
140
+
141
+ @property
142
+ def num_clbits(self) -> int:
143
+ return len(self.clbits)
144
+
145
+ @property
146
+ def parameters(self):
147
+ found = set(parameters_in(self._bias))
148
+ for item in self.data:
149
+ found.update(parameters_in(item.operation.params))
150
+ found.update(
151
+ parameters_in(getattr(item.operation, "calibration", {})))
152
+ found.update(
153
+ parameters_in(getattr(item.operation, "parameters", {})))
154
+ return frozenset(found)
155
+
156
+ def bind_parameters(self,
157
+ values,
158
+ *,
159
+ per_qubit_values=None,
160
+ inplace: bool = False) -> "QuantumCircuit":
161
+ per_qubit_values = per_qubit_values or {}
162
+ unknown = (set(values).union(per_qubit_values).difference(
163
+ self.parameters).difference(parameter.name
164
+ for parameter in self.parameters))
165
+ if unknown:
166
+ raise ValueError(
167
+ f"Unknown circuit parameters: {sorted(map(str, unknown))}")
168
+ target = self if inplace else copy.deepcopy(self)
169
+ bound_bias = {}
170
+ for qubit, value in target._bias.items():
171
+ local_values = dict(values)
172
+ for parameter in parameters_in(value):
173
+ if parameter.name in values and parameter not in local_values:
174
+ if sum(p.name == parameter.name
175
+ for p in self.parameters) != 1:
176
+ raise ValueError(
177
+ f"Ambiguous parameter name {parameter.name!r}; use Parameter keys"
178
+ )
179
+ local_values[parameter] = values[parameter.name]
180
+ qubit_values = per_qubit_values.get(
181
+ parameter, per_qubit_values.get(parameter.name))
182
+ if qubit_values is not None:
183
+ physical = target.physical_qubits[qubit]
184
+ if physical not in qubit_values:
185
+ raise ValueError(
186
+ f"missing value for parameter {parameter.name!r} on physical qubit {physical}"
187
+ )
188
+ local_values[parameter] = qubit_values[physical]
189
+ bound_bias[qubit] = bind_value(value, local_values)
190
+ target._bias = target._validate_bias(bound_bias)
191
+ for item in target.data:
192
+ operation = item.operation
193
+ item_parameters = set(parameters_in(operation.params))
194
+ for attribute in ("calibration", "parameters"):
195
+ item_parameters.update(
196
+ parameters_in(getattr(operation, attribute, {})))
197
+ local_values = dict(values)
198
+ # Circuit binding is explicit user input, unlike pulse operation binding.
199
+ for parameter in item_parameters:
200
+ if parameter.name in values and parameter not in local_values:
201
+ same_name = {
202
+ p
203
+ for p in self.parameters if p.name == parameter.name
204
+ }
205
+ if len(same_name) != 1:
206
+ raise ValueError(
207
+ f"Ambiguous parameter name {parameter.name!r}; use Parameter keys"
208
+ )
209
+ local_values[parameter] = values[parameter.name]
210
+ for parameter, qubit_values in per_qubit_values.items():
211
+ if parameter not in item_parameters:
212
+ continue
213
+ if len(item.qubits) != 1:
214
+ raise ValueError(
215
+ f"per-qubit parameter {parameter.name!r} is used by "
216
+ f"multi-qubit instruction {operation.name!r}")
217
+ physical = target.physical_qubits[item.qubits[0].index]
218
+ if physical not in qubit_values:
219
+ raise ValueError(
220
+ f"missing value for parameter {parameter.name!r} on "
221
+ f"physical qubit {physical}")
222
+ local_values[parameter] = qubit_values[physical]
223
+ operation.params = bind_value(operation.params, local_values)
224
+ for attribute in ("calibration", "parameters"):
225
+ if hasattr(operation, attribute):
226
+ setattr(
227
+ operation, attribute,
228
+ bind_value(getattr(operation, attribute),
229
+ local_values))
230
+ return target
231
+
232
+ def append(
233
+ self,
234
+ operation: Instruction,
235
+ qubits: Sequence[int | Qubit],
236
+ clbits: Sequence[int | Clbit] = ()
237
+ ) -> CircuitInstruction:
238
+ if not isinstance(operation, Instruction):
239
+ raise TypeError("operation must be an MB Cloud Instruction")
240
+ qargs = tuple(self._resolve_qubit(bit) for bit in qubits)
241
+ cargs = tuple(self._resolve_clbit(bit) for bit in clbits)
242
+ if len(qargs) != operation.num_qubits or len(
243
+ cargs) != operation.num_clbits:
244
+ raise ValueError(
245
+ f"{operation.name} expects ({operation.num_qubits}, "
246
+ f"{operation.num_clbits}) bits, got ({len(qargs)}, {len(cargs)})"
247
+ )
248
+ if len(set(qargs)) != len(qargs):
249
+ raise ValueError("An instruction cannot use the same qubit twice")
250
+ item = CircuitInstruction(operation.copy(), qargs, cargs)
251
+ self.data.append(item)
252
+ return item
253
+
254
+ def compose(self,
255
+ other: "QuantumCircuit",
256
+ qubits: Sequence[int | Qubit] | None = None,
257
+ inplace: bool = False) -> "QuantumCircuit":
258
+ target = self if inplace else copy.deepcopy(self)
259
+ mapped = tuple(target.qubits if qubits is None else (
260
+ target._resolve_qubit(bit) for bit in qubits))
261
+ if len(mapped) != other.num_qubits:
262
+ raise ValueError("compose qubit mapping has the wrong size")
263
+ if len(set(mapped)) != len(mapped):
264
+ raise ValueError("compose qubit mapping contains duplicates")
265
+ merged_bias = target.bias
266
+ for qubit, value in other.bias.items():
267
+ mapped_qubit = mapped[qubit].index
268
+ if mapped_qubit in merged_bias and merged_bias[
269
+ mapped_qubit] != value:
270
+ raise ValueError(
271
+ f"conflicting static biases for logical qubit {mapped_qubit}"
272
+ )
273
+ merged_bias[mapped_qubit] = value
274
+ for item in other.data:
275
+ target.append(item.operation,
276
+ [mapped[bit.index] for bit in item.qubits],
277
+ [bit.index for bit in item.clbits])
278
+ target._bias = merged_bias
279
+ return target
280
+
281
+ def to_gate(self, name: str | None = None) -> Gate:
282
+ if self._bias:
283
+ raise ValueError("A circuit with static bias cannot become a Gate")
284
+ if self.num_clbits:
285
+ raise ValueError(
286
+ "A circuit containing classical bits cannot become a Gate")
287
+ gate = Gate(name or self.name, self.num_qubits)
288
+ gate.definition = copy.deepcopy(self)
289
+ return gate
290
+
291
+ def barrier(self, *qubits: int | Qubit) -> CircuitInstruction:
292
+ from mb_client.circuit.operations import Barrier
293
+ qargs = qubits or self.qubits
294
+ return self.append(Barrier(len(qargs)), qargs)
295
+
296
+ def measure(self, qubit: int | Qubit | Sequence[int | Qubit],
297
+ clbit: int | Clbit | Sequence[int | Clbit], **options):
298
+ """Measure a scalar pair or equally sized lists of quantum/classical bits.
299
+
300
+ List calls return a tuple and add no implicit alignment barrier.
301
+ All operands are validated before any instructions are appended.
302
+ """
303
+ from mb_client.circuit.operations import Measure
304
+ multiple = isinstance(qubit, (list, tuple))
305
+ if multiple != isinstance(clbit, (list, tuple)):
306
+ raise TypeError("measure requires two scalars or two lists/tuples")
307
+ qubits = qubit if multiple else (qubit, )
308
+ clbits = clbit if multiple else (clbit, )
309
+ if len(qubits) != len(clbits):
310
+ raise ValueError(
311
+ "measurement qubits and clbits must have equal lengths")
312
+ qargs = tuple(self._resolve_qubit(q) for q in qubits)
313
+ cargs = tuple(self._resolve_clbit(c) for c in clbits)
314
+ if len(set(qargs)) != len(qargs) or len(set(cargs)) != len(cargs):
315
+ raise ValueError(
316
+ "measurement operands must not contain duplicates")
317
+ operation = Measure(**options)
318
+ items = tuple(
319
+ self.append(operation, [q], [c]) for q, c in zip(qargs, cargs))
320
+ return items if multiple else items[0]
321
+
322
+ def measure_all(self, **options) -> tuple[CircuitInstruction, ...]:
323
+ """Measure logical qubit i into clbit i, adding missing classical bits.
324
+
325
+ Existing bits and instructions are retained. No barrier is inserted.
326
+ """
327
+ if self.num_clbits < self.num_qubits:
328
+ self.clbits += tuple(
329
+ Clbit(i) for i in range(self.num_clbits, self.num_qubits))
330
+ return self.measure(list(self.qubits),
331
+ list(self.clbits[:self.num_qubits]), **options)
332
+
333
+ def delay(self,
334
+ duration: float,
335
+ qubit: int | Qubit,
336
+ channel: str = "drive") -> CircuitInstruction:
337
+ from mb_client.circuit.operations import Delay
338
+ return self.append(Delay(duration, channel), [qubit])
339
+
340
+ def pulse_block(self,
341
+ sequence,
342
+ qubits,
343
+ *,
344
+ name: str | None = None) -> CircuitInstruction:
345
+ """Insert a copy of an explicitly engineered PulseSequence(template=True).
346
+
347
+ Channel index i addresses qubits[i]. The compiler ordering policy controls
348
+ synchronization with surrounding instructions. Delays and barriers inside
349
+ the sequence control internal timing.
350
+ """
351
+ from mb_client.pulse.block import PulseBlock
352
+ targets = (qubits, ) if isinstance(qubits,
353
+ (int, Qubit)) else tuple(qubits)
354
+ return self.append(PulseBlock(sequence, len(targets), name=name),
355
+ targets)
356
+
357
+ def x(self, qubit, **calibration):
358
+ from mb_client.circuit.gates import XGate
359
+ return self.append(XGate(**calibration), [qubit])
360
+
361
+ def y(self, qubit, **calibration):
362
+ from mb_client.circuit.gates import YGate
363
+ return self.append(YGate(**calibration), [qubit])
364
+
365
+ def id(self, duration: float, qubit):
366
+ from mb_client.circuit.gates import IGate
367
+ return self.append(IGate(duration), [qubit])
368
+
369
+ def sx(self, qubit, **calibration):
370
+ from mb_client.circuit.gates import SXGate
371
+ return self.append(SXGate(**calibration), [qubit])
372
+
373
+ def sy(self, qubit, **calibration):
374
+ from mb_client.circuit.gates import SYGate
375
+ return self.append(SYGate(**calibration), [qubit])
376
+
377
+ def unitary(self, matrix, qubits, *, name: str = "unitary"):
378
+ from mb_client.circuit.gates import UnitaryGate
379
+ if isinstance(qubits, (int, Qubit)):
380
+ qubits = [qubits]
381
+ return self.append(UnitaryGate(matrix, name=name), qubits)
382
+
383
+ def rz(self, phi, qubit):
384
+ from mb_client.circuit.gates import RZGate
385
+ return self.append(RZGate(phi), [qubit])
386
+
387
+ def rx(self, theta, qubit):
388
+ from mb_client.circuit.gates import RXGate
389
+ return self.append(RXGate(theta), [qubit])
390
+
391
+ def ry(self, theta, qubit):
392
+ from mb_client.circuit.gates import RYGate
393
+ return self.append(RYGate(theta), [qubit])
394
+
395
+ def rxy(self, theta, phi, qubit):
396
+ from mb_client.circuit.gates import RXYGate
397
+ return self.append(RXYGate(theta, phi), [qubit])
398
+
399
+ def h(self, qubit):
400
+ from mb_client.circuit.gates import HGate
401
+ return self.append(HGate(), [qubit])
402
+
403
+ def cx(self, control, target):
404
+ from mb_client.circuit.gates import CXGate
405
+ return self.append(CXGate(), [control, target])
406
+
407
+ def cz(self, control, target):
408
+ from mb_client.circuit.gates import CZGate
409
+ return self.append(CZGate(), [control, target])
410
+
411
+ def reset(self, *qubits, align: bool | None = None, **calibration):
412
+ """Reset scalar, positional, or list operands; omit operands for all qubits.
413
+
414
+ An aligned reset brackets the individual reset instructions with
415
+ barriers. The leading barrier makes every selected qubit start its
416
+ reset at the same time; the trailing barrier prevents subsequent
417
+ instructions from starting until every reset has finished.
418
+ """
419
+ from mb_client.circuit.operations import Reset
420
+ if len(qubits) == 1 and isinstance(qubits[0], (list, tuple)):
421
+ qubits = tuple(qubits[0])
422
+ targets = tuple(
423
+ self._resolve_qubit(q) for q in (qubits or tuple(self.qubits)))
424
+ if len(set(targets)) != len(targets):
425
+ raise ValueError("reset operands must not contain duplicates")
426
+ if align is None:
427
+ align = len(targets) > 1
428
+ if align:
429
+ self.barrier(*targets)
430
+ resets = tuple(
431
+ self.append(Reset(**calibration), [qubit]) for qubit in targets)
432
+ if align:
433
+ self.barrier(*targets)
434
+ return resets[0] if len(resets) == 1 else resets
435
+
436
+ def _resolve_qubit(self, bit: int | Qubit) -> Qubit:
437
+ resolved = self.qubits[bit] if isinstance(bit, int) else bit
438
+ if resolved not in self.qubits:
439
+ raise ValueError(f"Qubit {bit!r} is not in this circuit")
440
+ return resolved
441
+
442
+ def _resolve_clbit(self, bit: int | Clbit) -> Clbit:
443
+ resolved = self.clbits[bit] if isinstance(bit, int) else bit
444
+ if resolved not in self.clbits:
445
+ raise ValueError(f"Clbit {bit!r} is not in this circuit")
446
+ return resolved
447
+
448
+ def __iter__(self) -> Iterable[CircuitInstruction]:
449
+ return iter(self.data)
450
+
451
+ def __len__(self) -> int:
452
+ return len(self.data)
453
+
454
+ def draw(self, format: str = "text", style=None, *, show: bool = False):
455
+ """Draw the circuit as text or a Matplotlib figure.
456
+
457
+ Set ``show=True`` to emit the drawing immediately, which is useful for
458
+ calls made inside a notebook loop. The default keeps rendering free of
459
+ display side effects, so assigned figures can be saved or composed.
460
+ """
461
+ from mb_client.visualization.circuit import draw_circuit
462
+ drawing = draw_circuit(self, format=format, style=style)
463
+ if show:
464
+ self._display_drawing(drawing)
465
+ return drawing
466
+
467
+ @staticmethod
468
+ def _display_drawing(drawing) -> None:
469
+ """Display a drawing in IPython, with a terminal-safe fallback."""
470
+ try:
471
+ from IPython.display import display
472
+ except ImportError:
473
+ if isinstance(drawing, str):
474
+ print(drawing)
475
+ else:
476
+ import matplotlib.pyplot as plt
477
+ plt.show()
478
+ else:
479
+ display(drawing)
480
+
481
+ def __str__(self) -> str:
482
+ return self.draw(format="text")