qilisdk 0.1.0__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 (47) hide show
  1. qilisdk/__init__.py +47 -0
  2. qilisdk/__init__.pyi +30 -0
  3. qilisdk/_optionals.py +105 -0
  4. qilisdk/analog/__init__.py +17 -0
  5. qilisdk/analog/algorithms.py +111 -0
  6. qilisdk/analog/analog_backend.py +43 -0
  7. qilisdk/analog/analog_result.py +114 -0
  8. qilisdk/analog/exceptions.py +19 -0
  9. qilisdk/analog/hamiltonian.py +706 -0
  10. qilisdk/analog/quantum_objects.py +486 -0
  11. qilisdk/analog/schedule.py +311 -0
  12. qilisdk/common/__init__.py +20 -0
  13. qilisdk/common/algorithm.py +17 -0
  14. qilisdk/common/backend.py +16 -0
  15. qilisdk/common/model.py +16 -0
  16. qilisdk/common/optimizer.py +136 -0
  17. qilisdk/common/optimizer_result.py +110 -0
  18. qilisdk/common/result.py +17 -0
  19. qilisdk/digital/__init__.py +66 -0
  20. qilisdk/digital/ansatz.py +143 -0
  21. qilisdk/digital/circuit.py +106 -0
  22. qilisdk/digital/digital_algorithm.py +20 -0
  23. qilisdk/digital/digital_backend.py +90 -0
  24. qilisdk/digital/digital_result.py +145 -0
  25. qilisdk/digital/exceptions.py +31 -0
  26. qilisdk/digital/gates.py +989 -0
  27. qilisdk/digital/vqe.py +165 -0
  28. qilisdk/extras/__init__.py +13 -0
  29. qilisdk/extras/cuda/__init__.py +18 -0
  30. qilisdk/extras/cuda/cuda_analog_result.py +19 -0
  31. qilisdk/extras/cuda/cuda_backend.py +398 -0
  32. qilisdk/extras/cuda/cuda_digital_result.py +19 -0
  33. qilisdk/extras/qaas/__init__.py +13 -0
  34. qilisdk/extras/qaas/keyring.py +54 -0
  35. qilisdk/extras/qaas/models.py +57 -0
  36. qilisdk/extras/qaas/qaas_backend.py +154 -0
  37. qilisdk/extras/qaas/qaas_digital_result.py +20 -0
  38. qilisdk/extras/qaas/qaas_settings.py +23 -0
  39. qilisdk/py.typed +0 -0
  40. qilisdk/utils/__init__.py +27 -0
  41. qilisdk/utils/openqasm2.py +215 -0
  42. qilisdk/utils/serialization.py +128 -0
  43. qilisdk/yaml.py +71 -0
  44. qilisdk-0.1.0.dist-info/METADATA +237 -0
  45. qilisdk-0.1.0.dist-info/RECORD +47 -0
  46. qilisdk-0.1.0.dist-info/WHEEL +4 -0
  47. qilisdk-0.1.0.dist-info/licenses/LICENCE +201 -0
