emu-base 2.1.0__py3-none-any.whl → 2.2.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 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.1.0"
23
+ __version__ = "2.2.0"
@@ -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
@@ -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 / initial_norm]
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 = w - overlap * lanczos_vectors[k]
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 = w / n2
74
+ w /= n2
71
75
  lanczos_vectors.append(w)
72
76
 
73
77
  # Compute exponential of extended T matrix
@@ -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
@@ -1,15 +1,15 @@
1
- import pulser
2
- from typing import Tuple, Sequence
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
- from emu_base.utils import dist2, dist3
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 = [position.as_tensor() for position in register.qubits.values()]
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
- Computes the Ising interaction matrix from the qubit positions.
36
- Hᵢⱼ=C₆/R⁶ᵢⱼ (nᵢ⊗ nⱼ)
37
- """
57
+ Returns the Rydberg interaction matrix from the qubit positions.
58
+ Uᵢⱼ=C₆/|rᵢ-rⱼ|⁶
38
59
 
39
- num_qubits = len(sequence.register.qubit_ids)
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
- qubit_positions = _get_qubit_positions(sequence.register)
44
- interaction_matrix = torch.zeros(num_qubits, num_qubits)
45
-
46
- for numi in range(len(qubit_positions)):
47
- for numj in range(numi + 1, len(qubit_positions)):
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
- Computes the XY interaction matrix from the qubit positions.
58
- C₃ (1−3 cos(𝜃ᵢⱼ)²)/ Rᵢⱼ³ (𝜎ᵢ⁺ 𝜎ⱼ⁻ + 𝜎ᵢ⁻ 𝜎ⱼ⁺)
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
- interaction_matrix[numi][numj] = (
81
- c3 # check this value with pulser people
82
- * (1 - 3 * cosine**2)
83
- / dist3(qubit_positions[numi], qubit_positions[numj])
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
- return interaction_matrix
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("Emu-MPS only accepts ground-rydberg or mw_global channels")
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(sequence.register.qubit_ids),
216
+ len(q_ids),
134
217
  dtype=torch.complex128,
135
218
  )
136
219
 
137
220
  delta = torch.zeros(
138
221
  nsamples,
139
- len(sequence.register.qubit_ids),
222
+ len(q_ids),
140
223
  dtype=torch.complex128,
141
224
  )
142
225
  phi = torch.zeros(
143
226
  nsamples,
144
- len(sequence.register.qubit_ids),
227
+ len(q_ids),
145
228
  dtype=torch.complex128,
146
229
  )
230
+ qubit_positions = _get_qubit_positions(sequence.register)
147
231
 
148
- if laser_waist:
149
- qubit_positions = _get_qubit_positions(sequence.register)
150
- waist_factors = torch.tensor(
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
- for q_pos, q_id in enumerate(sequence.register.qubit_ids):
173
- t1 = math.floor(t)
174
- t2 = math.ceil(t)
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
- if t1 in global_times:
185
- omega_1 *= waist_factors
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(sequence.register.qubit_ids):
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
- config.noise_model.laser_waist if config.noise_model is not None else None
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 dist2(left: torch.Tensor, right: torch.Tensor) -> float:
5
- return torch.dist(left, right).item() ** 2
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
- def dist3(left: torch.Tensor, right: torch.Tensor) -> float:
9
- return torch.dist(left, right).item() ** 3
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.1.0
3
+ Version: 2.2.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
7
7
  Project-URL: Issues, https://github.com/pasqal-io/emulators/issues
8
- Author-email: Anton Quelle <anton.quelle@pasqal.com>, Mauro Mendizabal <mauro.mendizabal-pico@pasqal.com>, Stefano Grava <stefano.grava@pasqal.com>, Pablo Le Henaff <pablo.le-henaff@pasqal.com>
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.4.*
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=YZcFV352lDBnaV1Gbpb2AfmCikClpwCo21pWz8nIPNo,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.0.dist-info/METADATA,sha256=dw2ZzJVpY44wQk6T--Vxwd7mvjePegiyNF22HKzDpSA,3604
14
+ emu_base-2.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ emu_base-2.2.0.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- emu_base/__init__.py,sha256=nssAwoVeRB5OiBuBJcOzcfggDXp5IDB-2Xyft0kke6A,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.0.dist-info/METADATA,sha256=jkK_Y2sC5TRpYjxxTCouMtOklpiRu_EXSGpC6gkNp-s,3522
12
- emu_base-2.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
13
- emu_base-2.1.0.dist-info/RECORD,,