qilisdk 0.1.4__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 +152 -159
  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.4.dist-info → qilisdk-0.1.5.dist-info}/METADATA +180 -134
  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 -596
  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.4.dist-info/RECORD +0 -51
  82. {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/WHEEL +0 -0
  83. {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/licenses/LICENCE +0 -0
@@ -0,0 +1,45 @@
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
+ import sys
15
+
16
+ from qilisdk._optionals import ImportedFeature, OptionalFeature, Symbol, import_optional_dependencies
17
+
18
+ __all__ = []
19
+
20
+ OPTIONAL_FEATURES: list[OptionalFeature] = [
21
+ OptionalFeature(
22
+ name="cuda",
23
+ dependencies=["cuda-quantum-cu12"],
24
+ symbols=[
25
+ Symbol(path="qilisdk.backends.cuda_backend", name="CudaBackend"),
26
+ Symbol(path="qilisdk.backends.cuda_backend", name="CudaSamplingMethod"),
27
+ ],
28
+ ),
29
+ OptionalFeature(
30
+ name="qutip",
31
+ dependencies=["qutip", "qutip-qip", "matplotlib"],
32
+ symbols=[
33
+ Symbol(path="qilisdk.backends.qutip_backend", name="QutipBackend"),
34
+ ],
35
+ ),
36
+ ]
37
+
38
+ current_module = sys.modules[__name__]
39
+
40
+ # Dynamically import (or stub) each feature's symbols and attach them
41
+ for feature in OPTIONAL_FEATURES:
42
+ imported_feature: ImportedFeature = import_optional_dependencies(feature)
43
+ for symbol_name, symbol_obj in imported_feature.symbols.items():
44
+ setattr(current_module, symbol_name, symbol_obj)
45
+ __all__ += [symbol_name] # noqa: PLE0604
@@ -11,10 +11,8 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
- from qilisdk.common.algorithm import Algorithm
15
14
 
15
+ from .cuda_backend import CudaBackend, CudaSamplingMethod
16
+ from .qutip_backend import QutipBackend
16
17
 
17
- class DigitalAlgorithm(Algorithm):
18
- """
19
- Abstract base class for digital quantum algorithms.
20
- """
18
+ __all__ = ["CudaBackend", "CudaSamplingMethod", "QutipBackend"]
@@ -0,0 +1,117 @@
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 abc import ABC
17
+ from typing import TYPE_CHECKING, Callable, TypeVar, cast, overload
18
+
19
+ from qilisdk.functionals.functional_result import FunctionalResult
20
+ from qilisdk.functionals.sampling import Sampling
21
+ from qilisdk.functionals.time_evolution import TimeEvolution
22
+ from qilisdk.functionals.variational_program import VariationalProgram
23
+ from qilisdk.functionals.variational_program_result import VariationalProgramResult
24
+
25
+ if TYPE_CHECKING:
26
+ from qilisdk.functionals.functional import Functional, PrimitiveFunctional
27
+ from qilisdk.functionals.sampling_result import SamplingResult
28
+ from qilisdk.functionals.time_evolution_result import TimeEvolutionResult
29
+
30
+ TResult = TypeVar("TResult", bound=FunctionalResult)
31
+
32
+
33
+ class Backend(ABC):
34
+ def __init__(self) -> None:
35
+ self._handlers: dict[type[Functional], Callable[[Functional], FunctionalResult]] = {
36
+ Sampling: lambda f: self._execute_sampling(cast("Sampling", f)),
37
+ TimeEvolution: lambda f: self._execute_time_evolution(cast("TimeEvolution", f)),
38
+ VariationalProgram: lambda f: self._execute_variational_program(cast("VariationalProgram", f)),
39
+ }
40
+
41
+ @overload
42
+ def execute(self, functional: Sampling) -> SamplingResult: ...
43
+
44
+ @overload
45
+ def execute(self, functional: TimeEvolution) -> TimeEvolutionResult: ...
46
+
47
+ @overload
48
+ def execute(self, functional: VariationalProgram[Sampling]) -> VariationalProgramResult[SamplingResult]: ...
49
+
50
+ @overload
51
+ def execute(
52
+ self, functional: VariationalProgram[TimeEvolution]
53
+ ) -> VariationalProgramResult[TimeEvolutionResult]: ...
54
+
55
+ @overload
56
+ def execute(self, functional: PrimitiveFunctional[TResult]) -> TResult: ...
57
+
58
+ def execute(self, functional: Functional) -> FunctionalResult:
59
+ try:
60
+ handler = self._handlers[type(functional)]
61
+ except KeyError as exc:
62
+ raise NotImplementedError(
63
+ f"{type(self).__qualname__} does not support {type(functional).__qualname__}"
64
+ ) from exc
65
+
66
+ return handler(functional)
67
+
68
+ def _execute_sampling(self, functional: Sampling) -> SamplingResult:
69
+ raise NotImplementedError(f"{type(self).__qualname__} has no Sampling implementation")
70
+
71
+ def _execute_time_evolution(self, functional: TimeEvolution) -> TimeEvolutionResult:
72
+ raise NotImplementedError(f"{type(self).__qualname__} has no TimeEvolution implementation")
73
+
74
+ def _execute_variational_program(
75
+ self, functional: VariationalProgram[PrimitiveFunctional[TResult]]
76
+ ) -> VariationalProgramResult[TResult]:
77
+ """Optimize a Parameterized Program (:class:`~qilisdk.functionals.variational_program.VariationalProgram`)
78
+ and returns the optimal parameters and results.
79
+
80
+ Args:
81
+ functional (VariationalProgram): The variational program to be optimized.
82
+
83
+ Returns:
84
+ ParameterizedProgramResults: The final optimizer and functional results.
85
+
86
+ Raises:
87
+ ValueError: If the functional is not parameterized.
88
+ """
89
+
90
+ def evaluate_sample(parameters: list[float]) -> float:
91
+ param_names = functional.functional.get_parameter_names()
92
+ functional.functional.set_parameters({param_names[i]: param for i, param in enumerate(parameters)})
93
+ results = self.execute(functional.functional)
94
+ final_results = functional.cost_function.compute_cost(results)
95
+ if isinstance(final_results, float):
96
+ return final_results
97
+ if isinstance(final_results, complex) and final_results.imag == 0:
98
+ return final_results.real
99
+ raise ValueError(f"Unsupported result type {type(final_results)}.")
100
+
101
+ if len(functional.functional.get_parameters()) == 0:
102
+ raise ValueError("Functional provided is not parameterized.")
103
+
104
+ optimizer_result = functional.optimizer.optimize(
105
+ cost_function=evaluate_sample,
106
+ init_parameters=list(functional.functional.get_parameters().values()),
107
+ bounds=list(functional.functional.get_parameter_bounds().values()),
108
+ store_intermediate_results=functional.store_intermediate_results,
109
+ )
110
+
111
+ param_names = functional.functional.get_parameter_names()
112
+ functional.functional.set_parameters(
113
+ {param_names[i]: param for i, param in enumerate(optimizer_result.optimal_parameters)}
114
+ )
115
+ optimal_results: TResult = cast("TResult", self.execute(functional.functional))
116
+
117
+ return VariationalProgramResult(optimizer_result=optimizer_result, result=optimal_results)
@@ -13,41 +13,27 @@
13
13
  # limitations under the License.
14
14
  from __future__ import annotations
15
15
 
16
+ from enum import Enum
16
17
  from typing import TYPE_CHECKING, Callable, Type, TypeVar
17
18
 
18
19
  import cudaq
19
20
  import numpy as np
20
21
  from cudaq import ElementaryOperator, OperatorSum, ScalarOperator, State, evolve, spin
21
- from cudaq import Schedule as cuda_schedule
22
+ from cudaq import Schedule as CudaSchedule
23
+ from loguru import logger
22
24
 
23
- from qilisdk.analog.analog_backend import AnalogBackend
24
25
  from qilisdk.analog.hamiltonian import Hamiltonian, PauliI, PauliOperator, PauliX, PauliY, PauliZ
25
- from qilisdk.analog.quantum_objects import QuantumObject
26
- from qilisdk.digital import (
27
- RX,
28
- RY,
29
- RZ,
30
- U1,
31
- U2,
32
- U3,
33
- Circuit,
34
- H,
35
- M,
36
- S,
37
- T,
38
- X,
39
- Y,
40
- Z,
41
- )
42
- from qilisdk.digital.digital_backend import DigitalBackend, DigitalSimulationMethod
26
+ from qilisdk.backends.backend import Backend
27
+ from qilisdk.common.qtensor import QTensor
43
28
  from qilisdk.digital.exceptions import UnsupportedGateError
44
- from qilisdk.digital.gates import Adjoint, BasicGate, Controlled
45
-
46
- from .cuda_analog_result import CudaAnalogResult
47
- from .cuda_digital_result import CudaDigitalResult
29
+ from qilisdk.digital.gates import RX, RY, RZ, SWAP, U1, U2, U3, Adjoint, BasicGate, Controlled, H, I, M, S, T, X, Y, Z
30
+ from qilisdk.functionals.sampling_result import SamplingResult
31
+ from qilisdk.functionals.time_evolution_result import TimeEvolutionResult
48
32
 
49
33
  if TYPE_CHECKING:
50
- from qilisdk.analog.schedule import Schedule
34
+ from qilisdk.digital.circuit import Circuit
35
+ from qilisdk.functionals.sampling import Sampling
36
+ from qilisdk.functionals.time_evolution import TimeEvolution
51
37
 
52
38
 
53
39
  TBasicGate = TypeVar("TBasicGate", bound=BasicGate)
@@ -57,9 +43,19 @@ TPauliOperator = TypeVar("TPauliOperator", bound=PauliOperator)
57
43
  PauliOperatorHandlersMapping = dict[Type[TPauliOperator], Callable[[TPauliOperator], ElementaryOperator]]
58
44
 
59
45
 
60
- class CudaBackend(DigitalBackend, AnalogBackend):
46
+ class CudaSamplingMethod(str, Enum):
47
+ """
48
+ Enumeration of available simulation methods for the CUDA backend.
61
49
  """
62
- Digital backend implementation using CUDA-based simulation.
50
+
51
+ STATE_VECTOR = "state_vector"
52
+ TENSOR_NETWORK = "tensor_network"
53
+ MATRIX_PRODUCT_STATE = "matrix_product_state"
54
+
55
+
56
+ class CudaBackend(Backend):
57
+ """
58
+ Backend implementation using CUDA-based simulation.
63
59
 
64
60
  This backend translates a quantum circuit into a CUDA-compatible kernel and executes it
65
61
  using the cudaq library. It supports different simulation methods including state vector,
@@ -67,19 +63,19 @@ class CudaBackend(DigitalBackend, AnalogBackend):
67
63
  mapped to CUDA operations via dedicated handler functions.
68
64
  """
69
65
 
70
- def __init__(
71
- self, digital_simulation_method: DigitalSimulationMethod = DigitalSimulationMethod.STATE_VECTOR
72
- ) -> None:
66
+ def __init__(self, sampling_method: CudaSamplingMethod = CudaSamplingMethod.STATE_VECTOR) -> None:
73
67
  """
74
68
  Initialize the CudaBackend.
75
69
 
76
70
  Args:
77
- digital_simulation_method (SimulationMethod, optional): The simulation method to use for executing circuits.
71
+ sampling_method (CudaSamplingMethod, optional): The simulation method to use for sampling circuits.
78
72
  Options include STATE_VECTOR, TENSOR_NETWORK, or MATRIX_PRODUCT_STATE.
79
73
  Defaults to STATE_VECTOR.
80
74
  """
81
- super().__init__(digital_simulation_method=digital_simulation_method)
75
+ super().__init__()
76
+ cudaq.register_operation("i", np.array([1, 0, 0, 1]))
82
77
  self._basic_gate_handlers: BasicGateHandlersMapping = {
78
+ I: CudaBackend._handle_I,
83
79
  X: CudaBackend._handle_X,
84
80
  Y: CudaBackend._handle_Y,
85
81
  Z: CudaBackend._handle_Z,
@@ -92,6 +88,7 @@ class CudaBackend(DigitalBackend, AnalogBackend):
92
88
  U1: CudaBackend._handle_U1,
93
89
  U2: CudaBackend._handle_U2,
94
90
  U3: CudaBackend._handle_U3,
91
+ SWAP: CudaBackend._handle_SWAP, # type: ignore[dict-item]
95
92
  }
96
93
  self._pauli_operator_handlers: PauliOperatorHandlersMapping = {
97
94
  PauliX: CudaBackend._handle_PauliX,
@@ -99,6 +96,18 @@ class CudaBackend(DigitalBackend, AnalogBackend):
99
96
  PauliZ: CudaBackend._handle_PauliZ,
100
97
  PauliI: CudaBackend._handle_PauliI,
101
98
  }
99
+ self._sampling_method = sampling_method
100
+ logger.success("CudaBackend initialised (sampling_method={})", sampling_method.value)
101
+
102
+ @property
103
+ def sampling_method(self) -> CudaSamplingMethod:
104
+ """
105
+ Get the simulation method currently configured for the backend.
106
+
107
+ Returns:
108
+ SimulationMethod: The simulation method to be used for circuit execution.
109
+ """
110
+ return self._sampling_method
102
111
 
103
112
  def _apply_digital_simulation_method(self) -> None:
104
113
  """
@@ -107,53 +116,110 @@ class CudaBackend(DigitalBackend, AnalogBackend):
107
116
  For the STATE_VECTOR method, it checks for GPU availability and selects an appropriate target.
108
117
  For TENSOR_NETWORK and MATRIX_PRODUCT_STATE methods, it explicitly sets the target to use tensor network-based simulations.
109
118
  """
110
- if self.digital_simulation_method == DigitalSimulationMethod.STATE_VECTOR:
119
+ logger.info("Applying sampling simulation method {}", self.sampling_method.value)
120
+ if self.sampling_method == CudaSamplingMethod.STATE_VECTOR:
111
121
  if cudaq.num_available_gpus() == 0:
112
122
  cudaq.set_target("qpp-cpu")
123
+ logger.debug("No GPU detected, using cudaq's 'qpp-cpu' backend")
113
124
  else:
114
125
  cudaq.set_target("nvidia")
115
- elif self.digital_simulation_method == DigitalSimulationMethod.TENSOR_NETWORK:
126
+ logger.debug("GPU detected, using cudaq's 'nvidia' backend")
127
+ elif self.sampling_method == CudaSamplingMethod.TENSOR_NETWORK:
116
128
  cudaq.set_target("tensornet")
129
+ logger.debug("Using cudaq's 'tensornet' backend")
117
130
  else:
118
131
  cudaq.set_target("tensornet-mps")
132
+ logger.debug("Using cudaq's 'tensornet-mps' backend")
119
133
 
120
- def execute(self, circuit: Circuit, nshots: int = 1000) -> CudaDigitalResult:
121
- """
122
- Execute a quantum circuit and return the measurement results.
123
-
124
- This method applies the selected simulation method, translates the circuit's gates into
125
- CUDA operations via their respective handlers, runs the simulation, and returns the result
126
- as a CudaDigitalResult.
127
-
128
- Args:
129
- circuit (Circuit): The quantum circuit to be executed.
130
- nshots (int, optional): The number of measurement shots to perform. Defaults to 1000.
131
-
132
- Returns:
133
- DigitalResult: A result object containing the measurement samples and computed probabilities.
134
-
135
- Raises:
136
- UnsupportedGateError: If the circuit contains a gate for which no handler is registered.
137
- """
134
+ def _execute_sampling(self, functional: Sampling) -> SamplingResult:
135
+ logger.info("Executing Sampling (shots={})", functional.nshots)
138
136
  self._apply_digital_simulation_method()
139
137
  kernel = cudaq.make_kernel()
140
- qubits = kernel.qalloc(circuit.nqubits)
138
+ qubits = kernel.qalloc(functional.circuit.nqubits)
141
139
 
142
- for gate in circuit.gates:
140
+ for gate in functional.circuit.gates:
143
141
  if isinstance(gate, Controlled):
144
142
  self._handle_controlled(kernel, gate, qubits[gate.control_qubits[0]], qubits[gate.target_qubits[0]])
145
143
  elif isinstance(gate, Adjoint):
146
144
  self._handle_adjoint(kernel, gate, qubits[gate.target_qubits[0]])
147
145
  elif isinstance(gate, M):
148
- self._handle_M(kernel, gate, circuit, qubits)
146
+ self._handle_M(kernel, gate, functional.circuit, qubits)
149
147
  else:
150
148
  handler = self._basic_gate_handlers.get(type(gate), None)
151
149
  if handler is None:
152
150
  raise UnsupportedGateError
153
- handler(kernel, gate, qubits[gate.target_qubits[0]])
151
+ handler(kernel, gate, *(qubits[gate.target_qubits[i]] for i in range(len(gate.target_qubits))))
152
+
153
+ cudaq_result = cudaq.sample(kernel, shots_count=functional.nshots)
154
+ logger.success("Sampling finished; {} distinct bitstrings", len(cudaq_result))
155
+ return SamplingResult(nshots=functional.nshots, samples=dict(cudaq_result.items()))
156
+
157
+ def _execute_time_evolution(self, functional: TimeEvolution) -> TimeEvolutionResult:
158
+ logger.info("Executing TimeEvolution (T={}, dt={})", functional.schedule.T, functional.schedule.dt)
159
+ cudaq.set_target("dynamics")
160
+
161
+ steps = np.linspace(0, functional.schedule.T, (round(functional.schedule.T / functional.schedule.dt) + 1))
162
+
163
+ cuda_schedule = CudaSchedule(steps, ["t"])
164
+
165
+ def get_schedule(key: str) -> Callable:
166
+ return lambda t: (functional.schedule.get_coefficient(t.real, key))
154
167
 
155
- cudaq_result = cudaq.sample(kernel, shots_count=nshots)
156
- return CudaDigitalResult(nshots=nshots, samples=dict(cudaq_result.items()))
168
+ cuda_hamiltonian = sum(
169
+ ScalarOperator(get_schedule(key)) * self._hamiltonian_to_cuda(ham)
170
+ for key, ham in functional.schedule.hamiltonians.items()
171
+ )
172
+
173
+ logger.trace("Hamiltonian compiled for evolution")
174
+
175
+ cuda_observables = []
176
+ for observable in functional.observables:
177
+ if isinstance(observable, PauliOperator):
178
+ cuda_observables.append(self._pauli_operator_handlers[type(observable)](observable))
179
+ elif isinstance(observable, Hamiltonian):
180
+ cuda_observables.append(self._hamiltonian_to_cuda(observable))
181
+ else:
182
+ logger.error("Unsupported observable type {}", observable.__class__.__name__)
183
+ raise ValueError(f"unsupported observable type of {observable.__class__}")
184
+ logger.trace("Observables compiled for evolution")
185
+
186
+ evolution_result = evolve(
187
+ hamiltonian=cuda_hamiltonian,
188
+ dimensions=dict.fromkeys(range(functional.schedule.nqubits), 2),
189
+ schedule=cuda_schedule,
190
+ initial_state=State.from_data(np.array(functional.initial_state.unit().dense, dtype=np.complex128)),
191
+ observables=cuda_observables,
192
+ collapse_operators=[],
193
+ store_intermediate_results=functional.store_intermediate_results,
194
+ )
195
+
196
+ logger.success("TimeEvolution finished")
197
+
198
+ final_expected_values = np.array(
199
+ [exp_val.expectation() for exp_val in evolution_result.final_expectation_values()]
200
+ )
201
+ expected_values = (
202
+ np.array([[val.expectation() for val in exp_vals] for exp_vals in evolution_result.expectation_values()])
203
+ if evolution_result.expectation_values() is not None and functional.store_intermediate_results
204
+ else None
205
+ )
206
+ final_state = (
207
+ QTensor(np.array(evolution_result.final_state()).reshape(-1, 1))
208
+ if evolution_result.final_state() is not None
209
+ else None
210
+ )
211
+ intermediate_states = (
212
+ [QTensor(np.array(state).reshape(-1, 1)) for state in evolution_result.intermediate_states()]
213
+ if evolution_result.intermediate_states() is not None and functional.store_intermediate_results
214
+ else None
215
+ )
216
+
217
+ return TimeEvolutionResult(
218
+ final_expected_values=final_expected_values,
219
+ expected_values=expected_values,
220
+ final_state=final_state,
221
+ intermediate_states=intermediate_states,
222
+ )
157
223
 
158
224
  def _handle_controlled(
159
225
  self, kernel: cudaq.Kernel, gate: Controlled, control_qubit: cudaq.QuakeValue, target_qubit: cudaq.QuakeValue
@@ -174,10 +240,12 @@ class CudaBackend(DigitalBackend, AnalogBackend):
174
240
  UnsupportedGateError: If the number of control qubits is not equal to one or if the basic gate is unsupported.
175
241
  """
176
242
  if len(gate.control_qubits) != 1:
243
+ logger.error("Controlled gate with {} control qubits not supported", len(gate.control_qubits))
177
244
  raise UnsupportedGateError
178
245
  target_kernel, qubit = cudaq.make_kernel(cudaq.qubit)
179
246
  handler = self._basic_gate_handlers.get(type(gate.basic_gate), None)
180
247
  if handler is None:
248
+ logger.error("Unsupported gate inside Controlled: {}", type(gate.basic_gate).__name__)
181
249
  raise UnsupportedGateError
182
250
  handler(target_kernel, gate.basic_gate, qubit)
183
251
  kernel.control(target_kernel, control_qubit, target_qubit)
@@ -200,103 +268,11 @@ class CudaBackend(DigitalBackend, AnalogBackend):
200
268
  target_kernel, qubit = cudaq.make_kernel(cudaq.qubit)
201
269
  handler = self._basic_gate_handlers.get(type(gate.basic_gate), None)
202
270
  if handler is None:
271
+ logger.error("Unsupported gate inside Adjoint: {}", type(gate.basic_gate).__name__)
203
272
  raise UnsupportedGateError
204
273
  handler(target_kernel, gate.basic_gate, qubit)
205
274
  kernel.adjoint(target_kernel, target_qubit)
206
275
 
207
- def _hamiltonian_to_cuda(self, hamiltonian: Hamiltonian) -> OperatorSum:
208
- out = None
209
- for offset, terms in hamiltonian:
210
- if out is None:
211
- out = offset * np.prod([self._pauli_operator_handlers[type(pauli)](pauli) for pauli in terms])
212
- else:
213
- out += offset * np.prod([self._pauli_operator_handlers[type(pauli)](pauli) for pauli in terms])
214
- return out
215
-
216
- def evolve(
217
- self,
218
- schedule: Schedule,
219
- initial_state: QuantumObject,
220
- observables: list[PauliOperator | Hamiltonian],
221
- store_intermediate_results: bool = False,
222
- ) -> CudaAnalogResult:
223
- """computes the time evolution under of an initial state under the given schedule.
224
-
225
- Args:
226
- schedule (Schedule): The evolution schedule of the system.
227
- initial_state (QuantumObject): the initial state of the evolution.
228
- observables (list[PauliOperator | Hamiltonian]): the list of observables to be measured at the end of the evolution.
229
- store_intermediate_results (bool): A flag to store the intermediate results along the evolution.
230
-
231
- Raises:
232
- ValueError: if the observables provided are not an instance of the PauliOperator or a Hamiltonian class.
233
-
234
- Returns:
235
- AnalogResult: The results of the evolution.
236
- """
237
- cudaq.set_target("dynamics")
238
-
239
- cuda_hamiltonian = None
240
- steps = np.linspace(0, schedule.T, int(schedule.T / schedule.dt))
241
-
242
- def parameter_values(time_steps: np.ndarray) -> cuda_schedule:
243
- def compute_value(param_name: str, step_idx: int) -> float:
244
- return schedule.get_coefficient(time_steps[int(step_idx)], param_name)
245
-
246
- return cuda_schedule(list(range(len(time_steps))), list(schedule.hamiltonians), compute_value)
247
-
248
- _cuda_schedule = parameter_values(steps)
249
-
250
- def get_schedule(key: str) -> Callable:
251
- return lambda **args: args[key]
252
-
253
- cuda_hamiltonian = sum(
254
- ScalarOperator(get_schedule(key)) * self._hamiltonian_to_cuda(ham)
255
- for key, ham in schedule.hamiltonians.items()
256
- )
257
-
258
- cuda_observables = []
259
- for observable in observables:
260
- if isinstance(observable, PauliOperator):
261
- cuda_observables.append(self._pauli_operator_handlers[type(observable)](observable))
262
- elif isinstance(observable, Hamiltonian):
263
- cuda_observables.append(self._hamiltonian_to_cuda(observable))
264
- else:
265
- raise ValueError(f"unsupported observable type of {observable.__class__}")
266
-
267
- evolution_result = evolve(
268
- hamiltonian=cuda_hamiltonian,
269
- dimensions=dict.fromkeys(range(schedule.nqubits), 2),
270
- schedule=_cuda_schedule,
271
- initial_state=State.from_data(np.array(initial_state.to_density_matrix().dense, dtype=np.complex128)),
272
- observables=cuda_observables,
273
- collapse_operators=[],
274
- store_intermediate_results=store_intermediate_results,
275
- )
276
-
277
- return CudaAnalogResult(
278
- final_expected_values=np.array(
279
- [exp_val.expectation() for exp_val in evolution_result.final_expectation_values()[0]]
280
- ),
281
- expected_values=(
282
- np.array(
283
- [[val.expectation() for val in exp_vals] for exp_vals in evolution_result.expectation_values()]
284
- )
285
- if evolution_result.expectation_values() is not None
286
- else None
287
- ),
288
- final_state=(
289
- QuantumObject(np.array(evolution_result.final_state())).adjoint()
290
- if evolution_result.final_state() is not None
291
- else None
292
- ),
293
- intermediate_states=(
294
- [QuantumObject(np.array(state)).adjoint() for state in evolution_result.intermediate_states()]
295
- if evolution_result.intermediate_states() is not None
296
- else None
297
- ),
298
- )
299
-
300
276
  @staticmethod
301
277
  def _handle_M(kernel: cudaq.Kernel, gate: M, circuit: Circuit, qubits: cudaq.QuakeValue) -> None:
302
278
  """
@@ -317,6 +293,11 @@ class CudaBackend(DigitalBackend, AnalogBackend):
317
293
  for idx in gate.target_qubits:
318
294
  kernel.mz(qubits[idx])
319
295
 
296
+ @staticmethod
297
+ def _handle_I(kernel: cudaq.Kernel, gate: I, qubit: cudaq.QuakeValue) -> None:
298
+ """Handle an X gate operation."""
299
+ kernel.i(qubit)
300
+
320
301
  @staticmethod
321
302
  def _handle_X(kernel: cudaq.Kernel, gate: X, qubit: cudaq.QuakeValue) -> None:
322
303
  """Handle an X gate operation."""
@@ -350,17 +331,17 @@ class CudaBackend(DigitalBackend, AnalogBackend):
350
331
  @staticmethod
351
332
  def _handle_RX(kernel: cudaq.Kernel, gate: RX, qubit: cudaq.QuakeValue) -> None:
352
333
  """Handle an RX gate operation."""
353
- kernel.rx(*gate.parameter_values, qubit)
334
+ kernel.rx(*gate.get_parameter_values(), qubit)
354
335
 
355
336
  @staticmethod
356
337
  def _handle_RY(kernel: cudaq.Kernel, gate: RY, qubit: cudaq.QuakeValue) -> None:
357
338
  """Handle an RY gate operation."""
358
- kernel.ry(*gate.parameter_values, qubit)
339
+ kernel.ry(*gate.get_parameter_values(), qubit)
359
340
 
360
341
  @staticmethod
361
342
  def _handle_RZ(kernel: cudaq.Kernel, gate: RZ, qubit: cudaq.QuakeValue) -> None:
362
343
  """Handle an RZ gate operation."""
363
- kernel.rz(*gate.parameter_values, qubit)
344
+ kernel.rz(*gate.get_parameter_values(), qubit)
364
345
 
365
346
  @staticmethod
366
347
  def _handle_U1(kernel: cudaq.Kernel, gate: U1, qubit: cudaq.QuakeValue) -> None:
@@ -376,20 +357,32 @@ class CudaBackend(DigitalBackend, AnalogBackend):
376
357
  def _handle_U3(kernel: cudaq.Kernel, gate: U3, qubit: cudaq.QuakeValue) -> None:
377
358
  """Handle an U3 gate operation."""
378
359
  kernel.u3(theta=gate.theta, phi=gate.phi, delta=gate.gamma, target=qubit)
379
- kernel.u3(theta=gate.theta, phi=gate.phi, delta=gate.gamma, target=qubit)
380
360
 
381
361
  @staticmethod
382
- def _handle_PauliX(operator: PauliX) -> ElementaryOperator:
362
+ def _handle_SWAP(kernel: cudaq.Kernel, gate: SWAP, qubit_0: cudaq.QuakeValue, qubit_1: cudaq.QuakeValue) -> None:
363
+ kernel.swap(qubit_0, qubit_1)
364
+
365
+ def _hamiltonian_to_cuda(self, hamiltonian: Hamiltonian, padding: int = 0) -> OperatorSum: # type: ignore
366
+ out = None
367
+ for offset, terms in hamiltonian:
368
+ if out is None:
369
+ out = offset * np.prod([self._pauli_operator_handlers[type(pauli)](pauli) for pauli in terms])
370
+ else:
371
+ out += offset * np.prod([self._pauli_operator_handlers[type(pauli)](pauli) for pauli in terms])
372
+ return out
373
+
374
+ @staticmethod
375
+ def _handle_PauliX(operator: PauliX) -> ElementaryOperator: # type: ignore
383
376
  return spin.x(target=operator.qubit)
384
377
 
385
378
  @staticmethod
386
- def _handle_PauliY(operator: PauliY) -> ElementaryOperator:
379
+ def _handle_PauliY(operator: PauliY) -> ElementaryOperator: # type: ignore
387
380
  return spin.y(target=operator.qubit)
388
381
 
389
382
  @staticmethod
390
- def _handle_PauliZ(operator: PauliZ) -> ElementaryOperator:
383
+ def _handle_PauliZ(operator: PauliZ) -> ElementaryOperator: # type: ignore
391
384
  return spin.z(target=operator.qubit)
392
385
 
393
386
  @staticmethod
394
- def _handle_PauliI(operator: PauliI) -> ElementaryOperator:
387
+ def _handle_PauliI(operator: PauliI) -> ElementaryOperator: # type: ignore
395
388
  return spin.i(target=operator.qubit)