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,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,42 +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
- from cudaq import State
21
- from cudaq.operator import ElementaryOperator, OperatorSum, ScalarOperator, evolve, spin
22
- from cudaq.operator import Schedule as cuda_schedule
21
+ from cudaq import ElementaryOperator, OperatorSum, ScalarOperator, State, evolve, spin
22
+ from cudaq import Schedule as CudaSchedule
23
+ from loguru import logger
23
24
 
24
- from qilisdk.analog.analog_backend import AnalogBackend
25
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
- Circuit,
35
- H,
36
- M,
37
- S,
38
- T,
39
- X,
40
- Y,
41
- Z,
42
- )
43
- from qilisdk.digital.digital_backend import DigitalBackend, DigitalSimulationMethod
26
+ from qilisdk.backends.backend import Backend
27
+ from qilisdk.common.qtensor import QTensor
44
28
  from qilisdk.digital.exceptions import UnsupportedGateError
45
- from qilisdk.digital.gates import Adjoint, BasicGate, Controlled
46
-
47
- from .cuda_analog_result import CudaAnalogResult
48
- 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
49
32
 
50
33
  if TYPE_CHECKING:
51
- 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
52
37
 
53
38
 
54
39
  TBasicGate = TypeVar("TBasicGate", bound=BasicGate)
@@ -58,9 +43,19 @@ TPauliOperator = TypeVar("TPauliOperator", bound=PauliOperator)
58
43
  PauliOperatorHandlersMapping = dict[Type[TPauliOperator], Callable[[TPauliOperator], ElementaryOperator]]
59
44
 
60
45
 
61
- class CudaBackend(DigitalBackend, AnalogBackend):
46
+ class CudaSamplingMethod(str, Enum):
47
+ """
48
+ Enumeration of available simulation methods for the CUDA backend.
62
49
  """
63
- 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.
64
59
 
65
60
  This backend translates a quantum circuit into a CUDA-compatible kernel and executes it
66
61
  using the cudaq library. It supports different simulation methods including state vector,
@@ -68,19 +63,19 @@ class CudaBackend(DigitalBackend, AnalogBackend):
68
63
  mapped to CUDA operations via dedicated handler functions.
69
64
  """
70
65
 
71
- def __init__(
72
- self, digital_simulation_method: DigitalSimulationMethod = DigitalSimulationMethod.STATE_VECTOR
73
- ) -> None:
66
+ def __init__(self, sampling_method: CudaSamplingMethod = CudaSamplingMethod.STATE_VECTOR) -> None:
74
67
  """
75
68
  Initialize the CudaBackend.
76
69
 
77
70
  Args:
78
- 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.
79
72
  Options include STATE_VECTOR, TENSOR_NETWORK, or MATRIX_PRODUCT_STATE.
80
73
  Defaults to STATE_VECTOR.
81
74
  """
82
- super().__init__(digital_simulation_method=digital_simulation_method)
75
+ super().__init__()
76
+ cudaq.register_operation("i", np.array([1, 0, 0, 1]))
83
77
  self._basic_gate_handlers: BasicGateHandlersMapping = {
78
+ I: CudaBackend._handle_I,
84
79
  X: CudaBackend._handle_X,
85
80
  Y: CudaBackend._handle_Y,
86
81
  Z: CudaBackend._handle_Z,
@@ -93,6 +88,7 @@ class CudaBackend(DigitalBackend, AnalogBackend):
93
88
  U1: CudaBackend._handle_U1,
94
89
  U2: CudaBackend._handle_U2,
95
90
  U3: CudaBackend._handle_U3,
91
+ SWAP: CudaBackend._handle_SWAP, # type: ignore[dict-item]
96
92
  }
97
93
  self._pauli_operator_handlers: PauliOperatorHandlersMapping = {
98
94
  PauliX: CudaBackend._handle_PauliX,
@@ -100,6 +96,18 @@ class CudaBackend(DigitalBackend, AnalogBackend):
100
96
  PauliZ: CudaBackend._handle_PauliZ,
101
97
  PauliI: CudaBackend._handle_PauliI,
102
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
103
111
 
104
112
  def _apply_digital_simulation_method(self) -> None:
105
113
  """
