1from collections.abc import Callable
2from typing import TYPE_CHECKING, Any, overload
3
4import numpy as np
5from dimod import BinaryQuadraticModel
6from pyqubo import Model as pyquboModel
7from pyqubo import Spin # type: ignore
8from qiskit import QuantumCircuit, QuantumRegister
9from qiskit.circuit import Gate
10from qiskit.circuit.library import QFTGate, SwapGate, UnitaryGate, XGate
11from qiskit.quantum_info import SparsePauliOp
12from qiskit_nature.second_q.drivers import PySCFDriver
13from qiskit_nature.second_q.formats.molecule_info import MoleculeInfo
14from qiskit_nature.second_q.mappers import ParityMapper, QubitMapper
15from qiskit_optimization.converters import QuadraticProgramToQubo
16from qiskit_optimization.translators import from_ising
17
18if TYPE_CHECKING:
19 from qiskit_nature.second_q.problems import ElectronicStructureProblem
20
21from qlauncher.hampy import Equation
22
23
[docs]
24class Model:
25 _all_problems: dict[str, type['Model']] = {}
26
27 def __init__(self, instance: Any) -> None:
28 self.instance = instance
29
30 def __init_subclass__(cls) -> None:
31 if Model not in cls.__bases__:
32 return
33 Model._all_problems[cls.__name__] = cls
34 cls._mapping: dict[type[Model], Callable[[], Model]] = {}
35 for method_name in cls.__dict__:
36 if method_name.startswith('to_'):
37 method = cls.__dict__[method_name]
38 cls._mapping[method.__annotations__['return']] = method
39
40
[docs]
41class QUBO(Model):
42 def __init__(self, matrix: np.ndarray, offset: float = 0) -> None:
43 self.matrix = matrix
44 self.offset = offset
45
[docs]
46 def to_hamiltonian(self) -> 'Hamiltonian':
47 num_vars = self.matrix.shape[0]
48 pauli = 0
49 for i, col in enumerate(self.matrix):
50 for j, entry in enumerate(col):
51 if entry == 0:
52 continue
53 if i == j:
54 pauli += SparsePauliOp.from_sparse_list([('I', [0], 0.5), ('Z', [i], -0.5)], num_vars) * entry
55 else:
56 pauli += (
57 SparsePauliOp.from_sparse_list(
58 [('I', [0], 0.25), ('Z', [i], -0.25), ('Z', [j], -0.25), ('ZZ', [i, j], 0.25)], num_vars
59 )
60 * entry
61 )
62 pauli += SparsePauliOp.from_sparse_list([('I', [], self.offset)], num_vars)
63 return Hamiltonian(Equation(pauli))
64
[docs]
65 def to_fn(self) -> 'FN':
66 def function(bin_vec: np.ndarray) -> float:
67 return np.dot(bin_vec, np.dot(self.matrix, bin_vec)) + self.offset
68
69 return FN(function)
70
[docs]
71 def to_bqm(self: 'QUBO') -> 'BQM':
72 matrix = self.matrix
73 symmetric = (self.matrix.transpose() == self.matrix).all()
74 if not symmetric:
75 for i in range(len(self.matrix)):
76 for j in range(len(self.matrix)):
77 if i > j:
78 matrix[i][j] = 0
79
80 values_and_qubits = {(x, y): c for y, r in enumerate(matrix) for x, c in enumerate(r) if c != 0}
81 number_of_qubits = len(matrix)
82 qubits = [Spin(f'x{i}') for i in range(number_of_qubits)]
83 H = 0
84 for (x, y), value in values_and_qubits.items():
85 if symmetric:
86 H += value / len({x, y}) * qubits[x] * qubits[y]
87 else:
88 H += value * qubits[x] * qubits[y]
89 model = H.compile()
90 return BQM(model)
91
92
[docs]
93class FN(Model):
94 def __init__(self, function: Callable[[np.ndarray], float]) -> None:
95 self.function = function
96
97 def __call__(self, vector: np.ndarray) -> float:
98 return self.function(vector)
99
100
[docs]
101class Hamiltonian(Model):
102 @overload
103 def __init__(
104 self, hamiltonian: SparsePauliOp, mixer_hamiltonian: SparsePauliOp | None = None, initial_state: QuantumCircuit | None = None
105 ) -> None: ...
106 @overload
107 def __init__(
108 self,
109 hamiltonian: Equation,
110 mixer_hamiltonian: Equation | None = None,
111 initial_state: QuantumCircuit | None = None,
112 ) -> None: ...
113 def __init__(
114 self,
115 hamiltonian: Equation | SparsePauliOp,
116 mixer_hamiltonian: Equation | SparsePauliOp | None = None,
117 initial_state: QuantumCircuit | None = None,
118 ) -> None:
119 if isinstance(hamiltonian, SparsePauliOp):
120 hamiltonian = Equation(hamiltonian)
121 if isinstance(mixer_hamiltonian, SparsePauliOp):
122 mixer_hamiltonian = Equation(mixer_hamiltonian)
123 self._hampy_equation = hamiltonian
124 self._hampy_mixer_equation: Equation | None = mixer_hamiltonian
125 self._initial_state: QuantumCircuit | None = initial_state
126
127 @property
128 def mixer_hamiltonian(self) -> SparsePauliOp | None:
129 if self._hampy_mixer_equation is not None:
130 return self._hampy_mixer_equation.hamiltonian
131 return None
132
133 @property
134 def hamiltonian(self) -> SparsePauliOp:
135 return self._hampy_equation.hamiltonian
136
137 @property
138 def is_quadratic(self) -> bool:
139 return self._hampy_equation.is_quadratic()
140
141 @mixer_hamiltonian.setter
142 def mixer_hamiltonian(self, mixer_hamiltonian: SparsePauliOp) -> None:
143 self._hampy_mixer_equation = Equation(mixer_hamiltonian)
144
145 @property
146 def initial_state(self) -> QuantumCircuit | None:
147 return self._initial_state
148
149 @initial_state.setter
150 def initial_state(self, initial_state: QuantumCircuit) -> None:
151 self._initial_state = initial_state
152
[docs]
153 def to_qubo(self) -> QUBO:
154 qp = from_ising(self.hamiltonian)
155 conv = QuadraticProgramToQubo()
156 qubo = conv.convert(qp).objective
157 return QUBO(qubo.quadratic.to_array(), qubo.constant)
158
159
[docs]
160class BQM(Model):
161 def __init__(self, model: pyquboModel) -> None: # noqa: ANN401
162 self.model = model
163
164 @property
165 def bqm(self) -> BinaryQuadraticModel:
166 return self.model.to_bqm()
167
[docs]
168 def to_qubo(self) -> QUBO:
169 """Returns Qubo function"""
170 model = self.model
171 variables = sorted(model.variables)
172 num_qubits = len(variables)
173 Q_matrix = np.zeros((num_qubits, num_qubits))
174 inv_map = {v: i for i, v in enumerate(variables)}
175 qubo_dict, offset = model.to_qubo()
176 for key, value in qubo_dict.items():
177 var1, var2 = key
178 Q_matrix[inv_map[var1], inv_map[var2]] = value
179 return QUBO(Q_matrix, offset)
180
181
[docs]
182class Molecule(Model):
183 def __init__(self, instance: MoleculeInfo, mapper: QubitMapper | None = None, basis_set: str = 'STO-6G') -> None:
184 self.instance = instance
185 self.basis_set = basis_set
186 self.mapper = ParityMapper() if mapper is None else mapper
187 self.problem: ElectronicStructureProblem = PySCFDriver.from_molecule(instance, basis=self.basis_set).run()
188 self.mapper.num_particles = self.problem.num_particles
189 operator = self.mapper.map(self.problem.hamiltonian.second_q_op())
190 if not isinstance(operator, SparsePauliOp):
191 raise TypeError
192 self.operator: SparsePauliOp = operator
193
[docs]
194 @staticmethod
195 def from_preset(instance_name: str) -> 'Molecule':
196 match instance_name:
197 case 'H2':
198 instance = MoleculeInfo(['H', 'H'], [(0.0, 0.0, 0.0), (0.0, 0.0, 0.74)])
199 case _:
200 raise ValueError(f"Molecule {instance_name} not supported, currently you can use: 'H2'")
201 return Molecule(instance)
202
203
[docs]
204class GroverCircuit(Model):
[docs]
205 @staticmethod
206 def create_oracle_from_bitstring(bit_string: str | list[str]) -> Gate:
207 """
208 Creates oracle from given bit string
209 """
210 num_qubits = len(bit_string) + 1
211 qc = QuantumCircuit(num_qubits, name=f'Oracle_{bit_string}')
212
213 reversed_s = bit_string[::-1] # Does this stay???
214
215 qc.x(-1)
216 qc.h(-1)
217
218 for i, char in enumerate(reversed_s):
219 if char == '0':
220 qc.x(i)
221
222 mcx = XGate().control(num_qubits - 1)
223 qc.append(mcx, range(num_qubits))
224
225 for i, char in enumerate(reversed_s):
226 if char == '0':
227 qc.x(i)
228
229 qc.h(-1)
230 qc.x(-1)
231
232 return qc.to_gate()
233
234 @staticmethod
235 def _create_hadamard_walsh_transform(num_qubits: int) -> Gate:
236 print('state_prep is None. Using Hadamard-Walsh Transform')
237 state_prep_circ = QuantumCircuit(num_qubits)
238 state_prep_circ.h(range(num_qubits))
239 return state_prep_circ.to_gate()
240
241 @staticmethod
242 def _validate_and_create_oracle(val: str | list[str]) -> Gate:
243 if not all(c in '01' for c in val):
244 raise ValueError('String/List must contain only zeros and ones.')
245 return GroverCircuit.create_oracle_from_bitstring(val)
246
247 def __init__(
248 self,
249 oracle: QuantumCircuit | np.ndarray | list[str] | str,
250 num_solutions: int = None,
251 num_iterations: int = None,
252 state_prep: QuantumCircuit | Gate | np.ndarray = None,
253 num_state_qubits: int = None,
254 ):
255 """ """
256 self.num_solutions = num_solutions
257
258 if not (num_solutions or num_iterations):
259 raise ValueError('At least one of num_solutions, num_iterations has to be not None')
260
261 self.state_prep = state_prep
262
263 self._gate = None
264
265 oracle_conversion_map = {
266 QuantumCircuit: lambda x: x.to_gate(),
267 str: GroverCircuit._validate_and_create_oracle,
268 list: GroverCircuit._validate_and_create_oracle,
269 np.ndarray: lambda x: UnitaryGate(x),
270 }
271
272 if type(oracle) in oracle_conversion_map:
273 self.oracle = oracle_conversion_map[type(oracle)](oracle)
274 else:
275 raise TypeError(f'Unsupported data type: {type(oracle)}')
276
277 theta = np.arcsin(np.sqrt(num_solutions / (2 ** (self.oracle.num_qubits - 1))))
278 self.num_iterations = num_iterations if num_iterations is not None else int(np.round(np.pi / (4 * theta)))
279
280 self.num_state_qubits = num_state_qubits if num_state_qubits is not None else self.oracle.num_qubits - 1
281
282 state_prep_conversion_map = {
283 QuantumCircuit: lambda x: x.to_gate(),
284 np.ndarray: lambda x: UnitaryGate(x),
285 type(None): lambda _: GroverCircuit._create_hadamard_walsh_transform(self.num_state_qubits),
286 }
287
288 if type(self.state_prep) in state_prep_conversion_map:
289 self.state_prep = state_prep_conversion_map[type(self.state_prep)](self.state_prep)
290
291 self.num_qubits = self.oracle.num_qubits
292
293
[docs]
294class ShorCircuit(Model):
[docs]
295 @staticmethod
296 def create_modular_multiplier_gate_with_uncomputation(factor: int, modulo: int) -> Gate:
297 n_modulo_number_qubits = int(np.floor(np.log2(modulo))) + 1
298
299 def create_adder_gate(factor: int, modulo: int) -> Gate:
300 phase_adder_circuit = QuantumCircuit(n_modulo_number_qubits + 1) # + 1 to avoid overflow
301
302 for qubit_ix in range(n_modulo_number_qubits + 1):
303 phi = 2 * np.pi * factor / 2 ** (n_modulo_number_qubits + 1 - qubit_ix)
304 phase_adder_circuit.rz(phi, qubit_ix)
305
306 return phase_adder_circuit.to_gate()
307
308 def create_modular_adder_gate(factor: int, modulo: int) -> Gate:
309 factor_adder = create_adder_gate(factor, modulo)
310 modulo_adder = create_adder_gate(modulo, modulo)
311
312 controlled_modulo_adder = modulo_adder.control(1)
313
314 factor_substractor = factor_adder.inverse()
315 modulo_substractor = modulo_adder.inverse()
316
317 qft = QFTGate(num_qubits=n_modulo_number_qubits + 1)
318 qft_i = qft.inverse()
319
320 y = QuantumRegister(n_modulo_number_qubits + 1)
321 ancilla = QuantumRegister(1)
322 controls = QuantumRegister(2)
323
324 modular_adder_circuit = QuantumCircuit(controls, y, ancilla)
325
326 modular_adder_circuit.append(factor_adder.control(2), controls[:] + y[:])
327 modular_adder_circuit.append(modulo_substractor, y)
328 modular_adder_circuit.append(qft_i, y)
329 modular_adder_circuit.cx(y[-1], ancilla)
330 modular_adder_circuit.append(qft, y)
331 modular_adder_circuit.append(controlled_modulo_adder, ancilla[:] + y[:])
332 modular_adder_circuit.append(factor_substractor.control(2), controls[:] + y[:])
333 modular_adder_circuit.append(qft_i, y)
334 modular_adder_circuit.x(y[-1])
335 modular_adder_circuit.cx(y[-1], ancilla)
336 modular_adder_circuit.x(y[-1])
337 modular_adder_circuit.append(qft, y[:])
338 modular_adder_circuit.append(factor_adder.control(2), controls[:] + y[:])
339
340 return modular_adder_circuit.to_gate()
341
342 def create_modular_multiplier_gate(factor: int, modulo: int) -> Gate:
343 qft = QFTGate(num_qubits=n_modulo_number_qubits + 1)
344 qft_i = qft.inverse()
345
346 x = QuantumRegister(n_modulo_number_qubits)
347 y = QuantumRegister(n_modulo_number_qubits + 1)
348 ancilla = QuantumRegister(1)
349 control = QuantumRegister(1)
350
351 modular_multiplier_circuit = QuantumCircuit(control, x, y, ancilla)
352
353 modular_multiplier_circuit.append(qft, y)
354
355 for qubit_ix in range(n_modulo_number_qubits):
356 controlled_modular_adder_gate = create_modular_adder_gate(((2**qubit_ix * factor) % modulo), modulo)
357 modular_multiplier_circuit.append(controlled_modular_adder_gate, control[:] + [x[qubit_ix]] + y[:] + ancilla[:])
358
359 modular_multiplier_circuit.append(qft_i, y)
360
361 return modular_multiplier_circuit.to_gate()
362
363 controlled_modular_multiplier_gate = create_modular_multiplier_gate(factor, modulo)
364 controlled_modular_multiplier_gate_i = create_modular_multiplier_gate(pow(factor, -1, modulo), modulo).inverse()
365
366 control = QuantumRegister(1)
367 x = QuantumRegister(n_modulo_number_qubits)
368 y_with_ancilla = QuantumRegister(n_modulo_number_qubits + 1 + 1)
369
370 controlled_modular_multiplier_gate_circuit = QuantumCircuit(control, x, y_with_ancilla)
371
372 controlled_modular_multiplier_gate_circuit.append(controlled_modular_multiplier_gate, control[:] + x[:] + y_with_ancilla[:])
373
374 controlled_swap_gate = SwapGate().control()
375
376 for qubit_ix in range(n_modulo_number_qubits):
377 controlled_modular_multiplier_gate_circuit.append(
378 controlled_swap_gate, control[:] + [x[qubit_ix]] + [y_with_ancilla[qubit_ix]]
379 ) # ancillas are 0 so it's ok
380
381 controlled_modular_multiplier_gate_circuit.append(controlled_modular_multiplier_gate_i, control[:] + x[:] + y_with_ancilla[:])
382
383 return controlled_modular_multiplier_gate_circuit.to_gate()
384
385 def __init__(
386 self,
387 factor: int,
388 modulo: int,
389 n_phase_kickback_qubits: int = None,
390 circuits: list[QuantumCircuit | np.ndarray | Gate] = None,
391 eigen_state_prep: QuantumCircuit | np.ndarray | Gate = None,
392 ):
393 self.factor = factor
394 self.modulo = modulo
395 self.gates = []
396
397 if not n_phase_kickback_qubits:
398 self.n_phase_kickback_qubits = 2 * int(np.floor(np.log2(self.modulo) + 1))
399 else:
400 self.n_phase_kickback_qubits = n_phase_kickback_qubits
401
402 conversion_map = {
403 QuantumCircuit: lambda c: c.to_gate(),
404 np.ndarray: lambda c: UnitaryGate(c),
405 Gate: lambda c: c,
406 type(None): lambda c: None,
407 }
408
409 if circuits is None:
410 for exp in range(2 * (int(np.floor(np.log2(modulo))) + 1)):
411 tmp_factor = (factor ** (2**exp)) % modulo
412 self.gates.append(ShorCircuit.create_modular_multiplier_gate_with_uncomputation(tmp_factor, modulo))
413 else:
414 for circuit in circuits:
415 target_type = type(circuit)
416
417 if target_type in conversion_map:
418 self.gates.append(conversion_map[target_type](circuit))
419 else:
420 raise TypeError(f'Unsupported data type: {target_type}')
421
422 self.num_qubits = self.gates[0].num_qubits
423
424 self.eigen_state_prep = eigen_state_prep
425
426 target_type = type(eigen_state_prep)
427 if target_type in conversion_map:
428 self.eigen_state_prep = conversion_map[target_type](self.eigen_state_prep)
429 else:
430 raise TypeError(f'Unsupported data type: {target_type}')