qilisdk 0.1.3__py3-none-any.whl → 0.1.5__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 (83) hide show
  1. qilisdk/__init__.py +11 -2
  2. qilisdk/__init__.pyi +2 -3
  3. qilisdk/_logging.py +135 -0
  4. qilisdk/_optionals.py +5 -7
  5. qilisdk/analog/__init__.py +3 -18
  6. qilisdk/analog/exceptions.py +2 -4
  7. qilisdk/analog/hamiltonian.py +455 -110
  8. qilisdk/analog/linear_schedule.py +118 -0
  9. qilisdk/analog/schedule.py +272 -79
  10. qilisdk/backends/__init__.py +45 -0
  11. qilisdk/{digital/digital_algorithm.py → backends/__init__.pyi} +3 -5
  12. qilisdk/backends/backend.py +117 -0
  13. qilisdk/{extras/cuda → backends}/cuda_backend.py +153 -161
  14. qilisdk/backends/qutip_backend.py +492 -0
  15. qilisdk/common/__init__.py +48 -2
  16. qilisdk/common/algorithm.py +2 -1
  17. qilisdk/{extras/qaas/qaas_settings.py → common/exceptions.py} +12 -6
  18. qilisdk/common/model.py +1019 -1
  19. qilisdk/common/parameterizable.py +75 -0
  20. qilisdk/common/qtensor.py +666 -0
  21. qilisdk/common/result.py +2 -1
  22. qilisdk/common/variables.py +1931 -0
  23. qilisdk/{extras/cuda/cuda_analog_result.py → cost_functions/__init__.py} +3 -4
  24. qilisdk/cost_functions/cost_function.py +77 -0
  25. qilisdk/cost_functions/model_cost_function.py +145 -0
  26. qilisdk/cost_functions/observable_cost_function.py +109 -0
  27. qilisdk/digital/__init__.py +3 -22
  28. qilisdk/digital/ansatz.py +203 -160
  29. qilisdk/digital/circuit.py +81 -9
  30. qilisdk/digital/exceptions.py +12 -6
  31. qilisdk/digital/gates.py +228 -85
  32. qilisdk/{extras/qaas/qaas_analog_result.py → functionals/__init__.py} +14 -5
  33. qilisdk/functionals/functional.py +39 -0
  34. qilisdk/{extras/cuda/cuda_digital_result.py → functionals/functional_result.py} +3 -4
  35. qilisdk/functionals/sampling.py +81 -0
  36. qilisdk/functionals/sampling_result.py +92 -0
  37. qilisdk/functionals/time_evolution.py +98 -0
  38. qilisdk/functionals/time_evolution_result.py +84 -0
  39. qilisdk/functionals/variational_program.py +80 -0
  40. qilisdk/functionals/variational_program_result.py +69 -0
  41. qilisdk/logging_config.yaml +16 -0
  42. qilisdk/{common/backend.py → optimizers/__init__.py} +2 -1
  43. qilisdk/optimizers/optimizer.py +39 -0
  44. qilisdk/{common → optimizers}/optimizer_result.py +3 -12
  45. qilisdk/{common/optimizer.py → optimizers/scipy_optimizer.py} +10 -28
  46. qilisdk/settings.py +78 -0
  47. qilisdk/{extras → speqtrum}/__init__.py +7 -8
  48. qilisdk/{extras → speqtrum}/__init__.pyi +3 -3
  49. qilisdk/speqtrum/experiments/__init__.py +25 -0
  50. qilisdk/speqtrum/experiments/experiment_functional.py +124 -0
  51. qilisdk/speqtrum/experiments/experiment_result.py +231 -0
  52. qilisdk/{extras/qaas → speqtrum}/keyring.py +8 -4
  53. qilisdk/speqtrum/speqtrum.py +432 -0
  54. qilisdk/speqtrum/speqtrum_models.py +300 -0
  55. qilisdk/utils/__init__.py +0 -14
  56. qilisdk/utils/openqasm2.py +1 -1
  57. qilisdk/utils/serialization.py +1 -1
  58. qilisdk/utils/visualization/PlusJakartaSans-SemiBold.ttf +0 -0
  59. qilisdk/utils/visualization/__init__.py +24 -0
  60. qilisdk/utils/visualization/circuit_renderers.py +781 -0
  61. qilisdk/utils/visualization/schedule_renderers.py +161 -0
  62. qilisdk/utils/visualization/style.py +154 -0
  63. qilisdk/utils/visualization/themes.py +76 -0
  64. qilisdk/yaml.py +126 -0
  65. {qilisdk-0.1.3.dist-info → qilisdk-0.1.5.dist-info}/METADATA +180 -135
  66. qilisdk-0.1.5.dist-info/RECORD +69 -0
  67. qilisdk/analog/algorithms.py +0 -111
  68. qilisdk/analog/analog_backend.py +0 -43
  69. qilisdk/analog/analog_result.py +0 -114
  70. qilisdk/analog/quantum_objects.py +0 -533
  71. qilisdk/digital/digital_backend.py +0 -90
  72. qilisdk/digital/digital_result.py +0 -145
  73. qilisdk/digital/vqe.py +0 -166
  74. qilisdk/extras/cuda/__init__.py +0 -13
  75. qilisdk/extras/qaas/__init__.py +0 -13
  76. qilisdk/extras/qaas/models.py +0 -132
  77. qilisdk/extras/qaas/qaas_backend.py +0 -255
  78. qilisdk/extras/qaas/qaas_digital_result.py +0 -20
  79. qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -20
  80. qilisdk/extras/qaas/qaas_vqe_result.py +0 -20
  81. qilisdk-0.1.3.dist-info/RECORD +0 -51
  82. {qilisdk-0.1.3.dist-info → qilisdk-0.1.5.dist-info}/WHEEL +0 -0
  83. {qilisdk-0.1.3.dist-info → qilisdk-0.1.5.dist-info}/licenses/LICENCE +0 -0