@@ -108,53 +116,110 @@ class CudaBackend(DigitalBackend, AnalogBackend):
108
116
  For the STATE_VECTOR method, it checks for GPU availability and selects an appropriate target.
109
117
  For TENSOR_NETWORK and MATRIX_PRODUCT_STATE methods, it explicitly sets the target to use tensor network-based simulations.
110
118
  """
111
- 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:
112
121
  if cudaq.num_available_gpus() == 0:
113
122
  cudaq.set_target("qpp-cpu")
123
+ logger.debug("No GPU detected, using cudaq's 'qpp-cpu' backend")
114
124
  else:
115
125
  cudaq.set_target("nvidia")
116
- 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:
117
128
  cudaq.set_target("tensornet")
129
+ logger.debug("Using cudaq's 'tensornet' backend")
118
130
  else:
119
131
  cudaq.set_target("tensornet-mps")
132
+ logger.debug("Using cudaq's 'tensornet-mps' backend")
120
133
 
121
- def execute(self, circuit: Circuit, nshots: int = 1000) -> CudaDigitalResult:
122
- """
123
- Execute a quantum circuit and return the measurement results.
124
-
125
- This method applies the selected simulation method, translates the circuit's gates into
126
- CUDA operations via their respective handlers, runs the simulation, and returns the result
127
- as a CudaDigitalResult.
128
-
129
- Args:
130
- circuit (Circuit): The quantum circuit to be executed.
131
- nshots (int, optional): The number of measurement shots to perform. Defaults to 1000.
132
-
133
- Returns:
134
- DigitalResult: A result object containing the measurement samples and computed probabilities.
135
-
136
- Raises:
137
- UnsupportedGateError: If the circuit contains a gate for which no handler is registered.
138
- """
134
+ def _execute_sampling(self, functional: Sampling) -> SamplingResult:
135
+ logger.info("Executing Sampling (shots={})", functional.nshots)
139
136
  self._apply_digital_simulation_method()
140
137
  kernel = cudaq.make_kernel()
141
- qubits = kernel.qalloc(circuit.nqubits)
138
+ qubits = kernel.qalloc(functional.circuit.nqubits)
142
139
 
143
- for gate in circuit.gates:
140
+ for gate in functional.circuit.gates:
144
141
  if isinstance(gate, Controlled):
145
142
  self._handle_controlled(kernel, gate, qubits[gate.control_qubits[0]], qubits[gate.target_qubits[0]])
146
143
  elif isinstance(gate, Adjoint):
147
144
  self._handle_adjoint(kernel, gate, qubits[gate.target_qubits[0]])
148
145
  elif isinstance(gate, M):
149
- self._handle_M(kernel, gate, circuit, qubits)
146
+ self._handle_M(kernel, gate, functional.circuit, qubits)
150
147
  else:
151
148
  handler = self._basic_gate_handlers.get(type(gate), None)
152
149
  if handler is None:
153
150
  raise UnsupportedGateError
154
- 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))
155
167
 
156
- cudaq_result = cudaq.sample(kernel, shots_count=nshots)
157
- 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
+ )
158
223
 
159
224
  def _handle_controlled(
160
225
  self, kernel: cudaq.Kernel, gate: Controlled, control_qubit: cudaq.QuakeValue, target_qubit: cudaq.QuakeValue
@@ -175,10 +240,12 @@ class CudaBackend(DigitalBackend, AnalogBackend):
175
240
  UnsupportedGateError: If the number of control qubits is not equal to one or if the basic gate is unsupported.
176
241
  """
177
242
  if len(gate.control_qubits) != 1:
243
+ logger.error("Controlled gate with {} control qubits not supported", len(gate.control_qubits))
178
244
  raise UnsupportedGateError
179
245
  target_kernel, qubit = cudaq.make_kernel(cudaq.qubit)
180
246
  handler = self._basic_gate_handlers.get(type(gate.basic_gate), None)
181
247
  if handler is None:
248
+ logger.error("Unsupported gate inside Controlled: {}", type(gate.basic_gate).__name__)
182
249
  raise UnsupportedGateError
