emu-base 2.1.1__py3-none-any.whl → 2.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.
- emu_base/__init__.py +5 -1
- emu_base/math/krylov_energy_min.py +130 -0
- emu_base/math/krylov_exp.py +7 -3
- emu_base/math/matmul.py +32 -0
- emu_base/pulser_adapter.py +164 -77
- emu_base/utils.py +30 -4
- {emu_base-2.1.1.dist-info → emu_base-2.2.1.dist-info}/METADATA +3 -3
- emu_base-2.2.1.dist-info/RECORD +15 -0
- emu_base-2.1.1.dist-info/RECORD +0 -13
- {emu_base-2.1.1.dist-info → emu_base-2.2.1.dist-info}/WHEEL +0 -0
emu_base/__init__.py
CHANGED
|
@@ -2,10 +2,14 @@ from .constants import DEVICE_COUNT
|
|
|
2
2
|
from .pulser_adapter import PulserData, HamiltonianType
|
|
3
3
|
from .math.brents_root_finding import find_root_brents
|
|
4
4
|
from .math.krylov_exp import krylov_exp, DEFAULT_MAX_KRYLOV_DIM
|
|
5
|
+
from .jump_lindblad_operators import compute_noise_from_lindbladians
|
|
6
|
+
from .math.matmul import matmul_2x2_with_batched
|
|
5
7
|
from .aggregators import AggregationType, aggregate
|
|
6
8
|
|
|
7
9
|
__all__ = [
|
|
8
10
|
"__version__",
|
|
11
|
+
"compute_noise_from_lindbladians",
|
|
12
|
+
"matmul_2x2_with_batched",
|
|
9
13
|
"AggregationType",
|
|
10
14
|
"aggregate",
|
|
11
15
|
"PulserData",
|
|
@@ -16,4 +20,4 @@ __all__ = [
|
|
|
16
20
|
"DEVICE_COUNT",
|
|
17
21
|
]
|
|
18
22
|
|
|
19
|
-
__version__ = "2.
|
|
23
|
+
__version__ = "2.2.1"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from typing import Callable, Tuple
|
|
3
|
+
|
|
4
|
+
DEFAULT_MAX_KRYLOV_DIM: int = 100
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _lowest_eigen_pair(
|
|
8
|
+
T_trunc: torch.Tensor,
|
|
9
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
10
|
+
"""
|
|
11
|
+
Return the lowest eigenpair of the hermitian matrix T_trunc.
|
|
12
|
+
"""
|
|
13
|
+
eig_energy, eig_state = torch.linalg.eigh(T_trunc)
|
|
14
|
+
return eig_energy[0], eig_state[:, 0]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class KrylovEnergyResult:
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
ground_state: torch.Tensor,
|
|
21
|
+
ground_energy: float,
|
|
22
|
+
converged: bool,
|
|
23
|
+
happy_breakdown: bool,
|
|
24
|
+
iteration_count: int,
|
|
25
|
+
):
|
|
26
|
+
self.ground_state = ground_state
|
|
27
|
+
self.ground_energy = ground_energy
|
|
28
|
+
self.converged = converged
|
|
29
|
+
self.happy_breakdown = happy_breakdown
|
|
30
|
+
self.iteration_count = iteration_count
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def krylov_energy_minimization_impl(
|
|
34
|
+
op: Callable[[torch.Tensor], torch.Tensor],
|
|
35
|
+
psi_local: torch.Tensor,
|
|
36
|
+
residual_tolerance: float,
|
|
37
|
+
norm_tolerance: float,
|
|
38
|
+
max_krylov_dim: int = DEFAULT_MAX_KRYLOV_DIM,
|
|
39
|
+
) -> KrylovEnergyResult:
|
|
40
|
+
"""
|
|
41
|
+
Computes the ground state of a Hermitian operator using Lanczos algorithm.
|
|
42
|
+
The Rayleigh quotient ⟨ψ|H|ψ⟩ is minimized over the Krylov subspace.
|
|
43
|
+
|
|
44
|
+
The convergence of the results is determined by a residual norm criterion or a happy breakdown.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
device = psi_local.device
|
|
48
|
+
dtype = psi_local.dtype
|
|
49
|
+
|
|
50
|
+
initial_norm = psi_local.norm()
|
|
51
|
+
lanczos_vectors = [psi_local / initial_norm]
|
|
52
|
+
T = torch.zeros(max_krylov_dim + 2, max_krylov_dim + 2, dtype=dtype, device=device)
|
|
53
|
+
|
|
54
|
+
converged = False
|
|
55
|
+
happy_breakdown = False
|
|
56
|
+
iteration_count = 0
|
|
57
|
+
|
|
58
|
+
for j in range(max_krylov_dim):
|
|
59
|
+
w = op(lanczos_vectors[-1])
|
|
60
|
+
|
|
61
|
+
for k in range(max(0, j - 1), j + 1):
|
|
62
|
+
alpha = torch.tensordot(lanczos_vectors[k].conj(), w, dims=w.dim())
|
|
63
|
+
T[k, j] = alpha
|
|
64
|
+
w = w - alpha * lanczos_vectors[k]
|
|
65
|
+
|
|
66
|
+
beta = w.norm()
|
|
67
|
+
T[j + 1, j] = beta
|
|
68
|
+
|
|
69
|
+
effective_dim = len(lanczos_vectors)
|
|
70
|
+
size = effective_dim + (0 if beta < norm_tolerance else 1)
|
|
71
|
+
T_truncated = T[:size, :size]
|
|
72
|
+
|
|
73
|
+
ground_energy, ground_eigenvector = _lowest_eigen_pair(
|
|
74
|
+
T_truncated
|
|
75
|
+
) # in Krylov subspace
|
|
76
|
+
iteration_count = j + 1
|
|
77
|
+
|
|
78
|
+
# happy breakdown check
|
|
79
|
+
if beta < norm_tolerance:
|
|
80
|
+
final_state = sum(
|
|
81
|
+
c * vec for c, vec in zip(ground_eigenvector, lanczos_vectors)
|
|
82
|
+
)
|
|
83
|
+
final_state = final_state / final_state.norm()
|
|
84
|
+
happy_breakdown = True
|
|
85
|
+
converged = True
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
# Reconstruct final state in original Hilbert space
|
|
89
|
+
lanczos_vectors.append(w / beta)
|
|
90
|
+
final_state = sum(c * vec for c, vec in zip(ground_eigenvector, lanczos_vectors))
|
|
91
|
+
final_state = final_state / final_state.norm()
|
|
92
|
+
|
|
93
|
+
# residual norm convergence check
|
|
94
|
+
residual_norm = torch.norm(op(final_state) - ground_energy * final_state)
|
|
95
|
+
if residual_norm < residual_tolerance:
|
|
96
|
+
happy_breakdown = False
|
|
97
|
+
converged = True
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
return KrylovEnergyResult(
|
|
101
|
+
ground_state=final_state,
|
|
102
|
+
ground_energy=ground_energy.item(),
|
|
103
|
+
converged=converged,
|
|
104
|
+
happy_breakdown=happy_breakdown,
|
|
105
|
+
iteration_count=iteration_count,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def krylov_energy_minimization(
|
|
110
|
+
op: Callable[[torch.Tensor], torch.Tensor],
|
|
111
|
+
v: torch.Tensor,
|
|
112
|
+
norm_tolerance: float,
|
|
113
|
+
residual_tolerance: float,
|
|
114
|
+
max_krylov_dim: int = DEFAULT_MAX_KRYLOV_DIM,
|
|
115
|
+
) -> Tuple[torch.Tensor, float]:
|
|
116
|
+
|
|
117
|
+
result = krylov_energy_minimization_impl(
|
|
118
|
+
op=op,
|
|
119
|
+
psi_local=v,
|
|
120
|
+
norm_tolerance=norm_tolerance,
|
|
121
|
+
residual_tolerance=residual_tolerance,
|
|
122
|
+
max_krylov_dim=max_krylov_dim,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if not result.converged and not result.happy_breakdown:
|
|
126
|
+
raise RecursionError(
|
|
127
|
+
"Krylov ground state solver did not converge within allotted iterations."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return result.ground_state, result.ground_energy
|
emu_base/math/krylov_exp.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Callable
|
|
2
2
|
import torch
|
|
3
3
|
|
|
4
|
+
|
|
4
5
|
DEFAULT_MAX_KRYLOV_DIM: int = 100
|
|
5
6
|
|
|
6
7
|
|
|
@@ -36,11 +37,14 @@ def krylov_exp_impl(
|
|
|
36
37
|
Convergence is checked using the exponential of the "extended T matrix", a criterion
|
|
37
38
|
described in "Expokit: A Software Package for Computing Matrix Exponentials"
|
|
38
39
|
(https://www.maths.uq.edu.au/expokit/paper.pdf).
|
|
40
|
+
|
|
41
|
+
The input tensor object `v` becomes invalid after calling that function.
|
|
39
42
|
"""
|
|
40
43
|
|
|
41
44
|
initial_norm = v.norm()
|
|
45
|
+
v /= initial_norm
|
|
42
46
|
|
|
43
|
-
lanczos_vectors = [v
|
|
47
|
+
lanczos_vectors = [v]
|
|
44
48
|
T = torch.zeros(max_krylov_dim + 2, max_krylov_dim + 2, dtype=v.dtype)
|
|
45
49
|
|
|
46
50
|
for j in range(max_krylov_dim):
|
|
@@ -52,7 +56,7 @@ def krylov_exp_impl(
|
|
|
52
56
|
for k in range(k_start, j + 1):
|
|
53
57
|
overlap = torch.tensordot(lanczos_vectors[k].conj(), w, dims=w.dim())
|
|
54
58
|
T[k, j] = overlap
|
|
55
|
-
w
|
|
59
|
+
w -= overlap * lanczos_vectors[k]
|
|
56
60
|
|
|
57
61
|
n2 = w.norm()
|
|
58
62
|
T[j + 1, j] = n2
|
|
@@ -67,7 +71,7 @@ def krylov_exp_impl(
|
|
|
67
71
|
result=result, converged=True, happy_breakdown=True, iteration_count=j + 1
|
|
68
72
|
)
|
|
69
73
|
|
|
70
|
-
w
|
|
74
|
+
w /= n2
|
|
71
75
|
lanczos_vectors.append(w)
|
|
72
76
|
|
|
73
77
|
# Compute exponential of extended T matrix
|
emu_base/math/matmul.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def matmul_2x2_with_batched(left: torch.Tensor, right: torch.Tensor) -> torch.Tensor:
|
|
5
|
+
result = torch.zeros_like(right)
|
|
6
|
+
zero = torch.tensor(0, device=right.device)
|
|
7
|
+
one = torch.tensor(1, device=right.device)
|
|
8
|
+
result = result.index_add_(
|
|
9
|
+
1,
|
|
10
|
+
zero,
|
|
11
|
+
right.select(1, 0).unsqueeze(1),
|
|
12
|
+
alpha=left[0, 0], # type: ignore [arg-type]
|
|
13
|
+
)
|
|
14
|
+
result = result.index_add_(
|
|
15
|
+
1,
|
|
16
|
+
zero,
|
|
17
|
+
right.select(1, 1).unsqueeze(1),
|
|
18
|
+
alpha=left[0, 1], # type: ignore [arg-type]
|
|
19
|
+
)
|
|
20
|
+
result = result.index_add_(
|
|
21
|
+
1,
|
|
22
|
+
one,
|
|
23
|
+
right.select(1, 0).unsqueeze(1),
|
|
24
|
+
alpha=left[1, 0], # type: ignore [arg-type]
|
|
25
|
+
)
|
|
26
|
+
result = result.index_add_(
|
|
27
|
+
1,
|
|
28
|
+
one,
|
|
29
|
+
right.select(1, 1).unsqueeze(1),
|
|
30
|
+
alpha=left[1, 1], # type: ignore [arg-type]
|
|
31
|
+
)
|
|
32
|
+
return result
|
emu_base/pulser_adapter.py
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
from
|
|
1
|
+
from typing import Tuple, Sequence, Any
|
|
2
|
+
from enum import Enum
|
|
3
3
|
import torch
|
|
4
4
|
import math
|
|
5
|
+
import pulser
|
|
5
6
|
from pulser.noise_model import NoiseModel
|
|
6
7
|
from pulser.register.base_register import BaseRegister, QubitId
|
|
7
|
-
from enum import Enum
|
|
8
|
-
|
|
9
8
|
from pulser.backend.config import EmulationConfig
|
|
10
|
-
|
|
11
9
|
from emu_base.jump_lindblad_operators import get_lindblad_operators
|
|
12
|
-
|
|
10
|
+
|
|
11
|
+
KB_PER_RUBIDIUM_MASS = 95.17241379310344 # J/K/kg
|
|
12
|
+
KEFF = 8.7 # µm^-1, conversion from atom velocity to detuning
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class HamiltonianType(Enum):
|
|
@@ -17,14 +17,36 @@ class HamiltonianType(Enum):
|
|
|
17
17
|
XY = 2
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
SUPPORTED_NOISES: dict = {
|
|
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
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
20
40
|
def _get_qubit_positions(
|
|
21
41
|
register: BaseRegister,
|
|
22
42
|
) -> list[torch.Tensor]:
|
|
23
43
|
"""Conversion from pulser Register to emu-mps register (torch type).
|
|
24
44
|
Each element will be given as [Rx,Ry,Rz]"""
|
|
25
45
|
|
|
26
|
-
positions = [
|
|
27
|
-
|
|
46
|
+
positions = [
|
|
47
|
+
position.as_tensor().to(dtype=torch.float64)
|
|
48
|
+
for position in register.qubits.values()
|
|
49
|
+
]
|
|
28
50
|
if len(positions[0]) == 2:
|
|
29
51
|
return [torch.cat((position, torch.zeros(1))) for position in positions]
|
|
30
52
|
return positions
|
|
@@ -32,59 +54,118 @@ def _get_qubit_positions(
|
|
|
32
54
|
|
|
33
55
|
def _rydberg_interaction(sequence: pulser.Sequence) -> torch.Tensor:
|
|
34
56
|
"""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"""
|
|
57
|
+
Returns the Rydberg interaction matrix from the qubit positions.
|
|
58
|
+
Uᵢⱼ=C₆/|rᵢ-rⱼ|⁶
|
|
38
59
|
|
|
39
|
-
|
|
60
|
+
see Pulser
|
|
61
|
+
[documentation](https://pulser.readthedocs.io/en/stable/conventions.html#interaction-hamiltonian).
|
|
62
|
+
"""
|
|
40
63
|
|
|
64
|
+
nqubits = len(sequence.register.qubit_ids)
|
|
41
65
|
c6 = sequence.device.interaction_coeff
|
|
66
|
+
positions = _get_qubit_positions(sequence.register)
|
|
42
67
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
interaction_matrix[numi][numj] = (
|
|
49
|
-
c6 / dist2(qubit_positions[numi], qubit_positions[numj]) ** 3
|
|
50
|
-
)
|
|
51
|
-
interaction_matrix[numj, numi] = interaction_matrix[numi, numj]
|
|
68
|
+
interaction_matrix = torch.zeros(nqubits, nqubits, dtype=torch.float64)
|
|
69
|
+
for i in range(nqubits):
|
|
70
|
+
for j in range(i + 1, nqubits):
|
|
71
|
+
rij = torch.dist(positions[i], positions[j])
|
|
72
|
+
interaction_matrix[[i, j], [j, i]] = c6 / rij**6
|
|
52
73
|
return interaction_matrix
|
|
53
74
|
|
|
54
75
|
|
|
55
76
|
def _xy_interaction(sequence: pulser.Sequence) -> torch.Tensor:
|
|
56
77
|
"""
|
|
57
|
-
|
|
58
|
-
|
|
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).
|
|
59
85
|
"""
|
|
60
|
-
num_qubits = len(sequence.register.qubit_ids)
|
|
61
86
|
|
|
87
|
+
nqubits = len(sequence.register.qubit_ids)
|
|
62
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
|
|
63
100
|
|
|
64
|
-
qubit_positions = _get_qubit_positions(sequence.register)
|
|
65
|
-
interaction_matrix = torch.zeros(num_qubits, num_qubits)
|
|
66
|
-
mag_field = torch.tensor(sequence.magnetic_field) # by default [0.0,0.0,30.0]
|
|
67
|
-
mag_norm = torch.linalg.norm(mag_field)
|
|
68
|
-
|
|
69
|
-
for numi in range(len(qubit_positions)):
|
|
70
|
-
for numj in range(numi + 1, len(qubit_positions)):
|
|
71
|
-
cosine = 0
|
|
72
|
-
if mag_norm >= 1e-8: # selected by hand
|
|
73
|
-
cosine = torch.dot(
|
|
74
|
-
(qubit_positions[numi] - qubit_positions[numj]), mag_field
|
|
75
|
-
) / (
|
|
76
|
-
torch.linalg.norm(qubit_positions[numi] - qubit_positions[numj])
|
|
77
|
-
* mag_norm
|
|
78
|
-
)
|
|
79
101
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
84
141
|
)
|
|
85
|
-
interaction_matrix[numj, numi] = interaction_matrix[numi, numj]
|
|
86
142
|
|
|
87
|
-
|
|
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,))
|
|
88
169
|
|
|
89
170
|
|
|
90
171
|
def _extract_omega_delta_phi(
|
|
@@ -93,6 +174,8 @@ def _extract_omega_delta_phi(
|
|
|
93
174
|
target_times: list[int],
|
|
94
175
|
with_modulation: bool,
|
|
95
176
|
laser_waist: float | None,
|
|
177
|
+
amp_sigma: float,
|
|
178
|
+
temperature: float,
|
|
96
179
|
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
97
180
|
"""
|
|
98
181
|
Samples the Pulser sequence and returns a tuple of tensors (omega, delta, phi)
|
|
@@ -111,6 +194,8 @@ def _extract_omega_delta_phi(
|
|
|
111
194
|
"modulation is not supported."
|
|
112
195
|
)
|
|
113
196
|
|
|
197
|
+
q_ids = sequence.register.qubit_ids
|
|
198
|
+
|
|
114
199
|
samples = pulser.sampler.sample(
|
|
115
200
|
sequence,
|
|
116
201
|
modulation=with_modulation,
|
|
@@ -123,44 +208,34 @@ def _extract_omega_delta_phi(
|
|
|
123
208
|
elif "XY" in sequence_dict and len(sequence_dict) == 1:
|
|
124
209
|
locals_a_d_p = sequence_dict["XY"]
|
|
125
210
|
else:
|
|
126
|
-
raise ValueError("
|
|
127
|
-
|
|
128
|
-
max_duration = sequence.get_duration(include_fall_time=with_modulation)
|
|
211
|
+
raise ValueError("Only `ground-rydberg` and `mw_global` channels are supported.")
|
|
129
212
|
|
|
130
213
|
nsamples = len(target_times) - 1
|
|
131
214
|
omega = torch.zeros(
|
|
132
215
|
nsamples,
|
|
133
|
-
len(
|
|
216
|
+
len(q_ids),
|
|
134
217
|
dtype=torch.complex128,
|
|
135
218
|
)
|
|
136
219
|
|
|
137
220
|
delta = torch.zeros(
|
|
138
221
|
nsamples,
|
|
139
|
-
len(
|
|
222
|
+
len(q_ids),
|
|
140
223
|
dtype=torch.complex128,
|
|
141
224
|
)
|
|
142
225
|
phi = torch.zeros(
|
|
143
226
|
nsamples,
|
|
144
|
-
len(
|
|
227
|
+
len(q_ids),
|
|
145
228
|
dtype=torch.complex128,
|
|
146
229
|
)
|
|
230
|
+
qubit_positions = _get_qubit_positions(sequence.register)
|
|
147
231
|
|
|
148
|
-
|
|
149
|
-
qubit_positions
|
|
150
|
-
|
|
151
|
-
[math.exp(-((x[:2].norm() / laser_waist) ** 2)) for x in qubit_positions]
|
|
152
|
-
)
|
|
153
|
-
else:
|
|
154
|
-
waist_factors = torch.ones(len(sequence.register.qubit_ids))
|
|
155
|
-
|
|
156
|
-
global_times = set()
|
|
157
|
-
for ch, ch_samples in samples.channel_samples.items():
|
|
158
|
-
if samples._ch_objs[ch].addressing == "Global":
|
|
159
|
-
for slot in ch_samples.slots:
|
|
160
|
-
global_times |= set(i for i in range(slot.ti, slot.tf))
|
|
232
|
+
times_to_amp_factors = _get_amp_factors(
|
|
233
|
+
samples, amp_sigma, laser_waist, qubit_positions, q_ids
|
|
234
|
+
)
|
|
161
235
|
|
|
162
236
|
omega_1 = torch.zeros_like(omega[0])
|
|
163
237
|
omega_2 = torch.zeros_like(omega[0])
|
|
238
|
+
max_duration = sequence.get_duration(include_fall_time=with_modulation)
|
|
164
239
|
|
|
165
240
|
for i in range(nsamples):
|
|
166
241
|
t = (target_times[i] + target_times[i + 1]) / 2
|
|
@@ -169,9 +244,9 @@ def _extract_omega_delta_phi(
|
|
|
169
244
|
if math.ceil(t) < max_duration:
|
|
170
245
|
# If we're not the final step, approximate this using linear interpolation
|
|
171
246
|
# Note that for dt even, t1=t2
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
247
|
+
t1 = math.floor(t)
|
|
248
|
+
t2 = math.ceil(t)
|
|
249
|
+
for q_pos, q_id in enumerate(q_ids):
|
|
175
250
|
omega_1[q_pos] = locals_a_d_p[q_id]["amp"][t1]
|
|
176
251
|
omega_2[q_pos] = locals_a_d_p[q_id]["amp"][t2]
|
|
177
252
|
delta[i, q_pos] = (
|
|
@@ -181,15 +256,13 @@ def _extract_omega_delta_phi(
|
|
|
181
256
|
locals_a_d_p[q_id]["phase"][t1] + locals_a_d_p[q_id]["phase"][t2]
|
|
182
257
|
) / 2.0
|
|
183
258
|
# omegas at different times need to have the laser waist applied independently
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if t2 in global_times:
|
|
187
|
-
omega_2 *= waist_factors
|
|
259
|
+
omega_1 *= times_to_amp_factors.get(t1, 1.0)
|
|
260
|
+
omega_2 *= times_to_amp_factors.get(t2, 1.0)
|
|
188
261
|
omega[i] = 0.5 * (omega_1 + omega_2)
|
|
189
262
|
else:
|
|
190
263
|
# We're in the final step and dt=1, approximate this using linear extrapolation
|
|
191
264
|
# we can reuse omega_1 and omega_2 from before
|
|
192
|
-
for q_pos, q_id in enumerate(
|
|
265
|
+
for q_pos, q_id in enumerate(q_ids):
|
|
193
266
|
delta[i, q_pos] = (
|
|
194
267
|
3.0 * locals_a_d_p[q_id]["det"][t2] - locals_a_d_p[q_id]["det"][t1]
|
|
195
268
|
) / 2.0
|
|
@@ -199,6 +272,8 @@ def _extract_omega_delta_phi(
|
|
|
199
272
|
) / 2.0
|
|
200
273
|
omega[i] = torch.clamp(0.5 * (3 * omega_2 - omega_1).real, min=0.0)
|
|
201
274
|
|
|
275
|
+
doppler_offset = _get_delta_offset(len(q_ids), temperature)
|
|
276
|
+
delta += doppler_offset
|
|
202
277
|
return omega, delta, phi
|
|
203
278
|
|
|
204
279
|
|
|
@@ -250,17 +325,17 @@ class PulserData:
|
|
|
250
325
|
self.target_times: list[int] = list(observable_times)
|
|
251
326
|
self.target_times.sort()
|
|
252
327
|
|
|
253
|
-
laser_waist =
|
|
254
|
-
|
|
255
|
-
|
|
328
|
+
laser_waist = config.noise_model.laser_waist
|
|
329
|
+
amp_sigma = config.noise_model.amp_sigma
|
|
330
|
+
temperature = config.noise_model.temperature
|
|
256
331
|
self.omega, self.delta, self.phi = _extract_omega_delta_phi(
|
|
257
332
|
sequence=sequence,
|
|
258
333
|
target_times=self.target_times,
|
|
259
334
|
with_modulation=config.with_modulation,
|
|
260
335
|
laser_waist=laser_waist,
|
|
336
|
+
amp_sigma=amp_sigma,
|
|
337
|
+
temperature=temperature,
|
|
261
338
|
)
|
|
262
|
-
self.lindblad_ops = _get_all_lindblad_noise_operators(config.noise_model)
|
|
263
|
-
self.has_lindblad_noise: bool = self.lindblad_ops != []
|
|
264
339
|
|
|
265
340
|
addressed_basis = sequence.get_addressed_bases()[0]
|
|
266
341
|
if addressed_basis == "ground-rydberg": # for local and global
|
|
@@ -270,6 +345,18 @@ class PulserData:
|
|
|
270
345
|
else:
|
|
271
346
|
raise ValueError(f"Unsupported basis: {addressed_basis}")
|
|
272
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
|
+
)
|
|
356
|
+
|
|
357
|
+
self.lindblad_ops = _get_all_lindblad_noise_operators(config.noise_model)
|
|
358
|
+
self.has_lindblad_noise: bool = self.lindblad_ops != []
|
|
359
|
+
|
|
273
360
|
if config.interaction_matrix is not None:
|
|
274
361
|
assert len(config.interaction_matrix) == self.qubit_count, (
|
|
275
362
|
"The number of qubits in the register should be the same as the size of "
|
emu_base/utils.py
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
import torch
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
def
|
|
5
|
-
|
|
4
|
+
def deallocate_tensor(t: torch.Tensor) -> None:
|
|
5
|
+
"""
|
|
6
|
+
Free the memory used by a tensor. This is done regardless of the
|
|
7
|
+
memory management done by Python: it is a forced deallocation
|
|
8
|
+
that ignores the current reference count of the Tensor object.
|
|
6
9
|
|
|
10
|
+
It is useful when you want to free memory that is no longer used
|
|
11
|
+
inside a function but that memory is also owned by a variable
|
|
12
|
+
in the outer scope, making it impossible to free it otherwise.
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
After calling that function, the Tensor object
|
|
15
|
+
should no longer be used.
|
|
16
|
+
|
|
17
|
+
To work properly with e.g. tensordot but also user-created views,
|
|
18
|
+
and since every view of a tensor owns the tensor's storage independently,
|
|
19
|
+
it has to change the storage of the base AND every view referring to the base.
|
|
20
|
+
However, it is not possible to access the views from the base, so
|
|
21
|
+
if there are extra inaccessible views, it will raise an exception.
|
|
22
|
+
"""
|
|
23
|
+
if (t._base is None and t._use_count() > 1) or ( # type: ignore[attr-defined]
|
|
24
|
+
t._base is not None and t._base._use_count() > 2 # type: ignore[attr-defined]
|
|
25
|
+
):
|
|
26
|
+
raise RuntimeError("Cannot deallocate tensor")
|
|
27
|
+
|
|
28
|
+
replacement_storage = torch.zeros(0, dtype=t.dtype, device=t.device).untyped_storage()
|
|
29
|
+
|
|
30
|
+
t.resize_(0)
|
|
31
|
+
t.set_(source=replacement_storage)
|
|
32
|
+
|
|
33
|
+
if t._base is not None:
|
|
34
|
+
t._base.resize_(0)
|
|
35
|
+
t._base.set_(source=replacement_storage)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: emu-base
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.1
|
|
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
|
|
7
7
|
Project-URL: Issues, https://github.com/pasqal-io/emulators/issues
|
|
8
|
-
Author-email:
|
|
8
|
+
Author-email: Kemal Bidzhiev <kemal.bidzhiev@pasqal.com>, Stefano Grava <stefano.grava@pasqal.com>, Pablo Le Henaff <pablo.le-henaff@pasqal.com>, Mauro Mendizabal <mauro.mendizabal-pico@pasqal.com>, Elie Merhej <elie.merhej@pasqal.com>, Anton Quelle <anton.quelle@pasqal.com>
|
|
9
9
|
License: PASQAL OPEN-SOURCE SOFTWARE LICENSE AGREEMENT (MIT-derived)
|
|
10
10
|
|
|
11
11
|
The author of the License is:
|
|
@@ -25,7 +25,7 @@ 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.
|
|
28
|
+
Requires-Dist: pulser-core==1.5.*
|
|
29
29
|
Requires-Dist: torch==2.7.0
|
|
30
30
|
Description-Content-Type: text/markdown
|
|
31
31
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
emu_base/__init__.py,sha256=lXgYx3VbA0qNMwDZyV4HpFcr6aUDxhqWjy8Um73JXrs,681
|
|
2
|
+
emu_base/aggregators.py,sha256=bB-rldoDAErxQMpL715K5lpiabGOpkCY0GyxW7mfHuc,5000
|
|
3
|
+
emu_base/constants.py,sha256=41LYkKLUCz-oxPbd-j7nUDZuhIbUrnez6prT0uR0jcE,56
|
|
4
|
+
emu_base/jump_lindblad_operators.py,sha256=Y30f8emVFS4Dazljc_Rh4lX9qU4QQY_AxPNahnzcsfY,2101
|
|
5
|
+
emu_base/pulser_adapter.py,sha256=WJX8y6iBYg8FX--q0Y10haz-35TbmUGoHbMDQEDuZIY,14534
|
|
6
|
+
emu_base/utils.py,sha256=-nIoJuu1pOxkPc2tiJTLjQ0ONsPR43CCB6vOJqdWaUc,1425
|
|
7
|
+
emu_base/math/__init__.py,sha256=6BbIytYV5uC-e5jLMtIErkcUl_PvfSNnhmVFY9Il8uQ,97
|
|
8
|
+
emu_base/math/brents_root_finding.py,sha256=AVx6L1Il6rpPJWrLJ7cn6oNmJyZOPRgEaaZaubC9lsU,3711
|
|
9
|
+
emu_base/math/double_krylov.py,sha256=X16dyCbyzdP7fFK-hmKS03Q-DJtC6TZ8sJrGTJ6akIc,3708
|
|
10
|
+
emu_base/math/krylov_energy_min.py,sha256=hm_B5qtBXHY1hl-r_LgDUKNDsdqVCDBHprQB3D-UFR8,4009
|
|
11
|
+
emu_base/math/krylov_exp.py,sha256=mGFddVQ8mEbwypbZtnlRPFpi4Nf8JZT6OKLHloIwCDQ,3934
|
|
12
|
+
emu_base/math/matmul.py,sha256=lEAnV0b5z_f1xEA-9p-WXxA8bM3QbShiHdXQ3ZkZFcQ,877
|
|
13
|
+
emu_base-2.2.1.dist-info/METADATA,sha256=Rs1xk1Xu4iB3-sWXgq0ASiM2bULwk0DVEfC-AclHIGs,3604
|
|
14
|
+
emu_base-2.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
15
|
+
emu_base-2.2.1.dist-info/RECORD,,
|
emu_base-2.1.1.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
emu_base/__init__.py,sha256=jwcX-VKAHMt-ZwhwGyFknseHFgZaQk5ZUBraEtrrj9s,493
|
|
2
|
-
emu_base/aggregators.py,sha256=bB-rldoDAErxQMpL715K5lpiabGOpkCY0GyxW7mfHuc,5000
|
|
3
|
-
emu_base/constants.py,sha256=41LYkKLUCz-oxPbd-j7nUDZuhIbUrnez6prT0uR0jcE,56
|
|
4
|
-
emu_base/jump_lindblad_operators.py,sha256=Y30f8emVFS4Dazljc_Rh4lX9qU4QQY_AxPNahnzcsfY,2101
|
|
5
|
-
emu_base/pulser_adapter.py,sha256=jDNUpVDlcxfUgI3q5vGfB34YZI1prT39wf--HOwkOJA,11330
|
|
6
|
-
emu_base/utils.py,sha256=RM8O0qfPAJfcdqqAojwEEKV7I3ZfVDklnTisTGhUg5k,233
|
|
7
|
-
emu_base/math/__init__.py,sha256=6BbIytYV5uC-e5jLMtIErkcUl_PvfSNnhmVFY9Il8uQ,97
|
|
8
|
-
emu_base/math/brents_root_finding.py,sha256=AVx6L1Il6rpPJWrLJ7cn6oNmJyZOPRgEaaZaubC9lsU,3711
|
|
9
|
-
emu_base/math/double_krylov.py,sha256=X16dyCbyzdP7fFK-hmKS03Q-DJtC6TZ8sJrGTJ6akIc,3708
|
|
10
|
-
emu_base/math/krylov_exp.py,sha256=MNLxgtiy2djRVtmXmtlBQ6A8rSuw1OK6dTtRQUZvaHs,3854
|
|
11
|
-
emu_base-2.1.1.dist-info/METADATA,sha256=9k7qJ9KPavksjlcUNEcSdnDi1mxHk0FY47NxLBtYE5E,3522
|
|
12
|
-
emu_base-2.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
13
|
-
emu_base-2.1.1.dist-info/RECORD,,
|
|
File without changes
|