@@ -0,0 +1,492 @@
1
+ # Copyright 2025 Qilimanjaro Quantum Tech
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
+ # http://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
+ from __future__ import annotations
15
+
16
+ from collections import Counter
17
+ from typing import TYPE_CHECKING, Callable, Type, TypeVar
18
+
19
+ import numpy as np
20
+ import qutip_qip.operations as QutipGates
21
+ from loguru import logger
22
+ from qutip import Qobj, basis, mesolve, qeye, tensor
23
+ from qutip_qip.circuit import CircuitSimulator, QubitCircuit
24
+ from qutip_qip.operations.gateclass import SingleQubitGate, is_qutip5
25
+
26
+ from qilisdk.analog.hamiltonian import Hamiltonian, PauliI, PauliOperator
27
+ from qilisdk.backends.backend import Backend
28
+ from qilisdk.common.qtensor import QTensor, tensor_prod
29
+ from qilisdk.digital import RX, RY, RZ, SWAP, U1, U2, U3, Circuit, H, I, M, S, T, X, Y, Z
30
+ from qilisdk.digital.exceptions import UnsupportedGateError
31
+ from qilisdk.digital.gates import Adjoint, BasicGate, Controlled
32
+ from qilisdk.functionals.sampling_result import SamplingResult
33
+ from qilisdk.functionals.time_evolution_result import TimeEvolutionResult
34
+
35
+ if TYPE_CHECKING:
36
+ from qilisdk.common.variables import Number
37
+ from qilisdk.functionals.sampling import Sampling
38
+ from qilisdk.functionals.time_evolution import TimeEvolution
39
+
40
+
41
+ TBasicGate = TypeVar("TBasicGate", bound=BasicGate)
42
+ BasicGateHandlersMapping = dict[Type[TBasicGate], Callable[[QubitCircuit, TBasicGate, int], None]]
43
+
44
+ TPauliOperator = TypeVar("TPauliOperator", bound=PauliOperator)
45
+ PauliOperatorHandlersMapping = dict[Type[TPauliOperator], Callable[[TPauliOperator], Qobj]]
46
+
47
+
48
+ class QutipI(SingleQubitGate):
49
+ """
50
+ Single-qubit I gate.
51
+
52
+ Examples
53
+ --------
54
+ >>> from qutip_qip.operations import X
55
+ >>> I(0).get_compact_qobj() # doctest: +NORMALIZE_WHITESPACE
56
+ Quantum object: dims=[[2], [2]], shape=(2, 2), type='oper', dtype=Dense, isherm=True
57
+ Qobj data =
58
+ [[1. 0.]
59
+ [0. 1.]]
60
+ """
61
+
62
+ def __init__(self, targets, **kwargs) -> None: # noqa: ANN001, ANN003
63
+ super().__init__(targets=targets, **kwargs)
64
+ self.name = "I"
65
+ self.latex_str = r"I"
66
+
67
+ def get_compact_qobj(self): # noqa: ANN201, PLR6301
68
+ return qeye(2) if not is_qutip5 else qeye(2, dtype="dense")
69
+
70
+
71
+ class QutipBackend(Backend):
72
+ """
73
+ Backend that runs both digital-circuit sampling and analog
74
+ time-evolution experiments using the **QuTiP** simulation library.
75
+
76
+ The backend is CPU-only and has no hardware dependencies, which makes it
77
+ ideal for local development, CI pipelines, and educational notebooks.
78
+ """
79
+
80
+ def __init__(self, nsteps: int = 10_000) -> None:
81
+ """Instantiate a new :class:`QutipBackend`.
82
+ Args:
83
+ nsteps (int): The maximum number of internal steps for the ODE solver."""
84
+ self.nsteps = nsteps
85
+
86
+ super().__init__()
87
+ self._basic_gate_handlers: BasicGateHandlersMapping = {
88
+ I: QutipBackend._handle_I,
89
+ X: QutipBackend._handle_X,
90
+ Y: QutipBackend._handle_Y,
91
+ Z: QutipBackend._handle_Z,
92
+ H: QutipBackend._handle_H,
93
+ S: QutipBackend._handle_S,
94
+ T: QutipBackend._handle_T,
95
+ RX: QutipBackend._handle_RX,
96
+ RY: QutipBackend._handle_RY,
97
+ RZ: QutipBackend._handle_RZ,
98
+ U1: QutipBackend._handle_U1,
99
+ U2: QutipBackend._handle_U2,
100
+ U3: QutipBackend._handle_U3,
101
+ SWAP: QutipBackend._handle_SWAP, # type: ignore[dict-item]
102
+ }
103
+ logger.success("QutipBackend initialised")
104
+
105
+ def _execute_sampling(self, functional: Sampling) -> SamplingResult:
106
+ """
107
+ Execute a quantum circuit and return the measurement results.
108
+
109
+ This method applies the selected simulation method, translates the circuit's gates into
110
+ CUDA operations via their respective handlers, runs the simulation, and returns the result
111
+ as a QutipDigitalResult.
112
+
113
+ Args:
114
+ functional (Sampling): The Sampling function to execute.
115
+
116
+ Returns:
117
+ DigitalResult: A result object containing the measurement samples and computed probabilities.
118
+
119
+ """
120
+ logger.info("Executing Sampling (shots={})", functional.nshots)
121
+ qutip_circuit = self._get_qutip_circuit(functional.circuit)
122
+
123
+ counts: Counter[str] = Counter()
124
+ init_state = tensor(*[basis(2, 0) for _ in range(functional.circuit.nqubits)])
125
+
126
+ measurements_set = set()
127
+ for m in functional.circuit.gates:
128
+ if isinstance(m, M):
129
+ measurements_set.update(list(m.target_qubits))
130
+
131
+ measurements = sorted(measurements_set)
132
+
133
+ sim = CircuitSimulator(qutip_circuit)
134
+
135
+ res = sim.run_statistics(init_state) # runs the full circuit for one shot
136
+ _bits = res.cbits # classical measurement bits
137
+ bits = []
138
+ probs = res.probabilities
139
+
140
+ if sum(probs) != 1:
141
+ probs /= sum(probs)
142
+
143
+ if len(measurements) > 0:
144
+ for b in _bits:
145
+ aux = []
146
+ for i in measurements:
147
+ aux.append(b[i])
148
+ bits.append(aux)
149
+ else:
150
+ bits = _bits
151
+
152
+ bits_list = ["".join(map(str, cb)) for cb in bits]
153
+
154
+ rng = np.random.default_rng()
155
+ samples = rng.choice(bits_list, size=functional.nshots, p=probs)
156
+ samples_py = map(str, samples)
157
+
158
+ counts = Counter(samples_py)
159
+
160
+ logger.success("Sampling finished; {} distinct bitstrings", len(counts))
161
+ return SamplingResult(nshots=functional.nshots, samples=dict(counts))
162
+
163
+ def _execute_time_evolution(self, functional: TimeEvolution) -> TimeEvolutionResult:
164
+ """computes the time evolution under of an initial state under the given schedule.
165
+
166
+ Args:
167
+ functional (TimeEvolution): The TimeEvolution functional to execute.
168
+
169
+ Returns:
170
+ TimeEvolutionResult: The results of the evolution.
171
+
172
+ Raises:
173
+ ValueError: if the initial state provided is invalid.
174
+ """
175
+ logger.info("Executing TimeEvolution (T={}, dt={})", functional.schedule.T, functional.schedule.dt)
176
+ tlist = np.linspace(0, functional.schedule.T, int(functional.schedule.T / functional.schedule.dt))
177
+
178
+ qutip_hamiltonians = []
179
+ for hamiltonian in functional.schedule.hamiltonians.values():
180
+ qutip_hamiltonians.append(
181
+ Qobj(
182
+ hamiltonian.to_matrix().toarray(), dims=[[2 for _ in range(hamiltonian.nqubits)] for _ in range(2)]
183
+ )
184
+ )
185
+
186
+ def get_hamiltonian_schedule(
187
+ hamiltonian: str, dt: float, schedule: dict[int, dict[str, Number]], T: float
188
+ ) -> Callable:
189
+ def get_coeff(t: float) -> Number:
190
+ if int(t / dt) in schedule:
191
+ return schedule[int(t / dt)][hamiltonian]
192
+ time_step = int(t / dt)
193
+ while time_step > 0:
194
+ time_step -= 1
195
+ if time_step in schedule:
196
+ return schedule[time_step][hamiltonian]
197
+ return 0
198
+
199
+ return get_coeff
200
+ # return lambda t: schedule[int(t / dt)][ham] if int(t / dt) < int(T / dt) else schedule[int(T / dt)][ham]
201
+
202
+ H_t = [
203
+ [
204
+ qutip_hamiltonians[i],
205
+ get_hamiltonian_schedule(
206
+ h, functional.schedule.dt, functional.schedule.schedule, functional.schedule.T
207
+ ),
208
+ ]
209
+ for i, h in enumerate(functional.schedule.hamiltonians)
210
+ ]
211
+ state_dim = []
212
+ if functional.initial_state.is_density_matrix():
213
+ state_dim = [[2 for _ in range(functional.initial_state.nqubits)] for _ in range(2)]
214
+ elif functional.initial_state.is_bra():
215
+ state_dim = [[1], [2 for _ in range(functional.initial_state.nqubits)]]
216
+ elif functional.initial_state.is_ket():
217
+ state_dim = [[2 for _ in range(functional.initial_state.nqubits)], [1]]
218
+ else:
219
+ logger.error("Invalid initial state provided")
220
+ raise ValueError("invalid initial state provided.")
221
+
222
+ qutip_init_state = Qobj(functional.initial_state.dense, dims=state_dim)
223
+
224
+ qutip_obs: list[Qobj] = []
225
+
226
+ identity = QTensor(PauliI(0).matrix)
227
+ for obs in functional.observables:
228
+ aux_obs = None
229
+ if isinstance(obs, PauliOperator):
230
+ for i in range(functional.schedule.nqubits):
231
+ if aux_obs is None:
232
+ aux_obs = identity if i != obs.qubit else QTensor(obs.matrix)
233
+ else:
234
+ aux_obs = (
235
+ tensor_prod([aux_obs, identity])
236
+ if i != obs.qubit
237
+ else tensor_prod([aux_obs, QTensor(obs.matrix)])
238
+ )
239
+ elif isinstance(obs, Hamiltonian):
240
+ aux_obs = QTensor(obs.to_matrix())
241
+ if obs.nqubits < functional.schedule.nqubits:
242
+ for _ in range(functional.schedule.nqubits - obs.nqubits):
243
+ aux_obs = tensor_prod([aux_obs, identity])
244
+ elif isinstance(obs, QTensor):
245
+ aux_obs = obs
246
+ else:
247
+ logger.error("Unsupported observable type {}", obs.__class__.__name__)
248
+ raise ValueError(f"unsupported observable type of {obs.__class__}")
249
+ if aux_obs is not None:
250
+ qutip_obs.append(
251
+ Qobj(aux_obs.dense, dims=[[2 for _ in range(functional.schedule.nqubits)] for _ in range(2)])
252
+ )
253
+
254
+ results = mesolve(
255
+ H=H_t,
256
+ e_ops=qutip_obs,
257
+ rho0=qutip_init_state,
258
+ tlist=tlist,
259
+ options={
260
+ "store_states": functional.store_intermediate_results,
261
+ "store_final_state": True,
262
+ "nsteps": self.nsteps,
263
+ },
264
+ )
265
+
266
+ logger.success("TimeEvolution finished")
267
+ return TimeEvolutionResult(
268
+ final_expected_values=np.array([results.expect[i][-1] for i in range(len(qutip_obs))]),
269
+ expected_values=(
270
+ np.array(
271
+ [
272
+ [results.expect[val][i] for val in range(len(results.expect))]
273
+ for i in range(len(results.expect[0]))
274
+ ]
275
+ )
276
+ if len(results.expect) > 0 and functional.store_intermediate_results
277
+ else None
278
+ ),
279
+ final_state=(QTensor(results.final_state.full()) if results.final_state is not None else None),
280
+ intermediate_states=(
281
+ [QTensor(state.full()) for state in results.states]
282
+ if len(results.states) > 1 and functional.store_intermediate_results
283
+ else None
284
+ ),
285
+ )
286
+
287
+ def _get_qutip_circuit(self, circuit: Circuit) -> QubitCircuit:
288
+ """_summary_
289
+
290
+ Args:
291
+ circuit (Circuit): the qiliSDK circuit to be translated to qutip.
292
+
293
+ Raises:
294
+ UnsupportedGateError: If the circuit contains a gate for which no handler is registered.
295
+
296
+ Returns:
297
+ QubitCircuit: the translated qutip circuit.
298
+ """
299
+ qutip_circuit = QubitCircuit(
300
+ circuit.nqubits, num_cbits=circuit.nqubits, input_states=[0 for _ in range(circuit.nqubits)]
301
+ )
302
+
303
+ for gate in circuit.gates:
304
+ if isinstance(gate, Controlled):
305
+ self._handle_controlled(qutip_circuit, gate)
306
+ elif isinstance(gate, Adjoint):
307
+ self._handle_adjoint(qutip_circuit, gate)
308
+ elif isinstance(gate, M):
309
+ self._handle_M(qutip_circuit, gate)
310
+ else:
311
+ handler = self._basic_gate_handlers.get(type(gate), None)
312
+ if handler is None:
313
+ logger.error("Unsupported gate {}", type(gate).__name__)
314
+ raise UnsupportedGateError(f"Unsupported gate {type(gate).__name__}")
315
+ handler(qutip_circuit, gate, *(qubit for qubit in gate.target_qubits))
316
+
317
+ no_measurement = True
318
+
319
+ for g in circuit.gates:
320
+ if isinstance(g, M):
321
+ no_measurement = False
322
+
323
+ if no_measurement:
324
+ for i in range(circuit.nqubits):
325
+ qutip_circuit.add_measurement(f"M{i}", targets=i, classical_store=i)
326
+ return qutip_circuit
327
+
328
+ def _handle_controlled(self, circuit: QubitCircuit, gate: Controlled) -> None: # noqa: PLR6301
329
+ """
330
+ Handle a controlled gate operation.
331
+
332
+ This method processes a controlled gate by creating a temporary kernel for the basic gate,
333
+ applying its handler, and then integrating it into the main kernel as a controlled operation.
334
+
335
+ Raises:
336
+ UnsupportedGateError: If the number of control qubits is not equal to one or if the basic gate is unsupported.
337
+ """
338
+ if len(gate.control_qubits) != 1:
339
+ logger.error("Controlled gate with {} control qubits not supported", len(gate.control_qubits))
340
+ raise UnsupportedGateError
341
+
342
+ def qutip_controlled_gate() -> Qobj:
343
+ return QutipGates.controlled_gate(Qobj(gate.basic_gate.matrix), controls=0, targets=1)
344
+
345
+ if gate.name == "CNOT":
346
+ circuit.add_gate("CNOT", targets=[*gate.target_qubits], controls=[*gate.control_qubits])
347
+ else:
348
+ gate_name = "Controlled_" + gate.name
349
+ if gate_name not in circuit.user_gates:
350
+ circuit.user_gates[gate_name] = qutip_controlled_gate
351
+ circuit.add_gate(gate_name, targets=[*gate.control_qubits, *gate.target_qubits])
352
+
353
+ def _handle_adjoint(self, circuit: QubitCircuit, gate: Adjoint) -> None: # noqa: PLR6301
354
+ """
355
+ Handle an adjoint (inverse) gate operation.
356
+
357
+ This method creates a temporary kernel for the basic gate wrapped by the adjoint,
358
+ applies the corresponding handler, and then integrates it into the main kernel as an adjoint operation.
359
+ """
360
+
361
+ def qutip_adjoined_gate() -> Qobj:
362
+ return Qobj(gate.matrix)
363
+
364
+ gate_name = "Adjoint_" + gate.name
365
+ if gate_name not in circuit.user_gates:
366
+ circuit.user_gates[gate_name] = qutip_adjoined_gate
367
+ circuit.add_gate(gate_name, targets=[*gate.target_qubits])
368
+
369
+ @staticmethod
370
+ def _handle_M(qutip_circuit: QubitCircuit, gate: M) -> None:
371
+ """
372
+ Handle a measurement gate.
373
+
374
+ Depending on whether the measurement targets all qubits or a subset,
375
+ this method applies measurement operations accordingly.
376
+ """
377
+ for i in gate.target_qubits:
378
+ qutip_circuit.add_measurement(f"M{i}", targets=[i], classical_store=i)
379
+
380
+ @staticmethod
381
+ def _handle_I(circuit: QubitCircuit, gate: I, qubit: int) -> None:
382
+ """Handle an X gate operation."""
383
+ circuit.add_gate(QutipI(targets=qubit))
384
+
385
+ @staticmethod
386
+ def _handle_X(circuit: QubitCircuit, gate: X, qubit: int) -> None:
387
+ """Handle an X gate operation."""
388
+ circuit.add_gate(QutipGates.X(targets=qubit))
389
+
390
+ @staticmethod
391
+ def _handle_Y(circuit: QubitCircuit, gate: Y, qubit: int) -> None:
392
+ """Handle an Y gate operation."""
393
+ circuit.add_gate(QutipGates.Y(targets=qubit))
394
+
395
+ @staticmethod
396
+ def _handle_Z(circuit: QubitCircuit, gate: Z, qubit: int) -> None:
397
+ """Handle an Z gate operation."""
398
+ circuit.add_gate(QutipGates.Z(targets=qubit))
399
+
400
+ @staticmethod
401
+ def _handle_H(circuit: QubitCircuit, gate: H, qubit: int) -> None:
402
+ """Handle an H gate operation."""
403
+ circuit.add_gate(QutipGates.H(targets=qubit))
404
+
405
+ @staticmethod
406
+ def _handle_S(circuit: QubitCircuit, gate: S, qubit: int) -> None:
407
+ """Handle an S gate operation."""
408
+ circuit.add_gate(QutipGates.S(targets=qubit))
409
+
410
+ @staticmethod
411
+ def _handle_T(circuit: QubitCircuit, gate: T, qubit: int) -> None:
412
+ """Handle an T gate operation."""
413
+ circuit.add_gate(QutipGates.T(targets=qubit))
414
+
415
+ @staticmethod
416
+ def _handle_RX(circuit: QubitCircuit, gate: RX, qubit: int) -> None:
417
+ """Handle an RX gate operation."""
418
+ circuit.add_gate(QutipGates.RX(targets=[qubit], arg_value=gate.get_parameter_values()[0]))
419
+
420
+ @staticmethod
421
+ def _handle_RY(circuit: QubitCircuit, gate: RY, qubit: int) -> None:
422
+ """Handle an RY gate operation."""
423
+ circuit.add_gate(QutipGates.RY(targets=[qubit], arg_value=gate.get_parameter_values()[0]))
424
+
425
+ @staticmethod
426
+ def _handle_RZ(circuit: QubitCircuit, gate: RZ, qubit: int) -> None:
427
+ """Handle an RZ gate operation."""
428
+ circuit.add_gate(QutipGates.RZ(targets=[qubit], arg_value=gate.get_parameter_values()[0]))
429
+
430
+ @staticmethod
431
+ def _qutip_U1(phi: float) -> Qobj:
432
+ mat = np.array([[1, 0], [0, np.exp(1j * phi)]], dtype=complex)
433
+ return Qobj(mat, dims=[[2], [2]])
434
+
435
+ @staticmethod
436
+ def _handle_U1(circuit: QubitCircuit, gate: U1, qubit: int) -> None:
437
+ """Handle an U1 gate operation."""
438
+ U1_label = "U1"
439
+
440
+ if U1_label not in circuit.user_gates:
441
+ circuit.user_gates[U1_label] = QutipBackend._qutip_U1
442
+ circuit.add_gate(U1_label, targets=qubit, arg_value=gate.phi)
443
+
444
+ @staticmethod
445
+ def _qutip_U2(angles: list[float]) -> Qobj:
446
+ phi = angles[0]
447
+ gamma = angles[1]
448
+ mat = (1 / np.sqrt(2)) * np.array(
449
+ [
450
+ [1, -np.exp(1j * gamma)],
451
+ [np.exp(1j * phi), np.exp(1j * (phi + gamma))],
452
+ ],
453
+ dtype=complex,
454
+ )
455
+ return Qobj(mat, dims=[[2], [2]])
456
+
457
+ @staticmethod
458
+ def _handle_U2(circuit: QubitCircuit, gate: U2, qubit: int) -> None:
459
+ """Handle an U2 gate operation."""
460
+ U2_label = "U2"
461
+
462
+ if U2_label not in circuit.user_gates:
463
+ circuit.user_gates[U2_label] = QutipBackend._qutip_U2
464
+ circuit.add_gate(U2_label, targets=qubit, arg_value=[gate.phi, gate.gamma])
465
+
466
+ @staticmethod
467
+ def _qutip_U3(angles: list[float]) -> Qobj:
468
+ phi = angles[0]
469
+ gamma = angles[1]
470
+ theta = angles[2]
471
+ mat = np.array(
472
+ [
473
+ [np.cos(theta / 2), -np.exp(1j * gamma) * np.sin(theta / 2)],
474
+ [np.exp(1j * phi) * np.sin(theta / 2), np.exp(1j * (phi + gamma)) * np.cos(theta / 2)],
475
+ ],
476
+ dtype=complex,
477
+ )
478
+ return Qobj(mat, dims=[[2], [2]])
479
+
480
+ @staticmethod
481
+ def _handle_U3(circuit: QubitCircuit, gate: U3, qubit: int) -> None:
482
+ """Handle an U3 gate operation."""
483
+ U3_label = "U3"
484
+
485
+ if U3_label not in circuit.user_gates:
486
+ circuit.user_gates[U3_label] = QutipBackend._qutip_U3
487
+ circuit.add_gate(U3_label, targets=qubit, arg_value=[gate.phi, gate.gamma, gate.theta])
488
+
489
+ @staticmethod
490
+ def _handle_SWAP(circuit: QubitCircuit, gate: SWAP, qubit_0: int, qubit_1: int) -> None:
491
+ """Handle a SWAP gate operation."""
492
+ circuit.add_gate(QutipGates.SWAP(targets=[qubit_0, qubit_1]))
@@ -12,6 +12,52 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from .optimizer import SciPyOptimizer
15
+ from .model import Constraint, Model, Objective, ObjectiveSense
16
+ from .qtensor import QTensor, basis_state, bra, expect_val, ket, tensor_prod
17
+ from .variables import (
18
+ EQ,
19
+ GEQ,
20
+ GT,
21
+ LEQ,
22
+ LT,
23
+ NEQ,
24
+ BinaryVariable,
25
+ Equal,
26
+ GreaterThan,
27
+ GreaterThanOrEqual,
28
+ LessThan,
29
+ LessThanOrEqual,
30
+ NotEqual,
31
+ Parameter,
32
+ SpinVariable,
33
+ Variable,
34
+ )
16
35
 
