emu-base 2.2.1__py3-none-any.whl → 2.4.0__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.
- emu_base/__init__.py +5 -4
- emu_base/math/krylov_energy_min.py +1 -1
- emu_base/pulser_adapter.py +72 -269
- emu_base/utils.py +57 -0
- {emu_base-2.2.1.dist-info → emu_base-2.4.0.dist-info}/METADATA +3 -3
- {emu_base-2.2.1.dist-info → emu_base-2.4.0.dist-info}/RECORD +7 -8
- emu_base/aggregators.py +0 -158
- {emu_base-2.2.1.dist-info → emu_base-2.4.0.dist-info}/WHEEL +0 -0
emu_base/__init__.py
CHANGED
|
@@ -4,20 +4,21 @@ from .math.brents_root_finding import find_root_brents
|
|
|
4
4
|
from .math.krylov_exp import krylov_exp, DEFAULT_MAX_KRYLOV_DIM
|
|
5
5
|
from .jump_lindblad_operators import compute_noise_from_lindbladians
|
|
6
6
|
from .math.matmul import matmul_2x2_with_batched
|
|
7
|
-
from .
|
|
7
|
+
from .utils import get_max_rss, apply_measurement_errors, unix_like
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"__version__",
|
|
11
|
+
"get_max_rss",
|
|
11
12
|
"compute_noise_from_lindbladians",
|
|
12
13
|
"matmul_2x2_with_batched",
|
|
13
|
-
"AggregationType",
|
|
14
|
-
"aggregate",
|
|
15
14
|
"PulserData",
|
|
16
15
|
"find_root_brents",
|
|
17
16
|
"krylov_exp",
|
|
18
17
|
"HamiltonianType",
|
|
19
18
|
"DEFAULT_MAX_KRYLOV_DIM",
|
|
20
19
|
"DEVICE_COUNT",
|
|
20
|
+
"apply_measurement_errors",
|
|
21
|
+
"unix_like",
|
|
21
22
|
]
|
|
22
23
|
|
|
23
|
-
__version__ = "2.
|
|
24
|
+
__version__ = "2.4.0"
|
emu_base/pulser_adapter.py
CHANGED
|
@@ -1,242 +1,90 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Sequence
|
|
2
2
|
from enum import Enum
|
|
3
3
|
import torch
|
|
4
4
|
import math
|
|
5
5
|
import pulser
|
|
6
|
+
from pulser.sampler import SequenceSamples
|
|
6
7
|
from pulser.noise_model import NoiseModel
|
|
7
|
-
from pulser.register.base_register import
|
|
8
|
+
from pulser.register.base_register import QubitId
|
|
8
9
|
from pulser.backend.config import EmulationConfig
|
|
10
|
+
from pulser._hamiltonian_data import HamiltonianData
|
|
9
11
|
from emu_base.jump_lindblad_operators import get_lindblad_operators
|
|
10
12
|
|
|
11
|
-
KB_PER_RUBIDIUM_MASS = 95.17241379310344 # J/K/kg
|
|
12
|
-
KEFF = 8.7 # µm^-1, conversion from atom velocity to detuning
|
|
13
|
-
|
|
14
13
|
|
|
15
14
|
class HamiltonianType(Enum):
|
|
16
15
|
Rydberg = 1
|
|
17
16
|
XY = 2
|
|
18
17
|
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
HamiltonianType.Rydberg: {
|
|
22
|
-
"amplitude",
|
|
23
|
-
"dephasing",
|
|
24
|
-
"relaxation",
|
|
25
|
-
"depolarizing",
|
|
26
|
-
"doppler",
|
|
27
|
-
"eff_noise",
|
|
28
|
-
"SPAM",
|
|
29
|
-
# "leakage",
|
|
30
|
-
},
|
|
31
|
-
HamiltonianType.XY: {
|
|
32
|
-
"dephasing",
|
|
33
|
-
"depolarizing",
|
|
34
|
-
"eff_noise",
|
|
35
|
-
"SPAM",
|
|
36
|
-
}, # , "leakage"},
|
|
37
|
-
}
|
|
19
|
+
_NON_LINDBLADIAN_NOISE = {"SPAM", "doppler", "amplitude", "detuning", "register"}
|
|
38
20
|
|
|
39
21
|
|
|
40
|
-
def
|
|
41
|
-
|
|
22
|
+
def _get_all_lindblad_noise_operators(
|
|
23
|
+
noise_model: NoiseModel | None,
|
|
42
24
|
) -> list[torch.Tensor]:
|
|
43
|
-
|
|
44
|
-
|
|
25
|
+
if noise_model is None:
|
|
26
|
+
return []
|
|
45
27
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
for
|
|
28
|
+
return [
|
|
29
|
+
op
|
|
30
|
+
for noise_type in noise_model.noise_types
|
|
31
|
+
if noise_type not in _NON_LINDBLADIAN_NOISE
|
|
32
|
+
for op in get_lindblad_operators(noise_type=noise_type, noise_model=noise_model)
|
|
49
33
|
]
|
|
50
|
-
if len(positions[0]) == 2:
|
|
51
|
-
return [torch.cat((position, torch.zeros(1))) for position in positions]
|
|
52
|
-
return positions
|
|
53
|
-
|
|
54
34
|
|
|
55
|
-
def _rydberg_interaction(sequence: pulser.Sequence) -> torch.Tensor:
|
|
56
|
-
"""
|
|
57
|
-
Returns the Rydberg interaction matrix from the qubit positions.
|
|
58
|
-
Uᵢⱼ=C₆/|rᵢ-rⱼ|⁶
|
|
59
35
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
36
|
+
def _get_target_times(
|
|
37
|
+
sequence: pulser.Sequence, config: EmulationConfig, dt: int
|
|
38
|
+
) -> list[int]:
|
|
39
|
+
sequence_duration = sequence.get_duration(include_fall_time=config.with_modulation)
|
|
63
40
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
41
|
+
observable_times = set(range(0, sequence_duration, dt))
|
|
42
|
+
observable_times.add(sequence_duration)
|
|
43
|
+
for obs in config.observables:
|
|
44
|
+
times: Sequence[float]
|
|
45
|
+
if obs.evaluation_times is not None:
|
|
46
|
+
times = obs.evaluation_times
|
|
47
|
+
elif config.default_evaluation_times != "Full":
|
|
48
|
+
times = config.default_evaluation_times.tolist() # type: ignore[union-attr,assignment]
|
|
49
|
+
observable_times |= set([round(time * sequence_duration) for time in times])
|
|
67
50
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
rij = torch.dist(positions[i], positions[j])
|
|
72
|
-
interaction_matrix[[i, j], [j, i]] = c6 / rij**6
|
|
73
|
-
return interaction_matrix
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _xy_interaction(sequence: pulser.Sequence) -> torch.Tensor:
|
|
77
|
-
"""
|
|
78
|
-
Returns the XY interaction matrix from the qubit positions.
|
|
79
|
-
Uᵢⱼ=C₃(1−3cos(𝜃ᵢⱼ)²)/|rᵢ-rⱼ|³
|
|
80
|
-
with
|
|
81
|
-
cos(𝜃ᵢⱼ) = (rᵢ-rⱼ)·m/|m||rᵢ-rⱼ|
|
|
82
|
-
|
|
83
|
-
see Pulser
|
|
84
|
-
[documentation](https://pulser.readthedocs.io/en/stable/conventions.html#interaction-hamiltonian).
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
nqubits = len(sequence.register.qubit_ids)
|
|
88
|
-
c3 = sequence.device.interaction_coeff_xy
|
|
89
|
-
mag_field = torch.tensor(sequence.magnetic_field, dtype=torch.float64)
|
|
90
|
-
mag_field /= mag_field.norm()
|
|
91
|
-
positions = _get_qubit_positions(sequence.register)
|
|
92
|
-
|
|
93
|
-
interaction_matrix = torch.zeros(nqubits, nqubits, dtype=torch.float64)
|
|
94
|
-
for i in range(nqubits):
|
|
95
|
-
for j in range(i + 1, nqubits):
|
|
96
|
-
rij = torch.dist(positions[i], positions[j])
|
|
97
|
-
cos_ij = torch.dot(positions[i] - positions[j], mag_field) / rij
|
|
98
|
-
interaction_matrix[[i, j], [j, i]] = c3 * (1 - 3 * cos_ij**2) / rij**3
|
|
99
|
-
return interaction_matrix
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _get_amp_factors(
|
|
103
|
-
samples: pulser.sampler.SequenceSamples,
|
|
104
|
-
amp_sigma: float,
|
|
105
|
-
laser_waist: float | None,
|
|
106
|
-
qubit_positions: list[torch.Tensor],
|
|
107
|
-
q_ids: tuple[str, ...],
|
|
108
|
-
) -> dict[int, torch.Tensor]:
|
|
109
|
-
def perp_dist(pos: torch.Tensor, axis: torch.Tensor) -> Any:
|
|
110
|
-
return torch.linalg.vector_norm(pos - torch.vdot(pos, axis) * axis)
|
|
111
|
-
|
|
112
|
-
times_to_amp_factors: dict[int, torch.Tensor] = {}
|
|
113
|
-
for ch, ch_samples in samples.channel_samples.items():
|
|
114
|
-
ch_obj = samples._ch_objs[ch]
|
|
115
|
-
prop_dir = torch.tensor(
|
|
116
|
-
ch_obj.propagation_dir or [0.0, 1.0, 0.0], dtype=torch.float64
|
|
117
|
-
)
|
|
118
|
-
prop_dir /= prop_dir.norm()
|
|
119
|
-
|
|
120
|
-
# each channel has a noise on its laser amplitude
|
|
121
|
-
# we assume each channel has the same noise amplitude currently
|
|
122
|
-
# the hardware currently has only a global channel anyway
|
|
123
|
-
sigma_factor = (
|
|
124
|
-
1.0
|
|
125
|
-
if amp_sigma == 0.0
|
|
126
|
-
else torch.max(torch.tensor(0), torch.normal(1.0, amp_sigma, (1,))).item()
|
|
127
|
-
)
|
|
128
|
-
for slot in ch_samples.slots:
|
|
129
|
-
factors = (
|
|
130
|
-
torch.tensor(
|
|
131
|
-
[
|
|
132
|
-
math.exp(-((perp_dist(x, prop_dir) / laser_waist) ** 2))
|
|
133
|
-
for x in qubit_positions
|
|
134
|
-
],
|
|
135
|
-
dtype=torch.float64,
|
|
136
|
-
) # the lasers have a gaussian profile perpendicular to the propagation direction
|
|
137
|
-
if laser_waist and ch_obj.addressing == "Global"
|
|
138
|
-
else torch.ones(
|
|
139
|
-
len(q_ids), dtype=torch.float64
|
|
140
|
-
) # but for a local channel, this does not matter
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
# add the amplitude noise for the targeted qubits
|
|
144
|
-
factors[[x in slot.targets for x in q_ids]] *= sigma_factor
|
|
145
|
-
|
|
146
|
-
for i in range(slot.ti, slot.tf):
|
|
147
|
-
if i in times_to_amp_factors: # multiple local channels at the same time
|
|
148
|
-
# pulser enforces that no two lasers target the same qubit simultaneously
|
|
149
|
-
# so only a single factor will be != 1.0 for each qubit
|
|
150
|
-
times_to_amp_factors[i] = factors * times_to_amp_factors[i]
|
|
151
|
-
else:
|
|
152
|
-
times_to_amp_factors[i] = factors
|
|
153
|
-
return times_to_amp_factors
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def _get_delta_offset(nqubits: int, temperature: float) -> torch.Tensor:
|
|
157
|
-
"""
|
|
158
|
-
The delta values are shifted due to atomic velocities.
|
|
159
|
-
The atomic velocities follow the Maxwell distribution
|
|
160
|
-
https://en.wikipedia.org/wiki/Maxwell%E2%80%93Boltzmann_distribution
|
|
161
|
-
and then a given residual velocity is converted to a delta offset per
|
|
162
|
-
https://en.wikipedia.org/wiki/Doppler_broadening
|
|
163
|
-
"""
|
|
164
|
-
if temperature == 0.0:
|
|
165
|
-
return torch.zeros(nqubits, dtype=torch.float64)
|
|
166
|
-
t = temperature * 1e-6 # microKelvin -> Kelvin
|
|
167
|
-
sigma = KEFF * math.sqrt(KB_PER_RUBIDIUM_MASS * t)
|
|
168
|
-
return torch.normal(0.0, sigma, (nqubits,))
|
|
51
|
+
target_times: list[int] = list(observable_times)
|
|
52
|
+
target_times.sort()
|
|
53
|
+
return target_times
|
|
169
54
|
|
|
170
55
|
|
|
171
56
|
def _extract_omega_delta_phi(
|
|
172
|
-
|
|
173
|
-
|
|
57
|
+
noisy_samples: SequenceSamples,
|
|
58
|
+
qubit_ids: tuple[str, ...],
|
|
174
59
|
target_times: list[int],
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
180
|
-
"""
|
|
181
|
-
Samples the Pulser sequence and returns a tuple of tensors (omega, delta, phi)
|
|
182
|
-
containing:
|
|
183
|
-
- omega[i, q] = amplitude at time i * dt for qubit q
|
|
184
|
-
- delta[i, q] = detuning at time i * dt for qubit q
|
|
185
|
-
- phi[i, q] = phase at time i * dt for qubit q
|
|
186
|
-
|
|
187
|
-
if laser_waist is w_0 != None, the omega values coming from the global pulse channel
|
|
188
|
-
will me modulated as $\\Omega_i=\\Omega_i e^{-r_i^2/w_0^2}$
|
|
189
|
-
"""
|
|
190
|
-
|
|
191
|
-
if with_modulation and sequence._slm_mask_targets:
|
|
192
|
-
raise NotImplementedError(
|
|
193
|
-
"Simulation of sequences combining an SLM mask and output "
|
|
194
|
-
"modulation is not supported."
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
q_ids = sequence.register.qubit_ids
|
|
198
|
-
|
|
199
|
-
samples = pulser.sampler.sample(
|
|
200
|
-
sequence,
|
|
201
|
-
modulation=with_modulation,
|
|
202
|
-
extended_duration=sequence.get_duration(include_fall_time=with_modulation),
|
|
203
|
-
)
|
|
204
|
-
sequence_dict = samples.to_nested_dict(all_local=True, samples_type="tensor")["Local"]
|
|
205
|
-
|
|
206
|
-
if "ground-rydberg" in sequence_dict and len(sequence_dict) == 1:
|
|
207
|
-
locals_a_d_p = sequence_dict["ground-rydberg"]
|
|
208
|
-
elif "XY" in sequence_dict and len(sequence_dict) == 1:
|
|
209
|
-
locals_a_d_p = sequence_dict["XY"]
|
|
210
|
-
else:
|
|
211
|
-
raise ValueError("Only `ground-rydberg` and `mw_global` channels are supported.")
|
|
212
|
-
|
|
60
|
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
61
|
+
sequence_dict = noisy_samples.to_nested_dict(all_local=True, samples_type="tensor")[
|
|
62
|
+
"Local"
|
|
63
|
+
]
|
|
213
64
|
nsamples = len(target_times) - 1
|
|
214
65
|
omega = torch.zeros(
|
|
215
66
|
nsamples,
|
|
216
|
-
len(
|
|
67
|
+
len(qubit_ids),
|
|
217
68
|
dtype=torch.complex128,
|
|
218
69
|
)
|
|
219
|
-
|
|
220
70
|
delta = torch.zeros(
|
|
221
71
|
nsamples,
|
|
222
|
-
len(
|
|
72
|
+
len(qubit_ids),
|
|
223
73
|
dtype=torch.complex128,
|
|
224
74
|
)
|
|
225
75
|
phi = torch.zeros(
|
|
226
76
|
nsamples,
|
|
227
|
-
len(
|
|
77
|
+
len(qubit_ids),
|
|
228
78
|
dtype=torch.complex128,
|
|
229
79
|
)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
times_to_amp_factors = _get_amp_factors(
|
|
233
|
-
samples, amp_sigma, laser_waist, qubit_positions, q_ids
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
omega_1 = torch.zeros_like(omega[0])
|
|
237
|
-
omega_2 = torch.zeros_like(omega[0])
|
|
238
|
-
max_duration = sequence.get_duration(include_fall_time=with_modulation)
|
|
80
|
+
max_duration = noisy_samples.max_duration
|
|
239
81
|
|
|
82
|
+
if "ground-rydberg" in sequence_dict and len(sequence_dict) == 1:
|
|
83
|
+
locals_a_d_p = sequence_dict["ground-rydberg"]
|
|
84
|
+
elif "XY" in sequence_dict and len(sequence_dict) == 1:
|
|
85
|
+
locals_a_d_p = sequence_dict["XY"]
|
|
86
|
+
else:
|
|
87
|
+
raise ValueError("Only `ground-rydberg` and `mw_global` channels are supported.")
|
|
240
88
|
for i in range(nsamples):
|
|
241
89
|
t = (target_times[i] + target_times[i + 1]) / 2
|
|
242
90
|
# The sampled values correspond to the start of each interval
|
|
@@ -246,23 +94,20 @@ def _extract_omega_delta_phi(
|
|
|
246
94
|
# Note that for dt even, t1=t2
|
|
247
95
|
t1 = math.floor(t)
|
|
248
96
|
t2 = math.ceil(t)
|
|
249
|
-
for q_pos, q_id in enumerate(
|
|
250
|
-
|
|
251
|
-
|
|
97
|
+
for q_pos, q_id in enumerate(qubit_ids):
|
|
98
|
+
omega[i, q_pos] = (
|
|
99
|
+
locals_a_d_p[q_id]["amp"][t1] + locals_a_d_p[q_id]["amp"][t2]
|
|
100
|
+
) / 2.0
|
|
252
101
|
delta[i, q_pos] = (
|
|
253
102
|
locals_a_d_p[q_id]["det"][t1] + locals_a_d_p[q_id]["det"][t2]
|
|
254
103
|
) / 2.0
|
|
255
104
|
phi[i, q_pos] = (
|
|
256
105
|
locals_a_d_p[q_id]["phase"][t1] + locals_a_d_p[q_id]["phase"][t2]
|
|
257
106
|
) / 2.0
|
|
258
|
-
# omegas at different times need to have the laser waist applied independently
|
|
259
|
-
omega_1 *= times_to_amp_factors.get(t1, 1.0)
|
|
260
|
-
omega_2 *= times_to_amp_factors.get(t2, 1.0)
|
|
261
|
-
omega[i] = 0.5 * (omega_1 + omega_2)
|
|
262
107
|
else:
|
|
263
108
|
# We're in the final step and dt=1, approximate this using linear extrapolation
|
|
264
109
|
# we can reuse omega_1 and omega_2 from before
|
|
265
|
-
for q_pos, q_id in enumerate(
|
|
110
|
+
for q_pos, q_id in enumerate(qubit_ids):
|
|
266
111
|
delta[i, q_pos] = (
|
|
267
112
|
3.0 * locals_a_d_p[q_id]["det"][t2] - locals_a_d_p[q_id]["det"][t1]
|
|
268
113
|
) / 2.0
|
|
@@ -270,30 +115,14 @@ def _extract_omega_delta_phi(
|
|
|
270
115
|
3.0 * locals_a_d_p[q_id]["phase"][t2]
|
|
271
116
|
- locals_a_d_p[q_id]["phase"][t1]
|
|
272
117
|
) / 2.0
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
118
|
+
omega[i, q_pos] = max(
|
|
119
|
+
(3.0 * locals_a_d_p[q_id]["amp"][t2] - locals_a_d_p[q_id]["amp"][t1])
|
|
120
|
+
/ 2.0,
|
|
121
|
+
0.0,
|
|
122
|
+
)
|
|
277
123
|
return omega, delta, phi
|
|
278
124
|
|
|
279
125
|
|
|
280
|
-
_NON_LINDBLADIAN_NOISE = {"SPAM", "doppler", "amplitude"}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def _get_all_lindblad_noise_operators(
|
|
284
|
-
noise_model: NoiseModel | None,
|
|
285
|
-
) -> list[torch.Tensor]:
|
|
286
|
-
if noise_model is None:
|
|
287
|
-
return []
|
|
288
|
-
|
|
289
|
-
return [
|
|
290
|
-
op
|
|
291
|
-
for noise_type in noise_model.noise_types
|
|
292
|
-
if noise_type not in _NON_LINDBLADIAN_NOISE
|
|
293
|
-
for op in get_lindblad_operators(noise_type=noise_type, noise_model=noise_model)
|
|
294
|
-
]
|
|
295
|
-
|
|
296
|
-
|
|
297
126
|
class PulserData:
|
|
298
127
|
slm_end_time: float
|
|
299
128
|
full_interaction_matrix: torch.Tensor
|
|
@@ -308,51 +137,24 @@ class PulserData:
|
|
|
308
137
|
def __init__(self, *, sequence: pulser.Sequence, config: EmulationConfig, dt: int):
|
|
309
138
|
self.qubit_ids = sequence.register.qubit_ids
|
|
310
139
|
self.qubit_count = len(self.qubit_ids)
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if obs.evaluation_times is not None:
|
|
318
|
-
times = obs.evaluation_times
|
|
319
|
-
elif config.default_evaluation_times != "Full":
|
|
320
|
-
times = (
|
|
321
|
-
config.default_evaluation_times.tolist() # type: ignore[union-attr,assignment]
|
|
322
|
-
)
|
|
323
|
-
observable_times |= set([round(time * sequence_duration) for time in times])
|
|
324
|
-
|
|
325
|
-
self.target_times: list[int] = list(observable_times)
|
|
326
|
-
self.target_times.sort()
|
|
140
|
+
self.target_times = _get_target_times(sequence=sequence, config=config, dt=dt)
|
|
141
|
+
self.hamiltonian = HamiltonianData.from_sequence(
|
|
142
|
+
sequence,
|
|
143
|
+
with_modulation=config.with_modulation,
|
|
144
|
+
noise_model=config.noise_model,
|
|
145
|
+
)
|
|
327
146
|
|
|
328
|
-
laser_waist = config.noise_model.laser_waist
|
|
329
|
-
amp_sigma = config.noise_model.amp_sigma
|
|
330
|
-
temperature = config.noise_model.temperature
|
|
331
147
|
self.omega, self.delta, self.phi = _extract_omega_delta_phi(
|
|
332
|
-
|
|
333
|
-
target_times=self.target_times,
|
|
334
|
-
with_modulation=config.with_modulation,
|
|
335
|
-
laser_waist=laser_waist,
|
|
336
|
-
amp_sigma=amp_sigma,
|
|
337
|
-
temperature=temperature,
|
|
148
|
+
self.hamiltonian.noisy_samples, self.qubit_ids, self.target_times
|
|
338
149
|
)
|
|
339
150
|
|
|
340
|
-
|
|
341
|
-
if
|
|
151
|
+
int_type = self.hamiltonian.interaction_type
|
|
152
|
+
if int_type == "ising": # for local and global
|
|
342
153
|
self.hamiltonian_type = HamiltonianType.Rydberg
|
|
343
|
-
elif
|
|
154
|
+
elif int_type == "XY":
|
|
344
155
|
self.hamiltonian_type = HamiltonianType.XY
|
|
345
156
|
else:
|
|
346
|
-
raise ValueError(f"Unsupported basis: {
|
|
347
|
-
|
|
348
|
-
not_supported = (
|
|
349
|
-
set(config.noise_model.noise_types) - SUPPORTED_NOISES[self.hamiltonian_type]
|
|
350
|
-
)
|
|
351
|
-
if not_supported:
|
|
352
|
-
raise NotImplementedError(
|
|
353
|
-
f"Interaction mode '{self.hamiltonian_type}' does not support "
|
|
354
|
-
f"simulation of noise types: {', '.join(not_supported)}."
|
|
355
|
-
)
|
|
157
|
+
raise ValueError(f"Unsupported basis: {int_type}")
|
|
356
158
|
|
|
357
159
|
self.lindblad_ops = _get_all_lindblad_noise_operators(config.noise_model)
|
|
358
160
|
self.has_lindblad_noise: bool = self.lindblad_ops != []
|
|
@@ -364,10 +166,11 @@ class PulserData:
|
|
|
364
166
|
)
|
|
365
167
|
|
|
366
168
|
self.full_interaction_matrix = config.interaction_matrix.as_tensor()
|
|
367
|
-
|
|
368
|
-
self.full_interaction_matrix =
|
|
369
|
-
|
|
370
|
-
|
|
169
|
+
else:
|
|
170
|
+
self.full_interaction_matrix = (
|
|
171
|
+
self.hamiltonian.noisy_interaction_matrix.as_tensor()
|
|
172
|
+
)
|
|
173
|
+
|
|
371
174
|
self.full_interaction_matrix[
|
|
372
175
|
torch.abs(self.full_interaction_matrix) < config.interaction_cutoff
|
|
373
176
|
] = 0.0
|
emu_base/utils.py
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
from collections import Counter
|
|
2
|
+
import random
|
|
1
3
|
import torch
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
unix_like = os.name != "nt"
|
|
7
|
+
if unix_like:
|
|
8
|
+
from resource import RUSAGE_SELF, getrusage
|
|
2
9
|
|
|
3
10
|
|
|
4
11
|
def deallocate_tensor(t: torch.Tensor) -> None:
|
|
@@ -33,3 +40,53 @@ def deallocate_tensor(t: torch.Tensor) -> None:
|
|
|
33
40
|
if t._base is not None:
|
|
34
41
|
t._base.resize_(0)
|
|
35
42
|
t._base.set_(source=replacement_storage)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_max_rss(gpu: bool) -> float:
|
|
46
|
+
if gpu:
|
|
47
|
+
max_mem_per_device = (
|
|
48
|
+
torch.cuda.max_memory_allocated(device) * 1e-6
|
|
49
|
+
for device in range(torch.cuda.device_count())
|
|
50
|
+
)
|
|
51
|
+
max_mem = max(max_mem_per_device)
|
|
52
|
+
elif unix_like:
|
|
53
|
+
max_mem = getrusage(RUSAGE_SELF).ru_maxrss * 1e-3
|
|
54
|
+
else:
|
|
55
|
+
return 0.0
|
|
56
|
+
return max_mem
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def readout_with_error(c: str, *, p_false_pos: float, p_false_neg: float) -> str:
|
|
60
|
+
# p_false_pos = false positive, p_false_neg = false negative
|
|
61
|
+
r = random.random()
|
|
62
|
+
if c == "0" and r < p_false_pos:
|
|
63
|
+
return "1"
|
|
64
|
+
|
|
65
|
+
if c == "1" and r < p_false_neg:
|
|
66
|
+
return "0"
|
|
67
|
+
|
|
68
|
+
return c
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def apply_measurement_errors(
|
|
72
|
+
bitstrings: Counter[str], *, p_false_pos: float, p_false_neg: float
|
|
73
|
+
) -> Counter[str]:
|
|
74
|
+
"""
|
|
75
|
+
Given a bag of sampled bitstrings, returns another bag of bitstrings
|
|
76
|
+
sampled with readout/measurement errors.
|
|
77
|
+
|
|
78
|
+
p_false_pos: probability of false positive
|
|
79
|
+
p_false_neg: probability of false negative
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
result: Counter[str] = Counter()
|
|
83
|
+
for bitstring, count in bitstrings.items():
|
|
84
|
+
for _ in range(count):
|
|
85
|
+
bitstring_with_error = "".join(
|
|
86
|
+
readout_with_error(c, p_false_pos=p_false_pos, p_false_neg=p_false_neg)
|
|
87
|
+
for c in bitstring
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
result[bitstring_with_error] += 1
|
|
91
|
+
|
|
92
|
+
return result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: emu-base
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: Pasqal base classes for emulators
|
|
5
5
|
Project-URL: Documentation, https://pasqal-io.github.io/emulators/
|
|
6
6
|
Project-URL: Repository, https://github.com/pasqal-io/emulators
|
|
@@ -25,8 +25,8 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
25
25
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
26
26
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
27
27
|
Requires-Python: >=3.10
|
|
28
|
-
Requires-Dist: pulser-core==1.
|
|
29
|
-
Requires-Dist: torch==2.
|
|
28
|
+
Requires-Dist: pulser-core==1.6.*
|
|
29
|
+
Requires-Dist: torch==2.8.0
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
|
|
32
32
|
<div align="center">
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
emu_base/__init__.py,sha256=
|
|
2
|
-
emu_base/aggregators.py,sha256=bB-rldoDAErxQMpL715K5lpiabGOpkCY0GyxW7mfHuc,5000
|
|
1
|
+
emu_base/__init__.py,sha256=eMd_qHkRgYxhb7FHPekZkxCr7JENqGH1EtqRhP98KzA,725
|
|
3
2
|
emu_base/constants.py,sha256=41LYkKLUCz-oxPbd-j7nUDZuhIbUrnez6prT0uR0jcE,56
|
|
4
3
|
emu_base/jump_lindblad_operators.py,sha256=Y30f8emVFS4Dazljc_Rh4lX9qU4QQY_AxPNahnzcsfY,2101
|
|
5
|
-
emu_base/pulser_adapter.py,sha256=
|
|
6
|
-
emu_base/utils.py,sha256
|
|
4
|
+
emu_base/pulser_adapter.py,sha256=ss3EIgIq_hZJpcbwT5FBhQ2dxXqyncQJ8fQrDsAZ2qM,6965
|
|
5
|
+
emu_base/utils.py,sha256=OoJ0GjL1K8m9Jq-BeBfmbWuFp9bvwXp8UcI3BMW94Js,2974
|
|
7
6
|
emu_base/math/__init__.py,sha256=6BbIytYV5uC-e5jLMtIErkcUl_PvfSNnhmVFY9Il8uQ,97
|
|
8
7
|
emu_base/math/brents_root_finding.py,sha256=AVx6L1Il6rpPJWrLJ7cn6oNmJyZOPRgEaaZaubC9lsU,3711
|
|
9
8
|
emu_base/math/double_krylov.py,sha256=X16dyCbyzdP7fFK-hmKS03Q-DJtC6TZ8sJrGTJ6akIc,3708
|
|
10
|
-
emu_base/math/krylov_energy_min.py,sha256=
|
|
9
|
+
emu_base/math/krylov_energy_min.py,sha256=iR4hmE0eXptbAg3opikd5d4Zv7dhnDrawH-n_4KG-cc,4009
|
|
11
10
|
emu_base/math/krylov_exp.py,sha256=mGFddVQ8mEbwypbZtnlRPFpi4Nf8JZT6OKLHloIwCDQ,3934
|
|
12
11
|
emu_base/math/matmul.py,sha256=lEAnV0b5z_f1xEA-9p-WXxA8bM3QbShiHdXQ3ZkZFcQ,877
|
|
13
|
-
emu_base-2.
|
|
14
|
-
emu_base-2.
|
|
15
|
-
emu_base-2.
|
|
12
|
+
emu_base-2.4.0.dist-info/METADATA,sha256=q4KRKKVsXCzNSsDjSNmeMAZDsNmxufLy9kSSsxHPdTc,3604
|
|
13
|
+
emu_base-2.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
+
emu_base-2.4.0.dist-info/RECORD,,
|
emu_base/aggregators.py
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import statistics
|
|
2
|
-
import torch
|
|
3
|
-
from typing import Any, Callable
|
|
4
|
-
import collections
|
|
5
|
-
from enum import Enum, auto
|
|
6
|
-
from pulser.backend import (
|
|
7
|
-
Results,
|
|
8
|
-
)
|
|
9
|
-
import logging
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
_NUMERIC_TYPES = {int, float, complex}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class AggregationType(Enum):
|
|
16
|
-
"""
|
|
17
|
-
Defines how to combine multiple values from different simulation results.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
MEAN = auto() # statistics.fmean or list/matrix-wise equivalent
|
|
21
|
-
BAG_UNION = auto() # Counter.__add__
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def mean_aggregator(
|
|
25
|
-
values: list[Any],
|
|
26
|
-
) -> (
|
|
27
|
-
complex
|
|
28
|
-
| float
|
|
29
|
-
| list[complex]
|
|
30
|
-
| list[float]
|
|
31
|
-
| list[list[complex]]
|
|
32
|
-
| list[list[float]]
|
|
33
|
-
| torch.Tensor
|
|
34
|
-
): # FIXME: support tuples?
|
|
35
|
-
if values == []:
|
|
36
|
-
raise ValueError("Cannot average 0 samples")
|
|
37
|
-
|
|
38
|
-
element_type = type(values[0])
|
|
39
|
-
|
|
40
|
-
if element_type in _NUMERIC_TYPES:
|
|
41
|
-
return statistics.fmean(values)
|
|
42
|
-
|
|
43
|
-
if element_type == torch.Tensor:
|
|
44
|
-
acc = torch.zeros_like(values[0])
|
|
45
|
-
for ten in values:
|
|
46
|
-
acc += ten
|
|
47
|
-
return acc / len(values)
|
|
48
|
-
|
|
49
|
-
if element_type != list:
|
|
50
|
-
raise NotImplementedError("Cannot average this type of data")
|
|
51
|
-
|
|
52
|
-
if values[0] == []:
|
|
53
|
-
raise ValueError("Cannot average list of empty lists")
|
|
54
|
-
|
|
55
|
-
sub_element_type = type(values[0][0])
|
|
56
|
-
|
|
57
|
-
if sub_element_type in _NUMERIC_TYPES:
|
|
58
|
-
dim = len(values[0])
|
|
59
|
-
return [statistics.fmean(value[i] for value in values) for i in range(dim)]
|
|
60
|
-
|
|
61
|
-
if sub_element_type != list: # FIXME: ABC.Iterable? Collection? subclass?
|
|
62
|
-
raise ValueError(f"Cannot average list of lists of {sub_element_type}")
|
|
63
|
-
|
|
64
|
-
if values[0][0] == []:
|
|
65
|
-
raise ValueError("Cannot average list of matrices with no columns")
|
|
66
|
-
|
|
67
|
-
if (sub_sub_element_type := type(values[0][0][0])) not in _NUMERIC_TYPES:
|
|
68
|
-
raise ValueError(f"Cannot average list of matrices of {sub_sub_element_type}")
|
|
69
|
-
|
|
70
|
-
dim1 = len(values[0])
|
|
71
|
-
dim2 = len(values[0][0])
|
|
72
|
-
return [
|
|
73
|
-
[statistics.fmean(value[i][j] for value in values) for j in range(dim2)]
|
|
74
|
-
for i in range(dim1)
|
|
75
|
-
]
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def bag_union_aggregator(values: list[collections.Counter]) -> collections.Counter:
|
|
79
|
-
return sum(values, start=collections.Counter())
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
aggregation_types_definitions: dict[AggregationType, Callable] = {
|
|
83
|
-
AggregationType.MEAN: mean_aggregator,
|
|
84
|
-
AggregationType.BAG_UNION: bag_union_aggregator,
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _get_aggregation_type(tag: str) -> AggregationType | None:
|
|
89
|
-
if tag.startswith("bitstrings"):
|
|
90
|
-
return AggregationType.BAG_UNION
|
|
91
|
-
if tag.startswith("expectation"):
|
|
92
|
-
return AggregationType.MEAN
|
|
93
|
-
if tag.startswith("fidelity"):
|
|
94
|
-
return AggregationType.MEAN
|
|
95
|
-
if tag.startswith("correlation_matrix"):
|
|
96
|
-
return AggregationType.MEAN
|
|
97
|
-
if tag.startswith("occupation"):
|
|
98
|
-
return AggregationType.MEAN
|
|
99
|
-
if tag.startswith("energy"):
|
|
100
|
-
return AggregationType.MEAN
|
|
101
|
-
if tag.startswith("energy_second_moment"):
|
|
102
|
-
return AggregationType.MEAN
|
|
103
|
-
else:
|
|
104
|
-
return None
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def aggregate(
|
|
108
|
-
results_to_aggregate: list[Results],
|
|
109
|
-
**aggregator_functions: Callable[[Any], Any],
|
|
110
|
-
) -> Results:
|
|
111
|
-
if len(results_to_aggregate) == 0:
|
|
112
|
-
raise ValueError("no results to aggregate")
|
|
113
|
-
if len(results_to_aggregate) == 1:
|
|
114
|
-
return results_to_aggregate[0]
|
|
115
|
-
stored_callbacks = set(results_to_aggregate[0].get_result_tags())
|
|
116
|
-
if not all(
|
|
117
|
-
set(results.get_result_tags()) == stored_callbacks
|
|
118
|
-
for results in results_to_aggregate
|
|
119
|
-
):
|
|
120
|
-
raise ValueError(
|
|
121
|
-
"Monte-Carlo results seem to provide from incompatible simulations: "
|
|
122
|
-
"they do not all contain the same observables"
|
|
123
|
-
)
|
|
124
|
-
aggregated = Results(
|
|
125
|
-
atom_order=results_to_aggregate[0].atom_order,
|
|
126
|
-
total_duration=results_to_aggregate[0].total_duration,
|
|
127
|
-
)
|
|
128
|
-
for tag in stored_callbacks:
|
|
129
|
-
aggregation_type = aggregator_functions.get(
|
|
130
|
-
tag,
|
|
131
|
-
_get_aggregation_type(tag),
|
|
132
|
-
)
|
|
133
|
-
if aggregation_type is None:
|
|
134
|
-
logging.getLogger("global_logger").warning(f"Skipping aggregation of `{tag}`")
|
|
135
|
-
continue
|
|
136
|
-
aggregation_function: Any = (
|
|
137
|
-
aggregation_type
|
|
138
|
-
if callable(aggregation_type)
|
|
139
|
-
else aggregation_types_definitions[aggregation_type]
|
|
140
|
-
)
|
|
141
|
-
evaluation_times = results_to_aggregate[0].get_result_times(tag)
|
|
142
|
-
if not all(
|
|
143
|
-
results.get_result_times(tag) == evaluation_times
|
|
144
|
-
for results in results_to_aggregate
|
|
145
|
-
):
|
|
146
|
-
raise ValueError(
|
|
147
|
-
"Monte-Carlo results seem to provide from incompatible simulations: "
|
|
148
|
-
"the callbacks are not stored at the same times"
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
uuid = results_to_aggregate[0]._find_uuid(tag)
|
|
152
|
-
for t in results_to_aggregate[0].get_result_times(tag):
|
|
153
|
-
v = aggregation_function(
|
|
154
|
-
[result.get_result(tag, t) for result in results_to_aggregate]
|
|
155
|
-
)
|
|
156
|
-
aggregated._store_raw(uuid=uuid, tag=tag, time=t, value=v)
|
|
157
|
-
|
|
158
|
-
return aggregated
|
|
File without changes
|