qilisdk/digital/vqe.py ADDED
@@ -0,0 +1,165 @@
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 pprint import pformat
15
+ from typing import Callable
16
+
17
+ from qilisdk.common.optimizer import Optimizer
18
+ from qilisdk.common.optimizer_result import OptimizerIntermediateResult, OptimizerResult
19
+ from qilisdk.common.result import Result
20
+ from qilisdk.digital.ansatz import Ansatz
21
+ from qilisdk.digital.digital_algorithm import DigitalAlgorithm
22
+ from qilisdk.digital.digital_backend import DigitalBackend
23
+ from qilisdk.digital.digital_result import DigitalResult
24
+ from qilisdk.yaml import yaml
25
+
26
+
27
+ @yaml.register_class
28
+ class VQEResult(Result):
29
+ """
30
+ Represents the result of a VQE calculation.
31
+
32
+ Attributes:
33
+ optimal_cost (float): The estimated ground state energy (optimal cost).
34
+ optimal_parameters (list[float]): The optimal parameters found during the optimization.
35
+ """
36
+
37
+ def __init__(self, optimizer_result: OptimizerResult) -> None:
38
+ super().__init__()
39
+ self._optimizer_result: OptimizerResult = optimizer_result
40
+
41
+ @property
42
+ def optimal_cost(self) -> float:
43
+ """
44
+ Get the optimal cost (estimated ground state energy).
45
+
46
+ Returns:
47
+ float: The optimal cost.
48
+ """
49
+ return self._optimizer_result.optimal_cost
50
+
51
+ @property
52
+ def optimal_parameters(self) -> list[float]:
53
+ """
54
+ Get the optimal ansatz parameters.
55
+
56
+ Returns:
57
+ list[float]: The optimal parameters.
58
+ """
59
+ return self._optimizer_result.optimal_parameters
60
+
61
+ @property
62
+ def intermediate_results(self) -> list[OptimizerIntermediateResult]:
63
+ """
64
+ Get the intermediate results.
65
+
66
+ Returns:
67
+ list[OptimizerResult]: The intermediate results.
68
+ """
69
+ return self._optimizer_result.intermediate_results
70
+
71
+ def __repr__(self) -> str:
72
+ """
73
+ Return a string representation of the VQEResult for debugging.
74
+
75
+ Returns:
76
+ str: A formatted string detailing the optimal cost and parameters.
77
+ """
78
+ class_name = self.__class__.__name__
79
+ return (
80
+ f"{class_name}(\n Optimal Cost = {self.optimal_cost},"
81
+ + f"\n Optimal Parameters={pformat(self.optimal_parameters)},"
82
+ + f"\n Intermediate Results={pformat(self.intermediate_results)})"
83
+ )
84
+
85
+
86
+ class VQE(DigitalAlgorithm):
87
+ """
88
+ Implements the Variational Quantum Eigensolver (VQE) algorithm.
89
+
90
+ The VQE algorithm is a hybrid quantum-classical method used to approximate the ground
91
+ state energy of a quantum system. It relies on a parameterized quantum circuit (ansatz)
92
+ whose parameters are tuned by a classical optimizer to minimize the cost function—typically
93
+ the expectation value of the system's Hamiltonian.
94
+
95
+ The algorithm outputs a VQEResult that includes the optimal cost (estimated ground state energy)
96
+ and the optimal parameters that yield this cost.
97
+ """
98
+
99
+ def __init__(
100
+ self, ansatz: Ansatz, initial_params: list[float], cost_function: Callable[[DigitalResult], float]
101
+ ) -> None:
102
+ """
103
+ Initialize the VQE algorithm.
104
+
105
+ Args:
106
+ ansatz (Ansatz): The parameterized quantum circuit representing the trial state.
107
+ initial_params (list[float]): The initial set of parameters for the ansatz.
108
+ cost_function (Callable[[DigitalResult], float]): A function that computes the cost from
109
+ a DigitalResult obtained after executing the circuit. The cost generally represents the
110
+ expectation value of the Hamiltonian.
111
+ """
112
+ self._ansatz = ansatz
113
+ self._initial_params = initial_params
114
+ self._cost_function = cost_function
115
+ self._execution_results: list[DigitalResult]
116
+
117
+ def obtain_cost(self, params: list[float], backend: DigitalBackend, nshots: int = 1000) -> float:
118
+ """
119
+ Evaluate the cost at a given parameter set by executing the corresponding quantum circuit.
120
+
121
+ The process involves:
122
+ 1. Generating the quantum circuit using the ansatz with the specified parameters.
123
+ 2. Executing the circuit on the provided digital backend with the given number of shots.
124
+ 3. Passing the resulting DigitalResult to the cost_function to obtain the cost value.
125
+
126
+ Args:
127
+ params (list[float]): The ansatz parameters to evaluate.
128
+ backend (DigitalBackend): The digital backend that executes the quantum circuit.
129
+ nshots (int, optional): The number of shots (circuit executions). Defaults to 1000.
130
+
131
+ Returns:
132
+ float: The cost computed from the DigitalResult.
133
+ """
134
+ circuit = self._ansatz.get_circuit(params)
135
+ results = backend.execute(circuit=circuit, nshots=nshots)
136
+ return self._cost_function(results)
137
+
138
+ def execute(
139
+ self,
140
+ backend: DigitalBackend,
141
+ optimizer: Optimizer,
142
+ nshots: int = 1000,
143
+ store_intermediate_results: bool = False,
144
+ ) -> VQEResult:
145
+ """
146
+ Run the VQE algorithm to obtain the optimal parameters and the corresponding cost.
147
+
148
+ This method leverages a classical optimizer to minimize the cost function by varying the
149
+ ansatz parameters. The optimizer returns a tuple containing the optimal cost and the optimal
150
+ parameters. A VQEResult object is then created using these values.
151
+
152
+ Args:
153
+ backend (DigitalBackend): The backend for executing quantum circuits.
154
+ optimizer (Optimizer): The classical optimizer for tuning the ansatz parameters.
155
+ nshots (int, optional): The number of shots for each circuit execution. Defaults to 1000.
156
+
157
+ Returns:
158
+ VQEResult: An object containing the optimal cost and the optimal ansatz parameters.
159
+ """
160
+ optimizer_result = optimizer.optimize(
161
+ lambda x: self.obtain_cost(x, backend=backend, nshots=nshots),
162
+ self._initial_params,
163
+ store_intermediate_results=store_intermediate_results,
164
+ )
165
+ return VQEResult(optimizer_result=optimizer_result)
@@ -0,0 +1,13 @@
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.
@@ -0,0 +1,18 @@
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 .cuda_backend import CudaBackend
16
+ from .cuda_digital_result import CudaDigitalResult
17
+
18
+ __all__ = ["CudaBackend", "CudaDigitalResult"]
@@ -0,0 +1,19 @@
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 qilisdk.analog.analog_result import AnalogResult
15
+ from qilisdk.yaml import yaml
16
+
17
+
18
+ @yaml.register_class
19
+ class CudaAnalogResult(AnalogResult): ...
@@ -0,0 +1,398 @@
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 typing import TYPE_CHECKING, Callable, Type, TypeVar
17
+
18
+ import cudaq
19
+ import numpy as np
20
+ from cudaq import State
21
+ from cudaq.operator import ElementaryOperator, OperatorSum, ScalarOperator, evolve, spin
22
+ from cudaq.operator import Schedule as cuda_schedule
23
+
24
+ from qilisdk.analog.analog_backend import AnalogBackend
25
+ from qilisdk.analog.hamiltonian import Hamiltonian, PauliI, PauliOperator, PauliX, PauliY, PauliZ
26
+ from qilisdk.analog.quantum_objects import QuantumObject
27
+ from qilisdk.digital import (
28
+ RX,
29
+ RY,
30
+ RZ,
31
+ U1,
32
+ U2,
33
+ U3,
34
+ Adjoint,
35
+ BasicGate,
36
+ Circuit,
37
+ Controlled,
38
+ H,
39
+ M,
40
+ S,
41
+ T,
42
+ X,
43
+ Y,
44
+ Z,
45
+ )
46
+ from qilisdk.digital.digital_backend import DigitalBackend, DigitalSimulationMethod
47
+ from qilisdk.digital.exceptions import UnsupportedGateError
48
+
49
+ from .cuda_analog_result import CudaAnalogResult
50
+ from .cuda_digital_result import CudaDigitalResult
51
+
52
+ if TYPE_CHECKING:
53
+ from qilisdk.analog.schedule import Schedule
54
+
55
+
56
+ TBasicGate = TypeVar("TBasicGate", bound=BasicGate)
57
+ BasicGateHandlersMapping = dict[Type[TBasicGate], Callable[[cudaq.Kernel, TBasicGate, cudaq.QuakeValue], None]]
58
+
59
+ TPauliOperator = TypeVar("TPauliOperator", bound=PauliOperator)
60
+ PauliOperatorHandlersMapping = dict[Type[TPauliOperator], Callable[[TPauliOperator], ElementaryOperator]]
61
+
62
+
63
+ class CudaBackend(DigitalBackend, AnalogBackend):
64
+ """
65
+ Digital backend implementation using CUDA-based simulation.
66
+
67
+ This backend translates a quantum circuit into a CUDA-compatible kernel and executes it
68
+ using the cudaq library. It supports different simulation methods including state vector,
69
+ tensor network, and matrix product state simulations. Gate operations in the circuit are
70
+ mapped to CUDA operations via dedicated handler functions.
71
+ """
72
+
73
+ def __init__(
74
+ self, digital_simulation_method: DigitalSimulationMethod = DigitalSimulationMethod.STATE_VECTOR
75
+ ) -> None:
76
+ """
77
+ Initialize the CudaBackend.
78
+
79
+ Args:
80
+ digital_simulation_method (SimulationMethod, optional): The simulation method to use for executing circuits.
81
+ Options include STATE_VECTOR, TENSOR_NETWORK, or MATRIX_PRODUCT_STATE.
82
+ Defaults to STATE_VECTOR.
83
+ """
84
+ super().__init__(digital_simulation_method=digital_simulation_method)
85
+ self._basic_gate_handlers: BasicGateHandlersMapping = {
86
+ X: CudaBackend._handle_X,
87
+ Y: CudaBackend._handle_Y,
88
+ Z: CudaBackend._handle_Z,
89
+ H: CudaBackend._handle_H,
90
+ S: CudaBackend._handle_S,
91
+ T: CudaBackend._handle_T,
92
+ RX: CudaBackend._handle_RX,
93
+ RY: CudaBackend._handle_RY,
94
+ RZ: CudaBackend._handle_RZ,
95
+ U1: CudaBackend._handle_U1,
96
+ U2: CudaBackend._handle_U2,
97
+ U3: CudaBackend._handle_U3,
98
+ }
99
+ self._pauli_operator_handlers: PauliOperatorHandlersMapping = {
100
+ PauliX: CudaBackend._handle_PauliX,
101
+ PauliY: CudaBackend._handle_PauliY,
102
+ PauliZ: CudaBackend._handle_PauliZ,
103
+ PauliI: CudaBackend._handle_PauliI,
104
+ }
105
+
106
+ def _apply_digital_simulation_method(self) -> None:
107
+ """
108
+ Configure the cudaq simulation target based on the selected simulation method.
109
+
110
+ For the STATE_VECTOR method, it checks for GPU availability and selects an appropriate target.
111
+ For TENSOR_NETWORK and MATRIX_PRODUCT_STATE methods, it explicitly sets the target to use tensor network-based simulations.
112
+ """
113
+ if self.digital_simulation_method == DigitalSimulationMethod.STATE_VECTOR:
114
+ if cudaq.num_available_gpus() == 0:
115
+ cudaq.set_target("qpp-cpu")
116
+ else:
117
+ cudaq.set_target("nvidia")
118
+ elif self.digital_simulation_method == DigitalSimulationMethod.TENSOR_NETWORK:
119
+ cudaq.set_target("tensornet")
120
+ else:
121
+ cudaq.set_target("tensornet-mps")
122
+
123
+ def execute(self, circuit: Circuit, nshots: int = 1000) -> CudaDigitalResult:
124
+ """
125
+ Execute a quantum circuit and return the measurement results.
126
+
127
+ This method applies the selected simulation method, translates the circuit's gates into
128
+ CUDA operations via their respective handlers, runs the simulation, and returns the result
129
+ as a CudaDigitalResult.
130
+
131
+ Args:
132
+ circuit (Circuit): The quantum circuit to be executed.
133
+ nshots (int, optional): The number of measurement shots to perform. Defaults to 1000.
134
+
135
+ Returns:
136
+ DigitalResult: A result object containing the measurement samples and computed probabilities.
137
+
138
+ Raises:
139
+ UnsupportedGateError: If the circuit contains a gate for which no handler is registered.
140
+ """
141
+ self._apply_digital_simulation_method()
142
+ kernel = cudaq.make_kernel()
143
+ qubits = kernel.qalloc(circuit.nqubits)
144
+
145
+ for gate in circuit.gates:
146
+ if isinstance(gate, Controlled):
147
+ self._handle_controlled(kernel, gate, qubits[gate.control_qubits[0]], qubits[gate.target_qubits[0]])
148
+ elif isinstance(gate, Adjoint):
149
+ self._handle_adjoint(kernel, gate, qubits[gate.target_qubits[0]])
150
+ elif isinstance(gate, M):
151
+ self._handle_M(kernel, gate, circuit, qubits)
152
+ else:
153
+ handler = self._basic_gate_handlers.get(type(gate), None)
154
+ if handler is None:
155
+ raise UnsupportedGateError
156
+ handler(kernel, gate, qubits[gate.target_qubits[0]])
157
+
158
+ cudaq_result = cudaq.sample(kernel, shots_count=nshots)
159
+ return CudaDigitalResult(nshots=nshots, samples=dict(cudaq_result.items()))
160
+
161
+ def _handle_controlled(
162
+ self, kernel: cudaq.Kernel, gate: Controlled, control_qubit: cudaq.QuakeValue, target_qubit: cudaq.QuakeValue
163
+ ) -> None:
164
+ """
165
+ Handle a controlled gate operation.
166
+
167
+ This method processes a controlled gate by creating a temporary kernel for the basic gate,
168
+ applying its handler, and then integrating it into the main kernel as a controlled operation.
169
+
170
+ Args:
171
+ kernel (cudaq.Kernel): The main CUDA kernel being constructed.
172
+ gate (Controlled): The controlled gate to be handled.
173
+ control_qubit (cudaq.QuakeValue): The control qubit for the gate.
174
+ target_qubit (cudaq.QuakeValue): The target qubit for the gate.
175
+
176
+ Raises:
177
+ UnsupportedGateError: If the number of control qubits is not equal to one or if the basic gate is unsupported.
178
+ """
179
+ if len(gate.control_qubits) != 1:
180
+ raise UnsupportedGateError
181
+ target_kernel, qubit = cudaq.make_kernel(cudaq.qubit)
182
+ handler = self._basic_gate_handlers.get(type(gate.basic_gate), None)
183
+ if handler is None:
184
+ raise UnsupportedGateError
185
+ handler(target_kernel, gate.basic_gate, qubit)
186
+ kernel.control(target_kernel, control_qubit, target_qubit)
187
+
188
+ def _handle_adjoint(self, kernel: cudaq.Kernel, gate: Adjoint, target_qubit: cudaq.QuakeValue) -> None:
189
+ """
190
+ Handle an adjoint (inverse) gate operation.
191
+
192
+ This method creates a temporary kernel for the basic gate wrapped by the adjoint,
193
+ applies the corresponding handler, and then integrates it into the main kernel as an adjoint operation.
194
+
195
+ Args:
196
+ kernel (cudaq.Kernel): The main CUDA kernel being constructed.
197
+ gate (Adjoint): The adjoint gate to be handled.
198
+ target_qubit (cudaq.QuakeValue): The target qubit for the gate.
199
+
200
+ Raises:
201
+ UnsupportedGateError: If the basic gate inside the adjoint is unsupported.
202
+ """
203
+ target_kernel, qubit = cudaq.make_kernel(cudaq.qubit)
204
+ handler = self._basic_gate_handlers.get(type(gate.basic_gate), None)
205
+ if handler is None:
206
+ raise UnsupportedGateError
207
+ handler(target_kernel, gate.basic_gate, qubit)
208
+ kernel.adjoint(target_kernel, target_qubit)
209
+
210
+ def _hamiltonian_to_cuda(self, hamiltonian: Hamiltonian) -> OperatorSum:
211
+ out = None
212
+ for offset, terms in hamiltonian:
213
+ if out is None:
214
+ out = offset * np.prod([self._pauli_operator_handlers[type(pauli)](pauli) for pauli in terms])
215
+ else:
216
+ out += offset * np.prod([self._pauli_operator_handlers[type(pauli)](pauli) for pauli in terms])
217
+ return out
218
+
219
+ def evolve(
220
+ self,
221
+ schedule: Schedule,
222
+ initial_state: QuantumObject,
223
+ observables: list[PauliOperator | Hamiltonian],
224
+ store_intermediate_results: bool = False,
225
+ ) -> CudaAnalogResult:
226
+ """computes the time evolution under of an initial state under the given schedule.
227
+
228
+ Args:
229
+ schedule (Schedule): The evolution schedule of the system.
230
+ initial_state (QuantumObject): the initial state of the evolution.
231
+ observables (list[PauliOperator | Hamiltonian]): the list of observables to be measured at the end of the evolution.
232
+ store_intermediate_results (bool): A flag to store the intermediate results along the evolution.
233
+
234
+ Raises:
235
+ ValueError: if the observables provided are not an instance of the PauliOperator or a Hamiltonian class.
236
+
237
+ Returns:
238
+ AnalogResult: The results of the evolution.
239
+ """
240
+ cudaq.set_target("dynamics")
241
+
242
+ cuda_hamiltonian = None
243
+ steps = np.linspace(0, schedule.T, int(schedule.T / schedule.dt))
244
+
245
+ def parameter_values(time_steps: np.ndarray) -> cuda_schedule:
246
+ def compute_value(param_name: str, step_idx: int) -> float:
247
+ return schedule.get_coefficient(time_steps[int(step_idx)], param_name)
248
+
249
+ return cuda_schedule(list(range(len(time_steps))), list(schedule.hamiltonians), compute_value)
250
+
251
+ _cuda_schedule = parameter_values(steps)
252
+
253
+ def get_schedule(key: str) -> Callable:
254
+ return lambda **args: args[key]
255
+
256
+ cuda_hamiltonian = sum(
257
+ ScalarOperator(get_schedule(key)) * self._hamiltonian_to_cuda(ham)
258
+ for key, ham in schedule.hamiltonians.items()
259
+ )
260
+
261
+ cuda_observables = []
262
+ for observable in observables:
263
+ if isinstance(observable, PauliOperator):
264
+ cuda_observables.append(self._pauli_operator_handlers[type(observable)](observable))
265
+ elif isinstance(observable, Hamiltonian):
266
+ cuda_observables.append(self._hamiltonian_to_cuda(observable))
267
+ else:
268
+ raise ValueError(f"unsupported observable type of {observable.__class__}")
269
+
270
+ evolution_result = evolve(
271
+ hamiltonian=cuda_hamiltonian,
272
+ dimensions=dict.fromkeys(range(schedule.nqubits), 2),
273
+ schedule=_cuda_schedule,
274
+ initial_state=State.from_data(np.array(initial_state.to_density_matrix().dense, dtype=np.complex128)),
275
+ observables=cuda_observables,
276
+ collapse_operators=[],
277
+ store_intermediate_results=store_intermediate_results,
278
+ )
279
+
280
+ return CudaAnalogResult(
281
+ final_expected_values=np.array(
282
+ [exp_val.expectation() for exp_val in evolution_result.final_expectation_values()]
283
+ ),
284
+ expected_values=(
285
+ np.array(
286
+ [[val.expectation() for val in exp_vals] for exp_vals in evolution_result.expectation_values()]
287
+ )
288
+ if evolution_result.expectation_values() is not None
289
+ else None
290
+ ),
291
+ final_state=(
292
+ QuantumObject(np.array(evolution_result.final_state())).dag()
293
+ if evolution_result.final_state() is not None
294
+ else None
295
+ ),
296
+ intermediate_states=(
297
+ [QuantumObject(np.array(state)).dag() for state in evolution_result.intermediate_states()]
298
+ if evolution_result.intermediate_states() is not None
299
+ else None
300
+ ),
301
+ )
302
+
303
+ @staticmethod
304
+ def _handle_M(kernel: cudaq.Kernel, gate: M, circuit: Circuit, qubits: cudaq.QuakeValue) -> None:
305
+ """
306
+ Handle a measurement gate.
307
+
308
+ Depending on whether the measurement targets all qubits or a subset,
309
+ this method applies measurement operations accordingly.
310
+
311
+ Args:
312
+ kernel (cudaq.Kernel): The CUDA kernel being constructed.
313
+ gate (M): The measurement gate.
314
+ circuit (Circuit): The circuit containing the measurement gate.
315
+ qubits (cudaq.QuakeValue): The allocated qubits for the circuit.
316
+ """
317
+ if gate.nqubits == circuit.nqubits:
318
+ kernel.mz(qubits)
319
+ else:
320
+ for idx in gate.target_qubits:
321
+ kernel.mz(qubits[idx])
322
+
323
+ @staticmethod
324
+ def _handle_X(kernel: cudaq.Kernel, gate: X, qubit: cudaq.QuakeValue) -> None:
325
+ """Handle an X gate operation."""
326
+ kernel.x(qubit)
327
+
328
+ @staticmethod
329
+ def _handle_Y(kernel: cudaq.Kernel, gate: Y, qubit: cudaq.QuakeValue) -> None:
330
+ """Handle an Y gate operation."""
331
+ kernel.y(qubit)
332
+
333
+ @staticmethod
334
+ def _handle_Z(kernel: cudaq.Kernel, gate: Z, qubit: cudaq.QuakeValue) -> None:
335
+ """Handle an Z gate operation."""
336
+ kernel.z(qubit)
337
+
338
+ @staticmethod
339
+ def _handle_H(kernel: cudaq.Kernel, gate: H, qubit: cudaq.QuakeValue) -> None:
340
+ """Handle an H gate operation."""
341
+ kernel.h(qubit)
342
+
343
+ @staticmethod
344
+ def _handle_S(kernel: cudaq.Kernel, gate: S, qubit: cudaq.QuakeValue) -> None:
345
+ """Handle an S gate operation."""
346
+ kernel.s(qubit)
347
+
348
+ @staticmethod
349
+ def _handle_T(kernel: cudaq.Kernel, gate: T, qubit: cudaq.QuakeValue) -> None:
350
+ """Handle an T gate operation."""
351
+ kernel.t(qubit)
352
+
353
+ @staticmethod
354
+ def _handle_RX(kernel: cudaq.Kernel, gate: RX, qubit: cudaq.QuakeValue) -> None:
355
+ """Handle an RX gate operation."""
356
+ kernel.rx(*gate.parameter_values, qubit)
357
+
358
+ @staticmethod
359
+ def _handle_RY(kernel: cudaq.Kernel, gate: RY, qubit: cudaq.QuakeValue) -> None:
360
+ """Handle an RY gate operation."""
361
+ kernel.ry(*gate.parameter_values, qubit)
362
+
363
+ @staticmethod
364
+ def _handle_RZ(kernel: cudaq.Kernel, gate: RZ, qubit: cudaq.QuakeValue) -> None:
365
+ """Handle an RZ gate operation."""
366
+ kernel.rz(*gate.parameter_values, qubit)
367
+
368
+ @staticmethod
369
+ def _handle_U1(kernel: cudaq.Kernel, gate: U1, qubit: cudaq.QuakeValue) -> None:
370
+ """Handle an U1 gate operation."""
371
+ kernel.u3(theta=0.0, phi=gate.phi, delta=0.0, target=qubit)
372
+
373
+ @staticmethod
374
+ def _handle_U2(kernel: cudaq.Kernel, gate: U2, qubit: cudaq.QuakeValue) -> None:
375
+ """Handle an U2 gate operation."""
376
+ kernel.u3(theta=np.pi / 2, phi=gate.phi, delta=gate.gamma, target=qubit)
377
+
378
+ @staticmethod
379
+ def _handle_U3(kernel: cudaq.Kernel, gate: U3, qubit: cudaq.QuakeValue) -> None:
380
+ """Handle an U3 gate operation."""
381
+ kernel.u3(theta=gate.theta, phi=gate.phi, delta=gate.gamma, target=qubit)
382
+ kernel.u3(theta=gate.theta, phi=gate.phi, delta=gate.gamma, target=qubit)
383
+
384
+ @staticmethod
385
+ def _handle_PauliX(operator: PauliX) -> ElementaryOperator:
386
+ return spin.x(target=operator.qubit)
387
+
388
+ @staticmethod
389
+ def _handle_PauliY(operator: PauliY) -> ElementaryOperator:
390
+ return spin.y(target=operator.qubit)
391
+
392
+ @staticmethod
393
+ def _handle_PauliZ(operator: PauliZ) -> ElementaryOperator:
394
+ return spin.z(target=operator.qubit)
395
+
396
+ @staticmethod
397
+ def _handle_PauliI(operator: PauliI) -> ElementaryOperator:
398
+ return spin.i(target=operator.qubit)
@@ -0,0 +1,19 @@
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 qilisdk.digital.digital_result import DigitalResult
15
+ from qilisdk.yaml import yaml
16
+
17
+
18
+ @yaml.register_class
19
+ class CudaDigitalResult(DigitalResult): ...
@@ -0,0 +1,13 @@
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.