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.
- qilisdk/__init__.py +11 -2
- qilisdk/__init__.pyi +2 -3
- qilisdk/_logging.py +135 -0
- qilisdk/_optionals.py +5 -7
- qilisdk/analog/__init__.py +3 -18
- qilisdk/analog/exceptions.py +2 -4
- qilisdk/analog/hamiltonian.py +455 -110
- qilisdk/analog/linear_schedule.py +118 -0
- qilisdk/analog/schedule.py +272 -79
- qilisdk/backends/__init__.py +45 -0
- qilisdk/{digital/digital_algorithm.py → backends/__init__.pyi} +3 -5
- qilisdk/backends/backend.py +117 -0
- qilisdk/{extras/cuda → backends}/cuda_backend.py +152 -159
- qilisdk/backends/qutip_backend.py +492 -0
- qilisdk/common/__init__.py +48 -2
- qilisdk/common/algorithm.py +2 -1
- qilisdk/{extras/qaas/qaas_settings.py → common/exceptions.py} +12 -6
- qilisdk/common/model.py +1019 -1
- qilisdk/common/parameterizable.py +75 -0
- qilisdk/common/qtensor.py +666 -0
- qilisdk/common/result.py +2 -1
- qilisdk/common/variables.py +1931 -0
- qilisdk/{extras/cuda/cuda_analog_result.py → cost_functions/__init__.py} +3 -4
- qilisdk/cost_functions/cost_function.py +77 -0
- qilisdk/cost_functions/model_cost_function.py +145 -0
- qilisdk/cost_functions/observable_cost_function.py +109 -0
- qilisdk/digital/__init__.py +3 -22
- qilisdk/digital/ansatz.py +203 -160
- qilisdk/digital/circuit.py +81 -9
- qilisdk/digital/exceptions.py +12 -6
- qilisdk/digital/gates.py +228 -85
- qilisdk/{extras/qaas/qaas_analog_result.py → functionals/__init__.py} +14 -5
- qilisdk/functionals/functional.py +39 -0
- qilisdk/{extras/cuda/cuda_digital_result.py → functionals/functional_result.py} +3 -4
- qilisdk/functionals/sampling.py +81 -0
- qilisdk/functionals/sampling_result.py +92 -0
- qilisdk/functionals/time_evolution.py +98 -0
- qilisdk/functionals/time_evolution_result.py +84 -0
- qilisdk/functionals/variational_program.py +80 -0
- qilisdk/functionals/variational_program_result.py +69 -0
- qilisdk/logging_config.yaml +16 -0
- qilisdk/{common/backend.py → optimizers/__init__.py} +2 -1
- qilisdk/optimizers/optimizer.py +39 -0
- qilisdk/{common → optimizers}/optimizer_result.py +3 -12
- qilisdk/{common/optimizer.py → optimizers/scipy_optimizer.py} +10 -28
- qilisdk/settings.py +78 -0
- qilisdk/{extras → speqtrum}/__init__.py +7 -8
- qilisdk/{extras → speqtrum}/__init__.pyi +3 -3
- qilisdk/speqtrum/experiments/__init__.py +25 -0
- qilisdk/speqtrum/experiments/experiment_functional.py +124 -0
- qilisdk/speqtrum/experiments/experiment_result.py +231 -0
- qilisdk/{extras/qaas → speqtrum}/keyring.py +8 -4
- qilisdk/speqtrum/speqtrum.py +432 -0
- qilisdk/speqtrum/speqtrum_models.py +300 -0
- qilisdk/utils/__init__.py +0 -14
- qilisdk/utils/openqasm2.py +1 -1
- qilisdk/utils/serialization.py +1 -1
- qilisdk/utils/visualization/PlusJakartaSans-SemiBold.ttf +0 -0
- qilisdk/utils/visualization/__init__.py +24 -0
- qilisdk/utils/visualization/circuit_renderers.py +781 -0
- qilisdk/utils/visualization/schedule_renderers.py +161 -0
- qilisdk/utils/visualization/style.py +154 -0
- qilisdk/utils/visualization/themes.py +76 -0
- qilisdk/yaml.py +126 -0
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/METADATA +180 -134
- qilisdk-0.1.5.dist-info/RECORD +69 -0
- qilisdk/analog/algorithms.py +0 -111
- qilisdk/analog/analog_backend.py +0 -43
- qilisdk/analog/analog_result.py +0 -114
- qilisdk/analog/quantum_objects.py +0 -596
- qilisdk/digital/digital_backend.py +0 -90
- qilisdk/digital/digital_result.py +0 -145
- qilisdk/digital/vqe.py +0 -166
- qilisdk/extras/cuda/__init__.py +0 -13
- qilisdk/extras/qaas/__init__.py +0 -13
- qilisdk/extras/qaas/models.py +0 -132
- qilisdk/extras/qaas/qaas_backend.py +0 -255
- qilisdk/extras/qaas/qaas_digital_result.py +0 -20
- qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -20
- qilisdk/extras/qaas/qaas_vqe_result.py +0 -20
- qilisdk-0.1.4.dist-info/RECORD +0 -51
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
|
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.
|
|
26
|
-
from qilisdk.
|
|
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 .
|
|
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.
|
|
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
|
|
46
|
+
class CudaSamplingMethod(str, Enum):
|
|
47
|
+
"""
|
|
48
|
+
Enumeration of available simulation methods for the CUDA backend.
|
|
61
49
|
"""
|
|
62
|
-
|
|
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
|
-
|
|
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__(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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[
|
|
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
|
-
|
|
156
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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)
|