17
- __all__ = ["SciPyOptimizer"]
36
+ __all__ = [
37
+ "EQ",
38
+ "GEQ",
39
+ "GT",
40
+ "LEQ",
41
+ "LT",
42
+ "NEQ",
43
+ "BinaryVariable",
44
+ "Constraint",
45
+ "Equal",
46
+ "GreaterThan",
47
+ "GreaterThanOrEqual",
48
+ "LessThan",
49
+ "LessThanOrEqual",
50
+ "Model",
51
+ "NotEqual",
52
+ "Objective",
53
+ "ObjectiveSense",
54
+ "Parameter",
55
+ "QTensor",
56
+ "SpinVariable",
57
+ "Variable",
58
+ "basis_state",
59
+ "bra",
60
+ "expect_val",
61
+ "ket",
62
+ "tensor_prod",
63
+ ]
@@ -14,4 +14,5 @@
14
14
  from abc import ABC
15
15
 
16
16
 
17
- class Algorithm(ABC): ...
17
+ class Algorithm(ABC):
18
+ """Abstract base class for SDK algorithms."""
@@ -12,12 +12,18 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
 
15
- from pydantic import Field
16
- from pydantic_settings import BaseSettings, SettingsConfigDict
17
15
 
16
+ class OutOfBoundsException(Exception):
17
+ """Raised when a variable value falls outside its configured bounds."""
18
18
 
19
- class QaaSSettings(BaseSettings):
20
- model_config = SettingsConfigDict(env_prefix="qaas_", env_file=".env", env_file_encoding="utf-8")
21
19
 
22
- username: str = Field(..., description="QaaS Username")
23
- apikey: str = Field(..., description="QaaS API Key")
20
+ class NotSupportedOperation(Exception):
21
+ """Raised when a requested operation is not supported by the backend."""
22
+
23
+
24
+ class InvalidBoundsError(Exception):
25
+ """Raised when lower/upper bounds are inconsistent or invalid."""
26
+
27
+
28
+ class EvaluationError(Exception):
29
+ """Raised when a symbolic expression cannot be evaluated."""