qiskit-aer 0.17.2__cp314-cp314-win_amd64.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 (83) hide show
  1. qiskit_aer/VERSION.txt +1 -0
  2. qiskit_aer/__init__.py +89 -0
  3. qiskit_aer/aererror.py +30 -0
  4. qiskit_aer/aerprovider.py +119 -0
  5. qiskit_aer/backends/__init__.py +20 -0
  6. qiskit_aer/backends/aer_compiler.py +1085 -0
  7. qiskit_aer/backends/aer_simulator.py +1025 -0
  8. qiskit_aer/backends/aerbackend.py +679 -0
  9. qiskit_aer/backends/backend_utils.py +567 -0
  10. qiskit_aer/backends/backendconfiguration.py +395 -0
  11. qiskit_aer/backends/backendproperties.py +590 -0
  12. qiskit_aer/backends/compatibility.py +287 -0
  13. qiskit_aer/backends/controller_wrappers.cp314-win_amd64.pyd +0 -0
  14. qiskit_aer/backends/libopenblas.dll +0 -0
  15. qiskit_aer/backends/name_mapping.py +306 -0
  16. qiskit_aer/backends/qasm_simulator.py +925 -0
  17. qiskit_aer/backends/statevector_simulator.py +330 -0
  18. qiskit_aer/backends/unitary_simulator.py +316 -0
  19. qiskit_aer/jobs/__init__.py +35 -0
  20. qiskit_aer/jobs/aerjob.py +143 -0
  21. qiskit_aer/jobs/utils.py +66 -0
  22. qiskit_aer/library/__init__.py +204 -0
  23. qiskit_aer/library/control_flow_instructions/__init__.py +16 -0
  24. qiskit_aer/library/control_flow_instructions/jump.py +47 -0
  25. qiskit_aer/library/control_flow_instructions/mark.py +30 -0
  26. qiskit_aer/library/control_flow_instructions/store.py +29 -0
  27. qiskit_aer/library/default_qubits.py +44 -0
  28. qiskit_aer/library/instructions_table.csv +21 -0
  29. qiskit_aer/library/save_instructions/__init__.py +44 -0
  30. qiskit_aer/library/save_instructions/save_amplitudes.py +168 -0
  31. qiskit_aer/library/save_instructions/save_clifford.py +63 -0
  32. qiskit_aer/library/save_instructions/save_data.py +129 -0
  33. qiskit_aer/library/save_instructions/save_density_matrix.py +91 -0
  34. qiskit_aer/library/save_instructions/save_expectation_value.py +257 -0
  35. qiskit_aer/library/save_instructions/save_matrix_product_state.py +71 -0
  36. qiskit_aer/library/save_instructions/save_probabilities.py +156 -0
  37. qiskit_aer/library/save_instructions/save_stabilizer.py +70 -0
  38. qiskit_aer/library/save_instructions/save_state.py +79 -0
  39. qiskit_aer/library/save_instructions/save_statevector.py +120 -0
  40. qiskit_aer/library/save_instructions/save_superop.py +62 -0
  41. qiskit_aer/library/save_instructions/save_unitary.py +63 -0
  42. qiskit_aer/library/set_instructions/__init__.py +19 -0
  43. qiskit_aer/library/set_instructions/set_density_matrix.py +78 -0
  44. qiskit_aer/library/set_instructions/set_matrix_product_state.py +83 -0
  45. qiskit_aer/library/set_instructions/set_stabilizer.py +77 -0
  46. qiskit_aer/library/set_instructions/set_statevector.py +78 -0
  47. qiskit_aer/library/set_instructions/set_superop.py +78 -0
  48. qiskit_aer/library/set_instructions/set_unitary.py +78 -0
  49. qiskit_aer/noise/__init__.py +265 -0
  50. qiskit_aer/noise/device/__init__.py +25 -0
  51. qiskit_aer/noise/device/models.py +397 -0
  52. qiskit_aer/noise/device/parameters.py +202 -0
  53. qiskit_aer/noise/errors/__init__.py +30 -0
  54. qiskit_aer/noise/errors/base_quantum_error.py +119 -0
  55. qiskit_aer/noise/errors/pauli_error.py +283 -0
  56. qiskit_aer/noise/errors/pauli_lindblad_error.py +363 -0
  57. qiskit_aer/noise/errors/quantum_error.py +451 -0
  58. qiskit_aer/noise/errors/readout_error.py +355 -0
  59. qiskit_aer/noise/errors/standard_errors.py +498 -0
  60. qiskit_aer/noise/noise_model.py +1231 -0
  61. qiskit_aer/noise/noiseerror.py +30 -0
  62. qiskit_aer/noise/passes/__init__.py +18 -0
  63. qiskit_aer/noise/passes/local_noise_pass.py +160 -0
  64. qiskit_aer/noise/passes/relaxation_noise_pass.py +137 -0
  65. qiskit_aer/primitives/__init__.py +44 -0
  66. qiskit_aer/primitives/estimator.py +751 -0
  67. qiskit_aer/primitives/estimator_v2.py +159 -0
  68. qiskit_aer/primitives/sampler.py +361 -0
  69. qiskit_aer/primitives/sampler_v2.py +256 -0
  70. qiskit_aer/quantum_info/__init__.py +32 -0
  71. qiskit_aer/quantum_info/states/__init__.py +16 -0
  72. qiskit_aer/quantum_info/states/aer_densitymatrix.py +313 -0
  73. qiskit_aer/quantum_info/states/aer_state.py +525 -0
  74. qiskit_aer/quantum_info/states/aer_statevector.py +302 -0
  75. qiskit_aer/utils/__init__.py +44 -0
  76. qiskit_aer/utils/noise_model_inserter.py +66 -0
  77. qiskit_aer/utils/noise_transformation.py +431 -0
  78. qiskit_aer/version.py +86 -0
  79. qiskit_aer-0.17.2.dist-info/METADATA +209 -0
  80. qiskit_aer-0.17.2.dist-info/RECORD +83 -0
  81. qiskit_aer-0.17.2.dist-info/WHEEL +5 -0
  82. qiskit_aer-0.17.2.dist-info/licenses/LICENSE.txt +203 -0
  83. qiskit_aer-0.17.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,451 @@