183
250
  handler(target_kernel, gate.basic_gate, qubit)
184
251
  kernel.control(target_kernel, control_qubit, target_qubit)
@@ -201,103 +268,11 @@ class CudaBackend(DigitalBackend, AnalogBackend):
201
268
  target_kernel, qubit = cudaq.make_kernel(cudaq.qubit)
202
269
  handler = self._basic_gate_handlers.get(type(gate.basic_gate), None)
203
270
  if handler is None:
271
+ logger.error("Unsupported gate inside Adjoint: {}", type(gate.basic_gate).__name__)
204
272
  raise UnsupportedGateError
205
273
  handler(target_kernel, gate.basic_gate, qubit)
206
274
  kernel.adjoint(target_kernel, target_qubit)
207
275
 
208
- def _hamiltonian_to_cuda(self, hamiltonian: Hamiltonian) -> OperatorSum:
209
- out = None
210
- for offset, terms in hamiltonian:
211
- if out is None:
212
- out = offset * np.prod([self._pauli_operator_handlers[type(pauli)](pauli) for pauli in terms])
213
- else:
214
- out += offset * np.prod([self._pauli_operator_handlers[type(pauli)](pauli) for pauli in terms])
215
- return out
216
-
217
- def evolve(
218
- self,
219
- schedule: Schedule,
220
- initial_state: QuantumObject,
221
- observables: list[PauliOperator | Hamiltonian],
222
- store_intermediate_results: bool = False,
223
- ) -> CudaAnalogResult:
224
- """computes the time evolution under of an initial state under the given schedule.
225
-
226
- Args:
227
- schedule (Schedule): The evolution schedule of the system.
228
- initial_state (QuantumObject): the initial state of the evolution.
229
- observables (list[PauliOperator | Hamiltonian]): the list of observables to be measured at the end of the evolution.
230
- store_intermediate_results (bool): A flag to store the intermediate results along the evolution.
231
-
232
- Raises:
233
- ValueError: if the observables provided are not an instance of the PauliOperator or a Hamiltonian class.
234
-
235
- Returns:
236
- AnalogResult: The results of the evolution.
237
- """
238
- cudaq.set_target("dynamics")
239
-
240
- cuda_hamiltonian = None
241
- steps = np.linspace(0, schedule.T, int(schedule.T / schedule.dt))
242
-
243
- def parameter_values(time_steps: np.ndarray) -> cuda_schedule:
244
- def compute_value(param_name: str, step_idx: int) -> float:
245
- return schedule.get_coefficient(time_steps[int(step_idx)], param_name)
246
-
247
- return cuda_schedule(list(range(len(time_steps))), list(schedule.hamiltonians), compute_value)
248
-
249
- _cuda_schedule = parameter_values(steps)
250
-
251
- def get_schedule(key: str) -> Callable:
252
- return lambda **args: args[key]
253
-
254
- cuda_hamiltonian = sum(
255
- ScalarOperator(get_schedule(key)) * self._hamiltonian_to_cuda(ham)
256
- for key, ham in schedule.hamiltonians.items()
257
- )
258
-
259
- cuda_observables = []
260
- for observable in observables:
261
- if isinstance(observable, PauliOperator):
262
- cuda_observables.append(self._pauli_operator_handlers[type(observable)](observable))
263
- elif isinstance(observable, Hamiltonian):
264
- cuda_observables.append(self._hamiltonian_to_cuda(observable))
265
- else:
266
- raise ValueError(f"unsupported observable type of {observable.__class__}")
267
-
268
- evolution_result = evolve(
269
- hamiltonian=cuda_hamiltonian,
270
- dimensions=dict.fromkeys(range(schedule.nqubits), 2),
271
- schedule=_cuda_schedule,
272
- initial_state=State.from_data(np.array(initial_state.to_density_matrix().dense, dtype=np.complex128)),
273
- observables=cuda_observables,
274
- collapse_operators=[],
275
- store_intermediate_results=store_intermediate_results,
276
- )
277
-
278
- return CudaAnalogResult(
279
- final_expected_values=np.array(
280
- [exp_val.expectation() for exp_val in evolution_result.final_expectation_values()[0]]
281
- ),
282
- expected_values=(
283
- np.array(
284
- [[val.expectation() for val in exp_vals] for exp_vals in evolution_result.expectation_values()]
285
- )
286
- if evolution_result.expectation_values() is not None
287
- else None
288
- ),
289
- final_state=(
290
- QuantumObject(np.array(evolution_result.final_state())).adjoint()
291
- if evolution_result.final_state() is not None
292
- else None
293
- ),
294
- intermediate_states=(
295
- [QuantumObject(np.array(state)).adjoint() for state in evolution_result.intermediate_states()]
296
- if evolution_result.intermediate_states() is not None
297
- else None
298
- ),
299
- )
300
-
301
276
  @staticmethod
