Source code for qlauncher.base.models

  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}')