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