qadence 1.1.1__py3-none-any.whl → 1.2.1__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.
- qadence/__init__.py +1 -0
- qadence/analog/__init__.py +4 -2
- qadence/analog/addressing.py +167 -0
- qadence/analog/constants.py +59 -0
- qadence/analog/device.py +82 -0
- qadence/analog/hamiltonian_terms.py +101 -0
- qadence/analog/parse_analog.py +120 -0
- qadence/backend.py +42 -12
- qadence/backends/__init__.py +1 -2
- qadence/backends/api.py +27 -9
- qadence/backends/braket/backend.py +3 -2
- qadence/backends/horqrux/__init__.py +5 -0
- qadence/backends/horqrux/backend.py +216 -0
- qadence/backends/horqrux/config.py +26 -0
- qadence/backends/horqrux/convert_ops.py +273 -0
- qadence/backends/jax_utils.py +45 -0
- qadence/backends/pulser/__init__.py +0 -1
- qadence/backends/pulser/backend.py +31 -15
- qadence/backends/pulser/config.py +19 -10
- qadence/backends/pulser/devices.py +57 -63
- qadence/backends/pulser/pulses.py +70 -12
- qadence/backends/pyqtorch/backend.py +4 -4
- qadence/backends/pyqtorch/config.py +18 -12
- qadence/backends/pyqtorch/convert_ops.py +15 -7
- qadence/backends/utils.py +5 -9
- qadence/blocks/abstract.py +5 -1
- qadence/blocks/analog.py +18 -9
- qadence/blocks/block_to_tensor.py +11 -0
- qadence/blocks/embedding.py +46 -24
- qadence/blocks/primitive.py +81 -9
- qadence/blocks/utils.py +20 -1
- qadence/circuit.py +3 -9
- qadence/constructors/__init__.py +4 -0
- qadence/constructors/feature_maps.py +84 -60
- qadence/constructors/hamiltonians.py +27 -98
- qadence/constructors/rydberg_feature_maps.py +113 -0
- qadence/divergences.py +12 -0
- qadence/engines/__init__.py +0 -0
- qadence/engines/differentiable_backend.py +152 -0
- qadence/engines/jax/__init__.py +8 -0
- qadence/engines/jax/differentiable_backend.py +73 -0
- qadence/engines/jax/differentiable_expectation.py +94 -0
- qadence/engines/torch/__init__.py +4 -0
- qadence/engines/torch/differentiable_backend.py +85 -0
- qadence/extensions.py +21 -9
- qadence/finitediff.py +47 -0
- qadence/mitigations/readout.py +92 -25
- qadence/ml_tools/models.py +10 -3
- qadence/models/qnn.py +88 -23
- qadence/models/quantum_model.py +13 -2
- qadence/operations.py +55 -70
- qadence/parameters.py +24 -13
- qadence/register.py +91 -43
- qadence/transpile/__init__.py +1 -0
- qadence/transpile/apply_fn.py +40 -0
- qadence/types.py +32 -2
- qadence/utils.py +35 -0
- {qadence-1.1.1.dist-info → qadence-1.2.1.dist-info}/METADATA +22 -3
- {qadence-1.1.1.dist-info → qadence-1.2.1.dist-info}/RECORD +62 -44
- {qadence-1.1.1.dist-info → qadence-1.2.1.dist-info}/WHEEL +1 -1
- qadence/analog/interaction.py +0 -198
- qadence/analog/utils.py +0 -132
- /qadence/{backends/pytorch_wrapper.py → engines/torch/differentiable_expectation.py} +0 -0
- {qadence-1.1.1.dist-info → qadence-1.2.1.dist-info}/licenses/LICENSE +0 -0
qadence/__init__.py
CHANGED
qadence/analog/__init__.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
from .
|
3
|
+
from .addressing import AddressingPattern
|
4
|
+
from .device import IdealDevice, RealisticDevice, RydbergDevice
|
5
|
+
from .parse_analog import add_background_hamiltonian
|
4
6
|
|
5
|
-
__all__ = ["
|
7
|
+
__all__ = ["RydbergDevice", "IdealDevice", "RealisticDevice", "AddressingPattern"]
|
@@ -0,0 +1,167 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass, fields
|
4
|
+
from typing import Union
|
5
|
+
from warnings import warn
|
6
|
+
|
7
|
+
from sympy import Expr, Heaviside, exp
|
8
|
+
from torch import Tensor, pi
|
9
|
+
|
10
|
+
from qadence.parameters import Parameter, evaluate
|
11
|
+
|
12
|
+
# FIXME: Clarify the roles of these values in the context
|
13
|
+
# device specification and how they relate with the
|
14
|
+
# maximum values for delta and omega.
|
15
|
+
GLOBAL_MAX_AMPLITUDE = 300
|
16
|
+
GLOBAL_MAX_DETUNING = 2 * pi * 2000
|
17
|
+
LOCAL_MAX_AMPLITUDE = 3
|
18
|
+
LOCAL_MAX_DETUNING = 2 * pi * 20
|
19
|
+
|
20
|
+
TWeight = Union[str, float, Tensor, Parameter]
|
21
|
+
|
22
|
+
|
23
|
+
def sigmoid(x: Tensor, a: float, b: float) -> Expr:
|
24
|
+
return 1.0 / (1.0 + exp(-a * (x + b)))
|
25
|
+
|
26
|
+
|
27
|
+
@dataclass
|
28
|
+
class AddressingPattern:
|
29
|
+
"""Semi-local addressing pattern."""
|
30
|
+
|
31
|
+
n_qubits: int
|
32
|
+
"""Number of qubits in register."""
|
33
|
+
|
34
|
+
weights_amp: dict[int, TWeight]
|
35
|
+
"""List of weights for fixed amplitude pattern that cannot be changed during the execution."""
|
36
|
+
|
37
|
+
weights_det: dict[int, TWeight]
|
38
|
+
"""List of weights for fixed detuning pattern that cannot be changed during the execution."""
|
39
|
+
|
40
|
+
amp: TWeight = LOCAL_MAX_AMPLITUDE
|
41
|
+
"""Maximum amplitude of the amplitude pattern felt by a single qubit."""
|
42
|
+
|
43
|
+
det: TWeight = LOCAL_MAX_DETUNING
|
44
|
+
"""Maximum detuning of the detuning pattern felt by a single qubit."""
|
45
|
+
|
46
|
+
def _validate_weights(
|
47
|
+
self,
|
48
|
+
weights: dict[int, TWeight],
|
49
|
+
) -> None:
|
50
|
+
for v in weights.values():
|
51
|
+
if not isinstance(v, (str, Parameter)):
|
52
|
+
if not (v >= 0.0 and v <= 1.0):
|
53
|
+
raise ValueError("Addressing pattern weights must sum fall in range [0.0, 1.0]")
|
54
|
+
|
55
|
+
def _constrain_weights(
|
56
|
+
self,
|
57
|
+
weights: dict[int, TWeight],
|
58
|
+
) -> dict:
|
59
|
+
# augment weight dict if needed
|
60
|
+
weights = {
|
61
|
+
i: Parameter(0.0)
|
62
|
+
if i not in weights
|
63
|
+
else (Parameter(weights[i]) if not isinstance(weights[i], Parameter) else weights[i])
|
64
|
+
for i in range(self.n_qubits)
|
65
|
+
}
|
66
|
+
|
67
|
+
# restrict weights to [0, 1] range - equal to 0 everywhere else
|
68
|
+
weights = {
|
69
|
+
k: v if v.is_number else abs(v * (sigmoid(v, 20, 1) - sigmoid(v, 20.0, -1))) # type: ignore [union-attr]
|
70
|
+
for k, v in weights.items()
|
71
|
+
}
|
72
|
+
|
73
|
+
return weights
|
74
|
+
|
75
|
+
def _constrain_max_vals(self) -> None:
|
76
|
+
# enforce constraints:
|
77
|
+
# 0 <= amp <= GLOBAL_MAX_AMPLITUDE
|
78
|
+
# 0 <= abs(det) <= GLOBAL_MAX_DETUNING
|
79
|
+
self.amp = abs(
|
80
|
+
self.amp
|
81
|
+
* (
|
82
|
+
Heaviside(self.amp + GLOBAL_MAX_AMPLITUDE) # type: ignore [operator]
|
83
|
+
- Heaviside(self.amp - GLOBAL_MAX_AMPLITUDE) # type: ignore [operator]
|
84
|
+
)
|
85
|
+
)
|
86
|
+
self.det = -abs(
|
87
|
+
self.det
|
88
|
+
* (
|
89
|
+
Heaviside(self.det + GLOBAL_MAX_DETUNING)
|
90
|
+
- Heaviside(self.det - GLOBAL_MAX_DETUNING)
|
91
|
+
)
|
92
|
+
)
|
93
|
+
|
94
|
+
def _create_local_constraint(self, val: Expr, weights: dict, max_val: float) -> dict:
|
95
|
+
# enforce local constraints:
|
96
|
+
# amp * w_amp_i < LOCAL_MAX_AMPLITUDE or
|
97
|
+
# abs(det) * w_det_i < LOCAL_MAX_DETUNING
|
98
|
+
local_constr = {k: val * v for k, v in weights.items()}
|
99
|
+
local_constr = {k: Heaviside(v) - Heaviside(v - max_val) for k, v in local_constr.items()}
|
100
|
+
|
101
|
+
return local_constr
|
102
|
+
|
103
|
+
def _create_global_constraint(self, val: Expr, weights: dict, max_val: float) -> Expr:
|
104
|
+
# enforce global constraints:
|
105
|
+
# amp * sum(w_amp_0, w_amp_1, ...) < GLOBAL_MAX_AMPLITUDE or
|
106
|
+
# abs(det) * sum(w_det_0, w_det_1, ...) < GLOBAL_MAX_DETUNING
|
107
|
+
weighted_vals_global = val * sum([v for v in weights.values()])
|
108
|
+
weighted_vals_global = Heaviside(weighted_vals_global) - Heaviside(
|
109
|
+
weighted_vals_global - max_val
|
110
|
+
)
|
111
|
+
|
112
|
+
return weighted_vals_global
|
113
|
+
|
114
|
+
def __post_init__(self) -> None:
|
115
|
+
# validate amplitude/detuning weights
|
116
|
+
self._validate_weights(self.weights_amp)
|
117
|
+
self._validate_weights(self.weights_det)
|
118
|
+
|
119
|
+
# validate maximum global amplitude/detuning values
|
120
|
+
if not isinstance(self.amp, (str, Parameter)):
|
121
|
+
if self.amp > GLOBAL_MAX_AMPLITUDE:
|
122
|
+
warn("Maximum absolute value of amplitude is exceeded")
|
123
|
+
elif isinstance(self.amp, str):
|
124
|
+
self.amp = Parameter(self.amp, trainable=True)
|
125
|
+
if not isinstance(self.det, (str, Parameter)):
|
126
|
+
if abs(self.det) > GLOBAL_MAX_DETUNING:
|
127
|
+
warn("Maximum absolute value of detuning is exceeded")
|
128
|
+
elif isinstance(self.det, str):
|
129
|
+
self.det = Parameter(self.det, trainable=True)
|
130
|
+
|
131
|
+
# constrain amplitude/detuning parameterized weights to [0.0, 1.0] interval
|
132
|
+
self.weights_amp = self._constrain_weights(self.weights_amp)
|
133
|
+
self.weights_det = self._constrain_weights(self.weights_det)
|
134
|
+
|
135
|
+
# constrain max global amplitude and detuning to strict interval
|
136
|
+
self._constrain_max_vals()
|
137
|
+
|
138
|
+
# create additional local and global constraints for amplitude/detuning masks
|
139
|
+
self.local_constr_amp = self._create_local_constraint(
|
140
|
+
self.amp, self.weights_amp, LOCAL_MAX_AMPLITUDE
|
141
|
+
)
|
142
|
+
self.local_constr_det = self._create_local_constraint(
|
143
|
+
-self.det, self.weights_det, LOCAL_MAX_DETUNING
|
144
|
+
)
|
145
|
+
self.global_constr_amp = self._create_global_constraint(
|
146
|
+
self.amp, self.weights_amp, GLOBAL_MAX_AMPLITUDE
|
147
|
+
)
|
148
|
+
self.global_constr_det = self._create_global_constraint(
|
149
|
+
-self.det, self.weights_det, GLOBAL_MAX_DETUNING
|
150
|
+
)
|
151
|
+
|
152
|
+
# validate number of qubits in mask
|
153
|
+
if max(list(self.weights_amp.keys())) >= self.n_qubits:
|
154
|
+
raise ValueError("Amplitude weight specified for non-existing qubit")
|
155
|
+
if max(list(self.weights_det.keys())) >= self.n_qubits:
|
156
|
+
raise ValueError("Detuning weight specified for non-existing qubit")
|
157
|
+
|
158
|
+
def evaluate(self, weights: dict, values: dict) -> dict:
|
159
|
+
# evaluate weight expressions with actual values
|
160
|
+
return {k: evaluate(v, values, as_torch=True).flatten() for k, v in weights.items()} # type: ignore [union-attr]
|
161
|
+
|
162
|
+
def _to_dict(self) -> dict:
|
163
|
+
return {field.name: getattr(self, field.name) for field in fields(self)}
|
164
|
+
|
165
|
+
@classmethod
|
166
|
+
def _from_dict(cls, d: dict) -> AddressingPattern | None:
|
167
|
+
return cls(**d) if len(d) > 0 else None
|
@@ -0,0 +1,59 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
# Ising coupling coefficient depending on the Rydberg level
|
4
|
+
# Include a normalization to the Planck constant hbar
|
5
|
+
# In units of [rad . µm^6 / µs]
|
6
|
+
|
7
|
+
C6_DICT = {
|
8
|
+
50: 96120.72,
|
9
|
+
51: 122241.6,
|
10
|
+
52: 154693.02,
|
11
|
+
53: 194740.36,
|
12
|
+
54: 243973.91,
|
13
|
+
55: 304495.01,
|
14
|
+
56: 378305.98,
|
15
|
+
57: 468027.05,
|
16
|
+
58: 576714.85,
|
17
|
+
59: 707911.38,
|
18
|
+
60: 865723.02,
|
19
|
+
61: 1054903.11,
|
20
|
+
62: 1281042.11,
|
21
|
+
63: 1550531.15,
|
22
|
+
64: 1870621.31,
|
23
|
+
65: 2249728.57,
|
24
|
+
66: 2697498.69,
|
25
|
+
67: 3224987.51,
|
26
|
+
68: 3844734.37,
|
27
|
+
69: 4571053.32,
|
28
|
+
70: 5420158.53,
|
29
|
+
71: 6410399.4,
|
30
|
+
72: 7562637.31,
|
31
|
+
73: 8900342.14,
|
32
|
+
74: 10449989.62,
|
33
|
+
75: 12241414.53,
|
34
|
+
76: 14308028.03,
|
35
|
+
77: 16687329.94,
|
36
|
+
78: 19421333.62,
|
37
|
+
79: 22557029.94,
|
38
|
+
80: 26146720.74,
|
39
|
+
81: 30248886.65,
|
40
|
+
82: 34928448.69,
|
41
|
+
83: 40257623.67,
|
42
|
+
84: 46316557.88,
|
43
|
+
85: 53194043.52,
|
44
|
+
86: 60988354.64,
|
45
|
+
87: 69808179.15,
|
46
|
+
88: 79773468.88,
|
47
|
+
89: 91016513.07,
|
48
|
+
90: 103677784.57,
|
49
|
+
91: 117933293.96,
|
50
|
+
92: 133943541.9,
|
51
|
+
93: 151907135.94,
|
52
|
+
94: 172036137.34,
|
53
|
+
95: 194562889.89,
|
54
|
+
96: 219741590.56,
|
55
|
+
97: 247850178.91,
|
56
|
+
98: 279192193.77,
|
57
|
+
99: 314098829.39,
|
58
|
+
100: 352931119.11,
|
59
|
+
}
|
qadence/analog/device.py
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass, fields
|
4
|
+
|
5
|
+
from torch import pi
|
6
|
+
|
7
|
+
from qadence.analog import AddressingPattern
|
8
|
+
from qadence.types import DeviceType, Interaction
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass(frozen=True, eq=True)
|
12
|
+
class RydbergDevice:
|
13
|
+
"""
|
14
|
+
Dataclass for interacting Rydberg atoms under an Hamiltonian:
|
15
|
+
|
16
|
+
H = ∑_i [Ω/2 * (cos(φ) * Xᵢ - sin(φ) * Yᵢ) - δ * N_i] + H_int,
|
17
|
+
|
18
|
+
where:
|
19
|
+
|
20
|
+
H_int = ∑_(j<i) (C_6 / R**6) * (N_i @ N_j) for the NN interaction;
|
21
|
+
|
22
|
+
H_int = ∑_(j<i) (C_3 / R**3) * ((X_i @ X_j) + (Y_i @ Y_j)) for the XY interaction;
|
23
|
+
"""
|
24
|
+
|
25
|
+
interaction: Interaction = Interaction.NN
|
26
|
+
"""Defines the interaction Hamiltonian."""
|
27
|
+
|
28
|
+
rydberg_level: int = 60
|
29
|
+
"""Rydberg level affecting the value of C_6."""
|
30
|
+
|
31
|
+
coeff_xy: float = 3700.00
|
32
|
+
"""Value of C_3."""
|
33
|
+
|
34
|
+
max_detuning: float = 2 * pi * 4
|
35
|
+
"""Maximum value of the detuning δ."""
|
36
|
+
|
37
|
+
max_amp: float = 2 * pi * 3
|
38
|
+
"""Maximum value of the amplitude Ω."""
|
39
|
+
|
40
|
+
pattern: AddressingPattern | None = None
|
41
|
+
"""Semi-local addressing pattern configuration."""
|
42
|
+
|
43
|
+
type: DeviceType = DeviceType.IDEALIZED
|
44
|
+
"""DeviceType.IDEALIZED or REALISTIC to convert to the Pulser backend."""
|
45
|
+
|
46
|
+
def __post_init__(self) -> None:
|
47
|
+
# FIXME: Currently not supporting custom interaction functions.
|
48
|
+
if self.interaction not in [Interaction.NN, Interaction.XY]:
|
49
|
+
raise KeyError(
|
50
|
+
"RydbergDevice currently only supports Interaction.NN or Interaction.XY."
|
51
|
+
)
|
52
|
+
|
53
|
+
def _to_dict(self) -> dict:
|
54
|
+
device_dict = {}
|
55
|
+
for field in fields(self):
|
56
|
+
if field.name != "pattern":
|
57
|
+
device_dict[field.name] = getattr(self, field.name)
|
58
|
+
else:
|
59
|
+
device_dict[field.name] = (
|
60
|
+
self.pattern._to_dict() if self.pattern is not None else {}
|
61
|
+
)
|
62
|
+
return device_dict
|
63
|
+
|
64
|
+
@classmethod
|
65
|
+
def _from_dict(cls, d: dict) -> RydbergDevice:
|
66
|
+
pattern = AddressingPattern._from_dict(d["pattern"])
|
67
|
+
d["pattern"] = pattern
|
68
|
+
return cls(**d)
|
69
|
+
|
70
|
+
|
71
|
+
def IdealDevice(pattern: AddressingPattern | None = None) -> RydbergDevice:
|
72
|
+
return RydbergDevice(
|
73
|
+
pattern=pattern,
|
74
|
+
type=DeviceType.IDEALIZED,
|
75
|
+
)
|
76
|
+
|
77
|
+
|
78
|
+
def RealisticDevice(pattern: AddressingPattern | None = None) -> RydbergDevice:
|
79
|
+
return RydbergDevice(
|
80
|
+
pattern=pattern,
|
81
|
+
type=DeviceType.REALISTIC,
|
82
|
+
)
|
@@ -0,0 +1,101 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from sympy import cos, sin
|
4
|
+
from torch import float64, tensor
|
5
|
+
|
6
|
+
from qadence.analog.constants import C6_DICT
|
7
|
+
from qadence.blocks import add
|
8
|
+
from qadence.blocks.abstract import AbstractBlock
|
9
|
+
from qadence.blocks.analog import ConstantAnalogRotation
|
10
|
+
from qadence.constructors import hamiltonian_factory
|
11
|
+
from qadence.operations import I, N, X, Y, Z
|
12
|
+
from qadence.register import Register
|
13
|
+
from qadence.types import Interaction
|
14
|
+
|
15
|
+
|
16
|
+
def rydberg_interaction_hamiltonian(
|
17
|
+
register: Register,
|
18
|
+
) -> AbstractBlock:
|
19
|
+
"""
|
20
|
+
Computes the Rydberg Ising or XY interaction Hamiltonian for a register of qubits.
|
21
|
+
|
22
|
+
H_int = ∑_(j<i) (C_6 / R**6) * kron(N_i, N_j)
|
23
|
+
|
24
|
+
H_int = ∑_(j<i) (C_3 / R**3) * (kron(X_i, X_j) + kron(Y_i, Y_j))
|
25
|
+
|
26
|
+
Args:
|
27
|
+
register: the register of qubits.
|
28
|
+
"""
|
29
|
+
|
30
|
+
distances = tensor(list(register.distances.values()), dtype=float64)
|
31
|
+
device_specs = register.device_specs
|
32
|
+
|
33
|
+
if device_specs.interaction == Interaction.NN:
|
34
|
+
c6 = C6_DICT[device_specs.rydberg_level]
|
35
|
+
strength_list = c6 / (distances**6)
|
36
|
+
elif device_specs.interaction == Interaction.XY:
|
37
|
+
c3 = device_specs.coeff_xy
|
38
|
+
strength_list = c3 / (distances**3)
|
39
|
+
|
40
|
+
return hamiltonian_factory(
|
41
|
+
register,
|
42
|
+
interaction=device_specs.interaction,
|
43
|
+
interaction_strength=strength_list,
|
44
|
+
use_all_node_pairs=True,
|
45
|
+
)
|
46
|
+
|
47
|
+
|
48
|
+
def rydberg_drive_hamiltonian(block: ConstantAnalogRotation, register: Register) -> AbstractBlock:
|
49
|
+
"""
|
50
|
+
Computes the Rydberg drive Hamiltonian for some local or global rotation.
|
51
|
+
|
52
|
+
H_d = ∑_i (Ω/2 cos(φ) * X_i - sin(φ) * Y_i) - δ * N_i
|
53
|
+
|
54
|
+
Args:
|
55
|
+
block: the ConstantAnalogRotation block containing the parameters.
|
56
|
+
register: the register of qubits.
|
57
|
+
"""
|
58
|
+
|
59
|
+
if block.qubit_support.is_global:
|
60
|
+
qubit_support = tuple(register.nodes)
|
61
|
+
else:
|
62
|
+
qubit_support = block.qubit_support
|
63
|
+
|
64
|
+
omega = block.parameters.omega
|
65
|
+
delta = block.parameters.delta
|
66
|
+
phase = block.parameters.phase
|
67
|
+
|
68
|
+
x_terms = (omega / 2) * add(cos(phase) * X(i) for i in qubit_support)
|
69
|
+
y_terms = (omega / 2) * add(sin(phase) * Y(i) for i in qubit_support)
|
70
|
+
n_terms = delta * add(N(i) for i in qubit_support)
|
71
|
+
h_drive: AbstractBlock = x_terms - y_terms - n_terms
|
72
|
+
return h_drive
|
73
|
+
|
74
|
+
|
75
|
+
def rydberg_pattern_hamiltonian(register: Register) -> AbstractBlock | None:
|
76
|
+
support = tuple(range(register.n_qubits))
|
77
|
+
pattern = register.device_specs.pattern
|
78
|
+
if pattern is not None:
|
79
|
+
amp = pattern.amp
|
80
|
+
det = pattern.det
|
81
|
+
weights_amp = pattern.weights_amp
|
82
|
+
weights_det = pattern.weights_det
|
83
|
+
local_constr_amp = pattern.local_constr_amp
|
84
|
+
local_constr_det = pattern.local_constr_det
|
85
|
+
global_constr_amp = pattern.global_constr_amp
|
86
|
+
global_constr_det = pattern.global_constr_det
|
87
|
+
|
88
|
+
p_amp_terms: AbstractBlock = (
|
89
|
+
(1 / 2) # type: ignore [operator]
|
90
|
+
* amp
|
91
|
+
* global_constr_amp
|
92
|
+
* add(X(i) * weights_amp[i] * local_constr_amp[i] for i in support) # type: ignore [operator]
|
93
|
+
)
|
94
|
+
p_det_terms: AbstractBlock = (
|
95
|
+
-det # type: ignore [operator]
|
96
|
+
* global_constr_det
|
97
|
+
* add(0.5 * (I(i) - Z(i)) * weights_det[i] * local_constr_det[i] for i in support) # type: ignore [operator]
|
98
|
+
)
|
99
|
+
return p_amp_terms + p_det_terms
|
100
|
+
else:
|
101
|
+
return None
|
@@ -0,0 +1,120 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from qadence.analog.hamiltonian_terms import (
|
4
|
+
rydberg_drive_hamiltonian,
|
5
|
+
rydberg_interaction_hamiltonian,
|
6
|
+
rydberg_pattern_hamiltonian,
|
7
|
+
)
|
8
|
+
from qadence.blocks import chain
|
9
|
+
from qadence.blocks.abstract import AbstractBlock
|
10
|
+
from qadence.blocks.analog import (
|
11
|
+
AnalogKron,
|
12
|
+
ConstantAnalogRotation,
|
13
|
+
WaitBlock,
|
14
|
+
)
|
15
|
+
from qadence.circuit import QuantumCircuit
|
16
|
+
from qadence.operations import HamEvo
|
17
|
+
from qadence.register import Register
|
18
|
+
from qadence.transpile import apply_fn_to_blocks
|
19
|
+
|
20
|
+
|
21
|
+
def add_background_hamiltonian(
|
22
|
+
circuit: QuantumCircuit | AbstractBlock,
|
23
|
+
register: Register | None = None,
|
24
|
+
) -> QuantumCircuit | AbstractBlock:
|
25
|
+
"""
|
26
|
+
Parses a `QuantumCircuit` to transform `AnalogBlocks` to `HamEvo`.
|
27
|
+
|
28
|
+
Depends on the circuit `Register` and included `RydbergDevice` specifications.
|
29
|
+
|
30
|
+
Currently checks if input is either circuit or block and adjusts
|
31
|
+
the ouput accordingly. Running this function on single blocks is
|
32
|
+
currently used for eigenvalue computation for GPSR.
|
33
|
+
|
34
|
+
Arguments:
|
35
|
+
circuit: the circuit to parse, or single block to transform.
|
36
|
+
register: needed for calling the function on a single block.
|
37
|
+
"""
|
38
|
+
# FIXME: revisit eigenvalues of analog blocks and clean this code.
|
39
|
+
|
40
|
+
is_circuit_input = isinstance(circuit, QuantumCircuit)
|
41
|
+
|
42
|
+
if not is_circuit_input and register is None:
|
43
|
+
raise ValueError("Block input requires an input to the `register` argument.")
|
44
|
+
|
45
|
+
input_block: AbstractBlock = circuit.block if is_circuit_input else circuit # type: ignore
|
46
|
+
input_register: Register = circuit.register if is_circuit_input else register # type: ignore
|
47
|
+
|
48
|
+
if input_register.device_specs is not None:
|
49
|
+
# Create interaction hamiltonian:
|
50
|
+
h_int = rydberg_interaction_hamiltonian(input_register)
|
51
|
+
|
52
|
+
# Create addressing pattern:
|
53
|
+
h_addr = rydberg_pattern_hamiltonian(input_register)
|
54
|
+
|
55
|
+
output_block = apply_fn_to_blocks(
|
56
|
+
input_block,
|
57
|
+
_analog_to_hevo,
|
58
|
+
input_register,
|
59
|
+
(h_int, h_addr),
|
60
|
+
)
|
61
|
+
else:
|
62
|
+
output_block = input_block
|
63
|
+
|
64
|
+
if is_circuit_input:
|
65
|
+
return QuantumCircuit(input_register, output_block)
|
66
|
+
else:
|
67
|
+
return output_block
|
68
|
+
|
69
|
+
|
70
|
+
def _build_ham_evo(
|
71
|
+
block: WaitBlock | ConstantAnalogRotation,
|
72
|
+
h_int: AbstractBlock,
|
73
|
+
h_drive: AbstractBlock | None,
|
74
|
+
h_addr: AbstractBlock | None,
|
75
|
+
) -> HamEvo:
|
76
|
+
duration = block.parameters.duration
|
77
|
+
h_block = h_int
|
78
|
+
if h_drive is not None:
|
79
|
+
h_block += h_drive
|
80
|
+
if block.add_pattern and h_addr is not None:
|
81
|
+
h_block += h_addr
|
82
|
+
return HamEvo(h_block, duration / 1000)
|
83
|
+
|
84
|
+
|
85
|
+
def _analog_to_hevo(
|
86
|
+
block: AbstractBlock,
|
87
|
+
register: Register,
|
88
|
+
h_terms: tuple[AbstractBlock, AbstractBlock | None],
|
89
|
+
) -> AbstractBlock:
|
90
|
+
"""
|
91
|
+
Converter from AnalogBlock to the respective HamEvo.
|
92
|
+
|
93
|
+
Any other block not covered by the specific conditions below is left unchanged.
|
94
|
+
"""
|
95
|
+
|
96
|
+
h_int, h_addr = h_terms
|
97
|
+
|
98
|
+
if isinstance(block, WaitBlock):
|
99
|
+
return _build_ham_evo(block, h_int, None, h_addr)
|
100
|
+
|
101
|
+
if isinstance(block, ConstantAnalogRotation):
|
102
|
+
h_drive = rydberg_drive_hamiltonian(block, register)
|
103
|
+
return _build_ham_evo(block, h_int, h_drive, h_addr)
|
104
|
+
|
105
|
+
if isinstance(block, AnalogKron):
|
106
|
+
# Needed to ensure kronned Analog blocks are implemented
|
107
|
+
# in sequence, consistent with the current Pulser implementation.
|
108
|
+
# FIXME: Revisit this assumption and the need for AnalogKron to have
|
109
|
+
# the same duration, and clean this code accordingly.
|
110
|
+
# https://github.com/pasqal-io/qadence/issues/226
|
111
|
+
ops = []
|
112
|
+
for block in block.blocks:
|
113
|
+
if isinstance(block, ConstantAnalogRotation):
|
114
|
+
h_drive = rydberg_drive_hamiltonian(block, register)
|
115
|
+
ops.append(_build_ham_evo(block, h_int, h_drive, h_addr))
|
116
|
+
if len(ops) == 0:
|
117
|
+
ops.append(_build_ham_evo(block, h_int, None, h_addr)) # type: ignore [arg-type]
|
118
|
+
return chain(*ops)
|
119
|
+
|
120
|
+
return block
|
qadence/backend.py
CHANGED
@@ -25,7 +25,8 @@ from qadence.measurements import Measurements
|
|
25
25
|
from qadence.mitigations import Mitigations
|
26
26
|
from qadence.noise import Noise
|
27
27
|
from qadence.parameters import stringify
|
28
|
-
from qadence.types import BackendName, DiffMode, Endianness
|
28
|
+
from qadence.types import ArrayLike, BackendName, DiffMode, Endianness, Engine, ParamDictType
|
29
|
+
from qadence.utils import validate_values_and_state
|
29
30
|
|
30
31
|
logger = get_logger(__file__)
|
31
32
|
|
@@ -99,11 +100,14 @@ class Backend(ABC):
|
|
99
100
|
name: backend unique string identifier
|
100
101
|
supports_ad: whether or not the backend has a native autograd
|
101
102
|
supports_bp: whether or not the backend has a native backprop
|
103
|
+
supports_adjoint: Does the backend support native adjoint differentation.
|
102
104
|
is_remote: whether computations are executed locally or remotely on this
|
103
105
|
backend, useful when using cloud platforms where credentials are
|
104
106
|
needed for example.
|
105
107
|
with_measurements: whether it supports counts or not
|
106
108
|
with_noise: whether to add realistic noise or not
|
109
|
+
native_endianness: The native endianness of the backend
|
110
|
+
engine: The underlying (native) automatic differentiation engine of the backend.
|
107
111
|
"""
|
108
112
|
|
109
113
|
name: BackendName
|
@@ -113,6 +117,7 @@ class Backend(ABC):
|
|
113
117
|
is_remote: bool
|
114
118
|
with_measurements: bool
|
115
119
|
native_endianness: Endianness
|
120
|
+
engine: Engine
|
116
121
|
|
117
122
|
# FIXME: should this also go into the configuration?
|
118
123
|
with_noise: bool
|
@@ -198,7 +203,7 @@ class Backend(ABC):
|
|
198
203
|
|
199
204
|
conv_circ = self.circuit(circuit)
|
200
205
|
circ_params, circ_embedding_fn = embedding(
|
201
|
-
conv_circ.abstract.block, self.config._use_gate_params
|
206
|
+
conv_circ.abstract.block, self.config._use_gate_params, self.engine
|
202
207
|
)
|
203
208
|
params = circ_params
|
204
209
|
if observable is not None:
|
@@ -210,7 +215,7 @@ class Backend(ABC):
|
|
210
215
|
obs = check_observable(obs)
|
211
216
|
c_obs = self.observable(obs, max(circuit.n_qubits, obs.n_qubits))
|
212
217
|
obs_params, obs_embedding_fn = embedding(
|
213
|
-
c_obs.abstract, self.config._use_gate_params
|
218
|
+
c_obs.abstract, self.config._use_gate_params, self.engine
|
214
219
|
)
|
215
220
|
params.update(obs_params)
|
216
221
|
obs_embedding_fn_list.append(obs_embedding_fn)
|
@@ -235,7 +240,7 @@ class Backend(ABC):
|
|
235
240
|
circuit: ConvertedCircuit,
|
236
241
|
param_values: dict[str, Tensor] = {},
|
237
242
|
n_shots: int = 1000,
|
238
|
-
state:
|
243
|
+
state: ArrayLike | None = None,
|
239
244
|
noise: Noise | None = None,
|
240
245
|
mitigation: Mitigations | None = None,
|
241
246
|
endianness: Endianness = Endianness.BIG,
|
@@ -255,13 +260,37 @@ class Backend(ABC):
|
|
255
260
|
raise NotImplementedError
|
256
261
|
|
257
262
|
@abstractmethod
|
263
|
+
def _run(
|
264
|
+
self,
|
265
|
+
circuit: ConvertedCircuit,
|
266
|
+
param_values: dict[str, ArrayLike] = {},
|
267
|
+
state: ArrayLike | None = None,
|
268
|
+
endianness: Endianness = Endianness.BIG,
|
269
|
+
) -> ArrayLike:
|
270
|
+
"""Run a circuit and return the resulting wave function.
|
271
|
+
|
272
|
+
Arguments:
|
273
|
+
circuit: A converted circuit as returned by `backend.circuit`.
|
274
|
+
param_values: _**Already embedded**_ parameters of the circuit. See
|
275
|
+
[`embedding`][qadence.blocks.embedding.embedding] for more info.
|
276
|
+
state: Initial state.
|
277
|
+
endianness: Endianness of the resulting wavefunction.
|
278
|
+
|
279
|
+
Returns:
|
280
|
+
A list of Counter objects where each key represents a bitstring
|
281
|
+
and its value the number of times it has been sampled from the given wave function.
|
282
|
+
"""
|
283
|
+
raise NotImplementedError
|
284
|
+
|
258
285
|
def run(
|
259
286
|
self,
|
260
287
|
circuit: ConvertedCircuit,
|
261
|
-
param_values: dict[str,
|
288
|
+
param_values: dict[str, ArrayLike] = {},
|
262
289
|
state: Tensor | None = None,
|
263
290
|
endianness: Endianness = Endianness.BIG,
|
264
|
-
|
291
|
+
*args: Any,
|
292
|
+
**kwargs: Any,
|
293
|
+
) -> ArrayLike:
|
265
294
|
"""Run a circuit and return the resulting wave function.
|
266
295
|
|
267
296
|
Arguments:
|
@@ -275,14 +304,15 @@ class Backend(ABC):
|
|
275
304
|
A list of Counter objects where each key represents a bitstring
|
276
305
|
and its value the number of times it has been sampled from the given wave function.
|
277
306
|
"""
|
278
|
-
|
307
|
+
validate_values_and_state(state, circuit.abstract.n_qubits, param_values)
|
308
|
+
return self._run(circuit, param_values, state, endianness, *args, **kwargs)
|
279
309
|
|
280
310
|
@abstractmethod
|
281
311
|
def run_dm(
|
282
312
|
self,
|
283
313
|
circuit: ConvertedCircuit,
|
284
314
|
noise: Noise,
|
285
|
-
param_values: dict[str,
|
315
|
+
param_values: dict[str, ArrayLike] = {},
|
286
316
|
state: Tensor | None = None,
|
287
317
|
endianness: Endianness = Endianness.BIG,
|
288
318
|
) -> Tensor:
|
@@ -309,13 +339,13 @@ class Backend(ABC):
|
|
309
339
|
self,
|
310
340
|
circuit: ConvertedCircuit,
|
311
341
|
observable: list[ConvertedObservable] | ConvertedObservable,
|
312
|
-
param_values:
|
313
|
-
state:
|
342
|
+
param_values: ParamDictType = {},
|
343
|
+
state: ArrayLike | None = None,
|
314
344
|
measurement: Measurements | None = None,
|
315
345
|
noise: Noise | None = None,
|
316
346
|
mitigation: Mitigations | None = None,
|
317
347
|
endianness: Endianness = Endianness.BIG,
|
318
|
-
) ->
|
348
|
+
) -> ArrayLike:
|
319
349
|
"""Compute the expectation value of the `circuit` with the given `observable`.
|
320
350
|
|
321
351
|
Arguments:
|
@@ -372,7 +402,7 @@ class Converted:
|
|
372
402
|
circuit: ConvertedCircuit
|
373
403
|
observable: list[ConvertedObservable] | ConvertedObservable | None
|
374
404
|
embedding_fn: Callable
|
375
|
-
params:
|
405
|
+
params: ParamDictType
|
376
406
|
|
377
407
|
def __iter__(self) -> Iterator:
|
378
408
|
yield self.circuit
|