1
+ # This code is part of Qiskit.
2
+ #
3
+ # (C) Copyright IBM 2018-2023.
4
+ #
5
+ # This code is licensed under the Apache License, Version 2.0. You may
6
+ # obtain a copy of this license in the LICENSE.txt file in the root directory
7
+ # of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8
+ #
9
+ # Any modifications or derivative works of this code must retain this
10
+ # copyright notice, and modified files need to carry a notice indicating
11
+ # that they have been altered from the originals.
12
+ """
13
+ Quantum error class for Aer noise model
14
+ """
15
+ import numbers
16
+ from typing import Iterable
17
+ import numpy as np
18
+
19
+ from qiskit.circuit import QuantumCircuit, Instruction, Reset
20
+ from qiskit.circuit.exceptions import CircuitError
21
+ from qiskit.circuit.library.generalized_gates import PauliGate, UnitaryGate
22
+ from qiskit.circuit.library.standard_gates import IGate, XGate, YGate, ZGate
23
+ from qiskit.exceptions import QiskitError
24
+ from qiskit.quantum_info import Kraus, SuperOp, Clifford, Pauli
25
+ from qiskit.quantum_info.operators.base_operator import BaseOperator
26
+ from qiskit.quantum_info.operators.channel.quantum_channel import QuantumChannel
27
+ from qiskit.quantum_info.operators.mixins import TolerancesMixin
28
+ from qiskit.quantum_info.operators.predicates import is_identity_matrix
29
+
30
+ from .base_quantum_error import BaseQuantumError
31
+ from ..noiseerror import NoiseError
32
+
33
+
34
+ class QuantumError(BaseQuantumError, TolerancesMixin):
35
+ """
36
+ Quantum error class for Aer noise model
37
+
38
+ .. warning::
39
+ The init interface for this class is not finalized and may
40
+ change in future releases. For maximum backwards compatibility
41
+ use the QuantumError generating functions in the `noise.errors`
42
+ module.
43
+ """
44
+
45
+ def __init__(self, noise_ops):
46
+ """
47
+ Create a quantum error for a noise model.
48
+
49
+ Noise ops may either be specified as a
50
+ :obj:`~qiskit.quantum_info.operators.channel.quantum_channel.QuantumChannel`
51
+ for a general CPTP map, or as a list of ``(circuit, p)`` pairs
52
+ where ``circuit`` is a circuit-like object for the noise, and
53
+ ``p`` is the probability of the noise event. Any type of input
54
+ will be converted to the probabilistic mixture of circuit format.
55
+
56
+ **Example**
57
+
58
+ An example noise_ops for a bit-flip error with error probability
59
+ ``p = 0.1`` is:
60
+
61
+ .. code-block:: python
62
+
63
+ noise_ops = [(IGate(), 0.9),
64
+ (XGate(), 0.1)]
65
+
66
+ or specifying explicit qubit arguments,
67
+
68
+ .. code-block:: python
69
+
70
+ noise_ops = [((IGate(), [0]), 0.9),
71
+ ((XGate(), [0]), 0.1)]
72
+
73
+ The same error represented as a Kraus channel can be input as:
74
+
75
+ .. code-block:: python
76
+
77
+ noise_ops = Kraus([np.sqrt(0.9) * np.array([[1, 0], [0, 1]]),
78
+ np.sqrt(0.1) * np.array([[0, 1], [1, 0]])])
79
+
80
+ Args:
81
+ noise_ops (QuantumChannel or Iterable): Either a quantum channel or a list of
82
+ ``(circuit, p)`` pairs, which represents a quantum error, where
83
+ ``circuit`` is a circuit-like object for the noise, and
84
+ ``p`` is the probability of the noise event. Circuit-like types include
85
+ ``QuantumCircuit``, ``(Instruction, qargs)`` and a list of ``(Instruction, qargs)``.
86
+ Note that ``qargs`` should be a list of integers and can be omitted
87
+ (default qubits are used in that case). See also examples above.
88
+ Raises:
89
+ NoiseError: If input noise_ops is invalid, e.g. it's not a CPTP map.
90
+ """
91
+ # Shallow copy constructor
92
+ if isinstance(noise_ops, QuantumError):
93
+ self._circs = noise_ops.circuits
94
+ self._probs = noise_ops.probabilities
95
+ super().__init__(num_qubits=noise_ops.num_qubits)
96
+ return
97
+
98
+ # Single circuit case
99
+ if not isinstance(noise_ops, Iterable) or (
100
+ isinstance(noise_ops, tuple) and isinstance(noise_ops[0], Instruction)
101
+ ):
102
+ noise_ops = [(noise_ops, 1.0)]
103
+
104
+ # Convert zipped object to list (to enable multiple iteration over it)
105
+ if not isinstance(noise_ops, list):
106
+ noise_ops = list(noise_ops)
107
+
108
+ # Input checks
109
+ for pair in noise_ops:
110
+ if not isinstance(pair, tuple) or len(pair) != 2:
111
+ raise NoiseError(f"Invalid type of input is found around '{pair}'")
112
+ _, p = pair # pylint: disable=invalid-name
113
+ if not isinstance(p, numbers.Real):
114
+ raise NoiseError(f"Invalid type of probability: {p}")
115
+ if p < -1 * QuantumError.atol:
116
+ raise NoiseError(f"Negative probability is invalid: {p}")
117
+
118
+ # Remove zero probability circuits
119
+ noise_ops = [(op, prob) for op, prob in noise_ops if prob > 0]
120
+
121
+ if len(noise_ops) == 0:
122
+ raise NoiseError(
123
+ "noise_ops must contain at least one operator with non-zero probability"
124
+ )
125
+
126
+ ops, probs = zip(*noise_ops) # unzip
127
+
128
+ # Initialize internal variables with error checking
129
+ total_probs = sum(probs)
130
+ if not np.isclose(total_probs - 1, 0, atol=QuantumError.atol):
131
+ raise NoiseError(f"Probabilities are not normalized: {total_probs} != 1")
132
+ # Rescale probabilities if their sum is ok to avoid accumulation of rounding errors
133
+ self._probs = list(np.array(probs) / total_probs)
134
+
135
+ # Convert instructions to circuits
136
+ circs = [self._to_circuit(op) for op in ops]
137
+
138
+ num_qubits = max(qc.num_qubits for qc in circs)
139
+ self._circs = [self._enlarge_qreg(qc, num_qubits) for qc in circs]
140
+
141
+ # Check validity of circuits
142
+ for circ in self._circs:
143
+ if circ.clbits:
144
+ raise NoiseError("Circuit with classical register cannot be a channel")
145
+ if circ.num_qubits != num_qubits:
146
+ raise NoiseError("Number of qubits used in noise ops must be the same")
147
+
148
+ super().__init__(num_qubits=num_qubits)
149
+
150
+ # pylint: disable=too-many-return-statements
151
+ @classmethod
152
+ def _to_circuit(cls, op):
153
+ if isinstance(op, QuantumCircuit):
154
+ return op
155
+ if isinstance(op, tuple):
156
+ inst, qubits = op
157
+ circ = QuantumCircuit(max(qubits) + 1)
158
+ circ.append(inst, qargs=qubits)
159
+ return circ
160
+ if isinstance(op, Instruction):
161
+ if op.num_clbits > 0:
162
+ raise NoiseError(
163
+ f"Unable to convert instruction with clbits: {op.__class__.__name__}"
164
+ )
165
+ circ = QuantumCircuit(op.num_qubits)
166
+ circ.append(op, qargs=list(range(op.num_qubits)))
167
+ return circ
168
+ if isinstance(op, QuantumChannel):
169
+ if not op.is_cptp(atol=cls.atol):
170
+ raise NoiseError("Input quantum channel is not CPTP.")
171
+ try:
172
+ return cls._to_circuit(Kraus(op).to_instruction())
173
+ except QiskitError as err:
174
+ raise NoiseError(
175
+ f"Fail to convert {op.__class__.__name__} to Instruction."
176
+ ) from err
177
+ if isinstance(op, BaseOperator):
178
+ if hasattr(op, "to_instruction"):
179
+ try:
180
+ return cls._to_circuit(op.to_instruction())
181
+ except QiskitError as err:
182
+ raise NoiseError(
183
+ f"Fail to convert {op.__class__.__name__} to Instruction."
184
+ ) from err
185
+ else:
186
+ raise NoiseError(
187
+ f"Unacceptable Operator, not implementing to_instruction: "
188
+ f"{op.__class__.__name__}"
189
+ )
190
+ if isinstance(op, list):
191
+ if all(isinstance(aop, tuple) for aop in op):
192
+ num_qubits = max(max(qubits) for _, qubits in op) + 1
193
+ circ = QuantumCircuit(num_qubits)
194
+ for inst, qubits in op:
195
+ try:
196
+ circ.append(inst, qargs=qubits)
197
+ except CircuitError as err:
198
+ raise NoiseError(
199
+ f"Invalid operation type: {inst.__class__.__name__},"
200
+ f" not appendable to circuit."
201
+ ) from err
202
+ return circ
203
+ else:
204
+ raise NoiseError(f"Invalid type of op list: {op}")
205
+
206
+ raise NoiseError(f"Invalid noise op type {op.__class__.__name__}: {op}")
207
+
208
+ def __repr__(self):
209
+ """Display QuantumError."""
210
+ return (
211
+ f"<{self._repr_name()}, num_qubits={self.num_qubits},"
212
+ f" size={self.size}, probabilities={self.probabilities}>"
213
+ )
214
+
215
+ def __str__(self):
216
+ """Print error information."""
217
+ output = f"QuantumError on {self.num_qubits} qubits. Noise circuits:"
218
+ for j, pair in enumerate(zip(self.probabilities, self.circuits)):
219
+ output += f"\n P({j}) = {pair[0]}, Circuit = \n{pair[1]}"
220
+ return output
221
+
222
+ def __eq__(self, other):
223
+ """Test if two QuantumErrors are equal as SuperOps"""
224
+ if not isinstance(other, BaseQuantumError):
225
+ return False
226
+ return self.to_quantumchannel() == other.to_quantumchannel()
227
+
228
+ @property
229
+ def size(self):
230
+ """Return the number of error circuit."""
231
+ return len(self.circuits)
232
+
233
+ @property
234
+ def circuits(self):
235
+ """Return the list of error circuits."""
236
+ return self._circs
237
+
238
+ @property
239
+ def probabilities(self):
240
+ """Return the list of error probabilities."""
241
+ return self._probs
242
+
243
+ def ideal(self):
244
+ """Return True if this error object is composed only of identity operations.
245
+ Note that the identity check is best effort and up to global phase."""
246
+ for circ in self.circuits:
247
+ try:
248
+ # Circuit-level identity check for clifford Circuits
249
+ clifford = Clifford(circ)
250
+ if clifford != Clifford(np.eye(2 * circ.num_qubits, dtype=bool)):
251
+ return False
252
+ except QiskitError:
253
+ pass
254
+
255
+ # Component-wise check for non-Clifford circuits
256
+ for instruction in circ:
257
+ op = instruction.operation
258
+ if isinstance(op, IGate):
259
+ continue
260
+ if isinstance(op, PauliGate):
261
+ if op.params[0].replace("I", ""):
262
+ return False
263
+ else:
264
+ # Convert to Kraus and check if identity
265
+ kmats = Kraus(op).data
266
+ if len(kmats) > 1:
267
+ return False
268
+ if not is_identity_matrix(
269
+ kmats[0], ignore_phase=True, atol=self.atol, rtol=self.rtol
270
+ ):
271
+ return False
272
+ return True
273
+
274
+ def to_quantumchannel(self):
275
+ """Convert the QuantumError to a SuperOp quantum channel.
276
+ Required to enable SuperOp(QuantumError)."""
277
+ # Initialize as an empty superoperator of the correct size
278
+ dim = 2**self.num_qubits
279
+ ret = SuperOp(np.zeros([dim * dim, dim * dim]))
280
+ for circ, prob in zip(self.circuits, self.probabilities):
281
+ component = prob * SuperOp(circ)
282
+ ret = ret + component
283
+ return ret
284
+
285
+ def error_term(self, position):
286
+ """
287
+ Return a single term from the error.
288
+
289
+ Args:
290
+ position (int): the position of the error term.
291
+
292
+ Returns:
293
+ tuple: A pair `(circuit, p)` for error term at `position` < size
294
+ where `p` is the probability of the error term, and `circuit`
295
+ is the list of qobj instructions for the error term.
296
+
297
+ Raises:
298
+ NoiseError: If the position is greater than the size of
299
+ the quantum error.
300
+ """
301
+ if position < self.size:
302
+ return self.circuits[position], self.probabilities[position]
303
+ else:
304
+ raise NoiseError(
305
+ f"Position {position} is greater than the number of error outcomes {self.size}"
306
+ )
307
+
308
+ def to_dict(self):
309
+ """Return the current error as a dictionary."""
310
+ # Assemble noise circuits
311
+ instructions = []
312
+ for circ in self._circs:
313
+ circ_inst = []
314
+ for inst in circ.data:
315
+ inst_dict = {}
316
+ inst_dict["name"] = inst.operation.name
317
+ inst_dict["qubits"] = [circ.find_bit(q).index for q in inst.qubits]
318
+ if inst.operation.params:
319
+ inst_dict["params"] = inst.operation.params
320
+ if inst.operation.label:
321
+ inst_dict["label"] = inst.operation.label
322
+ condition = getattr(inst.operation, "condition", None)
323
+ if condition:
324
+ inst_dict["condition"] = condition
325
+ circ_inst.append(inst_dict)
326
+ instructions.append(circ_inst)
327
+ # Construct error dict
328
+ error = {
329
+ "type": "qerror",
330
+ "id": self.id,
331
+ "operations": [],
332
+ "instructions": instructions,
333
+ "probabilities": list(self.probabilities),
334
+ }
335
+ return error
336
+
337
+ @staticmethod
338
+ def from_dict(error):
339
+ """Implement current error from a dictionary."""
340
+ # check if dictionary
341
+ if not isinstance(error, dict):
342
+ raise NoiseError("error is not a dictionary")
343
+ # check expected keys "type, id, operations, instructions, probabilities"
344
+ if (
345
+ ("type" not in error)
346
+ or ("id" not in error)
347
+ or ("operations" not in error)
348
+ or ("instructions" not in error)
349
+ or ("probabilities" not in error)
350
+ ):
351
+ raise NoiseError("error dictionary not containing expected keys")
352
+ error_instructions = error["instructions"]
353
+ error_probabilities = error["probabilities"]
354
+
355
+ if len(error_instructions) != len(error_probabilities):
356
+ raise NoiseError("probabilities not matching with instructions")
357
+ # parse instructions and turn to noise_ops
358
+ noise_ops = []
359
+ for idx, inst in enumerate(error_instructions):
360
+ noise_elem = []
361
+ for elem in inst:
362
+ inst_name = elem["name"]
363
+ inst_qubits = elem["qubits"]
364
+ if inst_name == "x":
365
+ noise_elem.append((XGate(), inst_qubits))
366
+ elif inst_name == "id":
367
+ noise_elem.append((IGate(), inst_qubits))
368
+ elif inst_name == "y":
369
+ noise_elem.append((YGate(), inst_qubits))
370
+ elif inst_name == "z":
371
+ noise_elem.append((ZGate(), inst_qubits))
372
+ elif inst_name == "reset":
373
+ noise_elem.append((Reset(), inst_qubits))
374
+ elif inst_name == "measure":
375
+ raise NoiseError("instruction 'measure' not supported")
376
+ elif inst_name == "pauli":
377
+ if "params" not in elem:
378
+ raise NoiseError("pauli does not have a parameter value")
379
+ noise_elem.append((Pauli(elem["params"][0]), inst_qubits))
380
+ elif inst_name == "kraus":
381
+ if "params" not in elem:
382
+ raise NoiseError("kraus does not have a parameter value")
383
+ noise_elem.append((Kraus(elem["params"]), inst_qubits))
384
+ elif inst_name == "unitary":
385
+ if "params" not in elem:
386
+ raise NoiseError("unitary does not have a parameter value")
387
+ noise_elem.append((UnitaryGate(elem["params"][0]), inst_qubits))
388
+ else:
389
+ raise NoiseError("error gate for instruction not recognized")
390
+
391
+ noise_ops.append((noise_elem, error_probabilities[idx]))
392
+
393
+ error_obj = QuantumError(noise_ops)
394
+
395
+ return error_obj
396
+
397
+ def compose(self, other, qargs=None, front=False):
398
+ if not isinstance(other, QuantumError):
399
+ other = QuantumError(other)
400
+ if qargs is not None:
401
+ if self.num_qubits < other.num_qubits:
402
+ raise QiskitError(
403
+ "Number of qubits of this error must be less than"
404
+ " that of the error to be composed if using 'qargs' argument."
405
+ )
406
+ if len(qargs) != other.num_qubits:
407
+ raise QiskitError(
408
+ "Number of items in 'qargs' argument must be the same as"
409
+ " number of qubits of the error to be composed."
410
+ )
411
+ if front:
412
+ raise QiskitError(
413
+ "QuantumError.compose does not support 'qargs' when 'front=True'."
414
+ )
415
+
416
+ circs = [
417
+ self._compose_circ(lqc, rqc, qubits=qargs, front=front)
418
+ for lqc in self.circuits
419
+ for rqc in other.circuits
420
+ ]
421
+ probs = [lpr * rpr for lpr in self.probabilities for rpr in other.probabilities]
422
+ return QuantumError(zip(circs, probs))
423
+
424
+ @staticmethod
425
+ def _enlarge_qreg(qc: QuantumCircuit, num_qubits: int):
426
+ if qc.num_qubits < num_qubits:
427
+ enlarged = QuantumCircuit(num_qubits)
428
+ return enlarged.compose(qc)
429
+ return qc
430
+
431
+ @staticmethod
432
+ def _compose_circ(lqc: QuantumCircuit, rqc: QuantumCircuit, qubits, front):
433
+ if qubits is None:
434
+ if front:
435
+ lqc, rqc = rqc, lqc
436
+ if lqc.num_qubits < rqc.num_qubits:
437
+ lqc = QuantumError._enlarge_qreg(lqc, rqc.num_qubits)
438
+ return lqc.compose(rqc)
439
+
440
+ return lqc.compose(rqc, qubits=qubits, front=front)
441
+
442
+ def tensor(self, other):
443
+ if not isinstance(other, QuantumError):
444
+ other = QuantumError(other)
445
+
446
+ circs = [lqc.tensor(rqc) for lqc in self.circuits for rqc in other.circuits]
447
+ probs = [lpr * rpr for lpr in self.probabilities for rpr in other.probabilities]
448
+ return QuantumError(zip(circs, probs))
449
+
450
+ def expand(self, other):
451
+ return other.tensor(self)