302
277
  def _handle_M(kernel: cudaq.Kernel, gate: M, circuit: Circuit, qubits: cudaq.QuakeValue) -> None:
303
278
  """
@@ -318,6 +293,11 @@ class CudaBackend(DigitalBackend, AnalogBackend):
318
293
  for idx in gate.target_qubits:
319
294
  kernel.mz(qubits[idx])
320
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
+
321
301
  @staticmethod
322
302
  def _handle_X(kernel: cudaq.Kernel, gate: X, qubit: cudaq.QuakeValue) -> None:
323
303
  """Handle an X gate operation."""
@@ -351,17 +331,17 @@ class CudaBackend(DigitalBackend, AnalogBackend):
351
331
  @staticmethod
352
332
  def _handle_RX(kernel: cudaq.Kernel, gate: RX, qubit: cudaq.QuakeValue) -> None:
353
333
  """Handle an RX gate operation."""
354
- kernel.rx(*gate.parameter_values, qubit)
334
+ kernel.rx(*gate.get_parameter_values(), qubit)
355
335
 
356
336
  @staticmethod
357
337
  def _handle_RY(kernel: cudaq.Kernel, gate: RY, qubit: cudaq.QuakeValue) -> None:
358
338
  """Handle an RY gate operation."""
359
- kernel.ry(*gate.parameter_values, qubit)
339
+ kernel.ry(*gate.get_parameter_values(), qubit)
360
340
 
361
341
  @staticmethod
362
342
  def _handle_RZ(kernel: cudaq.Kernel, gate: RZ, qubit: cudaq.QuakeValue) -> None:
363
343
  """Handle an RZ gate operation."""
364
- kernel.rz(*gate.parameter_values, qubit)
344
+ kernel.rz(*gate.get_parameter_values(), qubit)
365
345
 
366
346
  @staticmethod
367
347
  def _handle_U1(kernel: cudaq.Kernel, gate: U1, qubit: cudaq.QuakeValue) -> None:
@@ -377,20 +357,32 @@ class CudaBackend(DigitalBackend, AnalogBackend):
377
357
  def _handle_U3(kernel: cudaq.Kernel, gate: U3, qubit: cudaq.QuakeValue) -> None:
378
358
  """Handle an U3 gate operation."""
379
359
  kernel.u3(theta=gate.theta, phi=gate.phi, delta=gate.gamma, target=qubit)
380
- kernel.u3(theta=gate.theta, phi=gate.phi, delta=gate.gamma, target=qubit)
381
360
 
382
361
  @staticmethod
383
- 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
384
376
  return spin.x(target=operator.qubit)
385
377
 
386
378
  @staticmethod
387
- def _handle_PauliY(operator: PauliY) -> ElementaryOperator:
379
+ def _handle_PauliY(operator: PauliY) -> ElementaryOperator: # type: ignore
388
380
  return spin.y(target=operator.qubit)
389
381
 
390
382
  @staticmethod
391
- def _handle_PauliZ(operator: PauliZ) -> ElementaryOperator:
383
+ def _handle_PauliZ(operator: PauliZ) -> ElementaryOperator: # type: ignore
392
384
  return spin.z(target=operator.qubit)
393
385
 
394
386
  @staticmethod
395
- def _handle_PauliI(operator: PauliI) -> ElementaryOperator:
387
+ def _handle_PauliI(operator: PauliI) -> ElementaryOperator: # type: ignore
396
388
  return spin.i(target=operator.qubit)