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.
- 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 +153 -161
- 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.3.dist-info → qilisdk-0.1.5.dist-info}/METADATA +180 -135
- 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 -533
- 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.3.dist-info/RECORD +0 -51
- {qilisdk-0.1.3.dist-info → qilisdk-0.1.5.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
|
22
|
-
from
|
|
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.
|
|
27
|
-
from qilisdk.
|
|
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 .
|
|
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.
|
|
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
|
|
46
|
+
class CudaSamplingMethod(str, Enum):
|
|
47
|
+
"""
|
|
48
|
+
Enumeration of available simulation methods for the CUDA backend.
|
|
62
49
|
"""
|
|
63
|
-
|
|
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
|
-
|
|
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__(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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[
|
|
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
|
-
|
|
157
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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)
|