1from __future__ import annotations
2
3from collections import defaultdict
4from itertools import chain
5from typing import TYPE_CHECKING, Literal
6
7import numpy as np
8from qiskit import QuantumCircuit, transpile
9from qiskit._accelerate.circuit import CircuitInstruction as AccelerateInstruction
10from qiskit.circuit import Instruction, Operation
11
12from qlauncher.routines.circuits import CIRCUIT_FORMATS
13from qlauncher.utils import sum_counts
14
15from .base import CircuitExecutionMethod
16
17if TYPE_CHECKING:
18 from qiskit.quantum_info import SparsePauliOp
19
20 from qlauncher.routines.qiskit.adapters import GateCircuitBackend
21 from qlauncher.routines.qiskit.backends.qiskit_backend import QiskitBackend
22
23
[docs]
24class NoMitigation(CircuitExecutionMethod):
25 compatible_circuit = CIRCUIT_FORMATS
26
[docs]
27 def sample(self, circuit: CIRCUIT_FORMATS, backend: GateCircuitBackend, shots: int = 1024) -> dict[str, int]:
28 return backend.sampler.run([circuit], shots=shots).result()[0].join_data().get_counts()
29
[docs]
30 def estimate(self, circuit: QuantumCircuit, observable: SparsePauliOp, backend: QiskitBackend) -> float:
31 return backend.estimator.run([(circuit, observable)]).result()[0].data.evs
32
33
[docs]
34class WeighedMitigation(CircuitExecutionMethod):
35 def __init__(self, mitigation_methods: list[CircuitExecutionMethod], method_weights: list[float] | None = None) -> None:
36 if method_weights is None:
37 method_weights = [1.0] * len(mitigation_methods)
38 if len(method_weights) != len(mitigation_methods):
39 raise ValueError(
40 f'You must provide as many weights as there are methods! Expected {len(mitigation_methods)}, got {len(method_weights)}'
41 )
42 self.weights = method_weights
43 self.methods = mitigation_methods
44 # TODO: Change into intersection between all mitigation methods
45 self.compatible_circuit = mitigation_methods[0].compatible_circuit
46 super().__init__()
47
[docs]
48 def sample(self, circuit: QuantumCircuit, backend: QiskitBackend, shots: int = 1024) -> dict[str, int]:
49 counts = (m.sample(circuit, backend, shots) for m in self.methods)
50 result = defaultdict(int)
51 for count, weight in zip(counts, self.weights, strict=True):
52 for k, v in count.items():
53 result[k] += int(round(v * weight, 0))
54 return result
55
[docs]
56 def estimate(self, circuit: QuantumCircuit, observable: SparsePauliOp, backend: QiskitBackend) -> float:
57 return sum(m.estimate(circuit, observable, backend) * w for m, w in zip(self.methods, self.weights, strict=True)) / len(
58 self.weights
59 )
60
61
[docs]
62class PauliTwirling(CircuitExecutionMethod):
63 """
64 Error mitigation technique based on averaging the results of running multiple "twirled" versions of the initial circuit.
65 The method appends additional gates on both sides of random 2 qubit gates (cx, ecr).
66 """
67
68 compatible_circuit = QuantumCircuit
69
70 def __init__(self, num_random_circuits: int, max_substitute_gates_per_circuit: int = 4, do_transpile: bool = True) -> None:
71 self.num_random_circuits = num_random_circuits
72 self.max_substitute_gates_per_circuit = max_substitute_gates_per_circuit
73 self.do_transpile = do_transpile
74
75 def _random_replacement_op(self, inst: AccelerateInstruction) -> list[AccelerateInstruction]:
76 op: Operation = inst.operation
77 match op.name:
78 case 'cx':
79 return [
80 [
81 AccelerateInstruction(
82 operation=Instruction(name='x', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[0]]
83 ),
84 AccelerateInstruction(
85 operation=Instruction(name='y', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[1]]
86 ),
87 inst,
88 AccelerateInstruction(
89 operation=Instruction(name='y', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[0]]
90 ),
91 AccelerateInstruction(
92 operation=Instruction(name='z', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[1]]
93 ),
94 ],
95 [
96 AccelerateInstruction(
97 operation=Instruction(name='x', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[0]]
98 ),
99 inst,
100 AccelerateInstruction(
101 operation=Instruction(name='x', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[0]]
102 ),
103 AccelerateInstruction(
104 operation=Instruction(name='x', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[1]]
105 ),
106 ],
107 [
108 AccelerateInstruction(
109 operation=Instruction(name='z', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[0]]
110 ),
111 inst,
112 AccelerateInstruction(
113 operation=Instruction(name='z', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[0]]
114 ),
115 ],
116 ][int(np.random.default_rng().integers(0, 3))]
117
118 case 'ecr':
119 return [
120 [
121 AccelerateInstruction(
122 operation=Instruction(name='x', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[0]]
123 ),
124 AccelerateInstruction(
125 operation=Instruction(name='y', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[1]]
126 ),
127 inst,
128 AccelerateInstruction(
129 operation=Instruction(name='x', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[0]]
130 ),
131 AccelerateInstruction(
132 operation=Instruction(name='y', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[1]]
133 ),
134 ],
135 [
136 AccelerateInstruction(
137 operation=Instruction(name='x', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[0]]
138 ),
139 AccelerateInstruction(
140 operation=Instruction(name='z', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[1]]
141 ),
142 inst,
143 AccelerateInstruction(
144 operation=Instruction(name='x', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[0]]
145 ),
146 AccelerateInstruction(
147 operation=Instruction(name='z', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[1]]
148 ),
149 ],
150 [
151 AccelerateInstruction(
152 operation=Instruction(name='x', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[1]]
153 ),
154 inst,
155 AccelerateInstruction(
156 operation=Instruction(name='x', num_qubits=1, num_clbits=0, params=[]), qubits=[inst.qubits[1]]
157 ),
158 ],
159 ][int(np.random.default_rng().integers(0, 3))]
160 case _:
161 return [inst]
162
163 def _twirl_circuit(self, transpiled_circuit: QuantumCircuit) -> QuantumCircuit:
164 """Apply random self.max_substitute_gates_per_circuit twirls on random (no replacement) gates of the circuit."""
165 circuit = transpiled_circuit.copy()
166
167 double_gates_with_indices: list[tuple[int, AccelerateInstruction]] = [
168 (i, x) for i, x in enumerate(circuit.data) if x.operation.num_qubits == 2
169 ]
170
171 choice_idxs = np.random.default_rng().choice(
172 range(len(double_gates_with_indices)),
173 size=min(self.max_substitute_gates_per_circuit, len(double_gates_with_indices)),
174 replace=False,
175 )
176
177 data_cpy = [[x] for x in circuit.data]
178
179 for i in choice_idxs:
180 data_cpy[i] = self._random_replacement_op(data_cpy[i][0])
181
182 circuit.data = list(chain.from_iterable(data_cpy)) # Collapse [[e1],[e2,e3],[e4],...] to [e1,e2,e3,e4,...]
183
184 return circuit
185
186 def _get_workable_circuit(self, circuit: QuantumCircuit, backend: QiskitBackend) -> QuantumCircuit:
187 """Get either transpiled circuit if do_transpile is set or copy of circuit you can change as you wish."""
188 return transpile(circuit, basis_gates=list(backend.backendv1v2.target.operation_names)) if self.do_transpile else circuit.copy()
189
[docs]
190 def sample(self, circuit: QuantumCircuit, backend: QiskitBackend, shots: int = 1024) -> dict[str, int]:
191 input_circ = self._get_workable_circuit(circuit, backend)
192 results = backend.sampler.run(
193 [transpile(self._twirl_circuit(input_circ), backend.backendv1v2) for _ in range(self.num_random_circuits)],
194 shots=shots // self.num_random_circuits,
195 ).result()
196
197 counts = [r.join_data().get_counts() for r in results]
198
199 return sum_counts(*counts)
200
[docs]
201 def estimate(self, circuit: QuantumCircuit, observable: SparsePauliOp, backend: QiskitBackend) -> float:
202 input_circ = self._get_workable_circuit(circuit, backend)
203
204 results = backend.estimator.run(
205 [
206 (transpile(self._twirl_circuit(input_circ), basis_gates=list(backend.backendv1v2.target.operation_names)), observable)
207 for _ in range(self.num_random_circuits)
208 ],
209 ).result()
210
211 sum_evs = 0
212 for r in results:
213 sum_evs += r.data.evs
214
215 return sum_evs / self.num_random_circuits
216
217