emu-mps 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.
emu_mps/__init__.py ADDED
@@ -0,0 +1,38 @@
1
+ from emu_base import (
2
+ Callback,
3
+ BitStrings,
4
+ CorrelationMatrix,
5
+ Energy,
6
+ EnergyVariance,
7
+ Expectation,
8
+ Fidelity,
9
+ QubitDensity,
10
+ StateResult,
11
+ SecondMomentOfEnergy,
12
+ )
13
+ from .mpo import MPO
14
+ from .mps import MPS, inner
15
+ from .mps_backend import MPSBackend
16
+ from .mps_config import MPSConfig
17
+
18
+
19
+ __all__ = [
20
+ "__version__",
21
+ "MPO",
22
+ "MPS",
23
+ "inner",
24
+ "MPSConfig",
25
+ "MPSBackend",
26
+ "Callback",
27
+ "StateResult",
28
+ "BitStrings",
29
+ "QubitDensity",
30
+ "CorrelationMatrix",
31
+ "Expectation",
32
+ "Fidelity",
33
+ "Energy",
34
+ "EnergyVariance",
35
+ "SecondMomentOfEnergy",
36
+ ]
37
+
38
+ __version__ = "1.2.1"
emu_mps/algebra.py ADDED
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+ import math
5
+ from emu_mps.utils import truncate_impl
6
+
7
+
8
+ def add_factors(
9
+ left: list[torch.tensor], right: list[torch.tensor]
10
+ ) -> list[torch.tensor]:
11
+ """
12
+ Direct sum algorithm implementation to sum two tensor trains (MPS/MPO).
13
+ It assumes the left and right bond are along the dimension 0 and -1 of each tensor.
14
+ """
15
+ num_sites = len(left)
16
+ if num_sites != len(right):
17
+ raise ValueError("Cannot sum two matrix products of different number of sites")
18
+
19
+ new_tt = []
20
+ for i, (core1, core2) in enumerate(zip(left, right)):
21
+ core2 = core2.to(core1.device)
22
+ if i == 0:
23
+ core = torch.cat((core1, core2), dim=-1) # concatenate along the right bond
24
+ elif i == (num_sites - 1):
25
+ core = torch.cat((core1, core2), dim=0) # concatenate along the left bond
26
+ else:
27
+ pad_shape_1 = (core2.shape[0], *core1.shape[1:])
28
+ padded_c1 = torch.cat(
29
+ (
30
+ core1,
31
+ torch.zeros(pad_shape_1, device=core1.device, dtype=core1.dtype),
32
+ ),
33
+ dim=0, # concatenate along the left bond
34
+ )
35
+ pad_shape_2 = (core1.shape[0], *core2.shape[1:])
36
+ padded_c2 = torch.cat(
37
+ (
38
+ torch.zeros(pad_shape_2, device=core1.device, dtype=core1.dtype),
39
+ core2,
40
+ ),
41
+ dim=0, # concatenate along the left bond
42
+ )
43
+ core = torch.cat(
44
+ (padded_c1, padded_c2), dim=-1
45
+ ) # concatenate along the right bond
46
+ new_tt.append(core)
47
+ return new_tt
48
+
49
+
50
+ def scale_factors(
51
+ factors: list[torch.tensor], scalar: complex, *, which: int
52
+ ) -> list[torch.tensor]:
53
+ """
54
+ Returns a new list of factors where the tensor at the given index is scaled by `scalar`.
55
+ """
56
+ return [scalar * f if i == which else f for i, f in enumerate(factors)]
57
+
58
+
59
+ def zip_right_step(
60
+ slider: torch.tensor,
61
+ top: torch.tensor,
62
+ bottom: torch.tensor,
63
+ ) -> torch.tensor:
64
+ """
65
+ Returns a new `MPS/O` factor of the result of the multiplication MPO @ MPS/O,
66
+ and the updated slider, performing a single step of the
67
+ [zip-up algorithm](https://tensornetwork.org/mps/algorithms/zip_up_mpo/).
68
+
69
+ Args:
70
+ - `slider`: utility tensor for the zip-up algorithm.
71
+ - `top`: factor of the applied MPO.
72
+ - `bottom`: factor of the MPS/O to which the MPO is being applied.
73
+
74
+ First, moves all tensors to `bottom.device`.
75
+ Second, it contracts `top` and then `bottom` to `slider`.
76
+ The resulting tensor is then QR factorized into a
77
+ new factor and the updated slider for the next zip step.
78
+
79
+ Note:
80
+ The method assumes that:
81
+ - `top` is a valid MPO factor of shape
82
+ (left_link_dim, out_site_dim, in_site_dim, right_link_dim).
83
+ - `bottom` is a valid MPO/S factor
84
+ """
85
+ if slider.shape[1:] != (top.shape[0], bottom.shape[0]):
86
+ msg = (
87
+ f"Contracted dimensions between the slider, {slider.shape[1:]} on dims 1 and 2, "
88
+ f"and the two factors, {(top.shape[0], bottom.shape[0])} on dim 0, need to match."
89
+ )
90
+ raise ValueError(msg)
91
+
92
+ slider = slider.to(bottom.device)
93
+ top = top.to(bottom.device)
94
+
95
+ # merge top and bottom into slider
96
+ slider = torch.tensordot(slider, top, dims=([1], [0]))
97
+ slider = torch.tensordot(slider, bottom, dims=([3, 1], [1, 0]))
98
+
99
+ if len(bottom.shape) == 4: # MPO factor
100
+ slider = slider.transpose(2, 3)
101
+
102
+ # reshape slider as matrix
103
+ left_inds = (slider.shape[0], *bottom.shape[1:-1])
104
+ right_inds = (top.shape[-1], bottom.shape[-1])
105
+ slider = slider.reshape(math.prod(left_inds), math.prod(right_inds))
106
+
107
+ L, slider = torch.linalg.qr(slider)
108
+
109
+ # reshape slider to its original shape
110
+ slider = slider.reshape((-1, *right_inds))
111
+ # reshape left as MPS/O factor and
112
+ return L.reshape(*left_inds, -1), slider
113
+
114
+
115
+ def zip_right(
116
+ top_factors: list[torch.tensor],
117
+ bottom_factors: list[torch.tensor],
118
+ max_error: float = 1e-5,
119
+ max_rank: int = 1024,
120
+ ) -> list[torch.tensor]:
121
+ """
122
+ Returns a new matrix product, resulting from applying `top` to `bottom`.
123
+ The resulting factors are:
124
+ - of the same order as `bottom` factors
125
+ - on the same device of `bottom` factors
126
+ - orthogonalized on the first element
127
+ - truncated to `max_error`/`max_rank`
128
+
129
+ Args:
130
+ - `top`: MPO factors to be applied.
131
+ - `bottom`: MPS/O factors to which the MPO factors are being applied.
132
+
133
+ Note:
134
+ Implements a general [zip-up](https://tensornetwork.org/mps/algorithms/zip_up_mpo/)
135
+ algorithm for applying MPO factors to both MPO and MPS factors.
136
+ A final truncation sweep, from right to left,
137
+ moves back the orthogonal center to the first element.
138
+ """
139
+ if len(top_factors) != len(bottom_factors):
140
+ raise ValueError("Cannot multiply two matrix products of different lengths.")
141
+
142
+ slider = torch.ones(1, 1, 1, dtype=torch.complex128)
143
+ new_factors = []
144
+ for top, bottom in zip(top_factors, bottom_factors):
145
+ res, slider = zip_right_step(slider, top, bottom)
146
+ new_factors.append(res)
147
+ new_factors[-1] @= slider[:, :, 0]
148
+
149
+ truncate_impl(new_factors, max_error=max_error, max_rank=max_rank)
150
+
151
+ return new_factors
emu_mps/hamiltonian.py ADDED
@@ -0,0 +1,449 @@
1
+ """
2
+ This file deals with creation of the MPO corresponding
3
+ to the Hamiltonian of a neutral atoms quantum processor.
4
+ """
5
+
6
+ from emu_base import HamiltonianType
7
+ import torch
8
+
9
+ from emu_mps.mpo import MPO
10
+
11
+ dtype = torch.complex128 # always complex128
12
+ iden_op = torch.eye(2, 2, dtype=dtype) # dtype is always complex128
13
+ n_op = torch.tensor([[0.0, 0.0], [0.0, 1.0]], dtype=dtype)
14
+ creation_op = torch.tensor([[0.0, 1.0], [0.0, 0.0]], dtype=dtype)
15
+ sx = torch.tensor([[0.0, 0.5], [0.5, 0.0]], dtype=dtype)
16
+ sy = torch.tensor([[0.0, -0.5j], [0.5j, 0.0]], dtype=dtype)
17
+ pu = torch.tensor([[0.0, 0.0], [0.0, 1.0]], dtype=dtype)
18
+
19
+
20
+ def truncate_factor(
21
+ factor: torch.Tensor,
22
+ left_interactions: torch.Tensor,
23
+ right_interactions: torch.Tensor,
24
+ hamiltonian_type: HamiltonianType,
25
+ ) -> torch.Tensor:
26
+ if hamiltonian_type == HamiltonianType.XY:
27
+ left_interactions = torch.stack(
28
+ (left_interactions, left_interactions), dim=-1
29
+ ).reshape(-1)
30
+ right_interactions = torch.stack(
31
+ (right_interactions, right_interactions), dim=-1
32
+ ).reshape(-1)
33
+ padding = torch.tensor([True] * 2)
34
+ trunc = factor[torch.cat((padding, left_interactions))]
35
+ return trunc[:, :, :, torch.cat((padding, right_interactions))]
36
+
37
+
38
+ def _first_factor_rydberg(interaction: bool) -> torch.Tensor:
39
+ """
40
+ Creates the first Ising Hamiltonian factor.
41
+ """
42
+ fac = torch.zeros(1, 2, 2, 3 if interaction else 2, dtype=dtype)
43
+ fac[0, :, :, 1] = iden_op
44
+ if interaction:
45
+ fac[0, :, :, 2] = n_op # number operator
46
+
47
+ return fac
48
+
49
+
50
+ def _first_factor_xy(interaction: bool) -> torch.Tensor:
51
+ """
52
+ Creates the first XY Hamiltonian factor.
53
+ """
54
+ fac = torch.zeros(1, 2, 2, 4 if interaction else 2, dtype=dtype)
55
+ fac[0, :, :, 1] = iden_op
56
+ if interaction:
57
+ fac[0, :, :, 2] = creation_op
58
+ fac[0, :, :, 3] = creation_op.T
59
+
60
+ return fac
61
+
62
+
63
+ def _last_factor_rydberg(scale: float | complex) -> torch.Tensor:
64
+ """
65
+ Creates the last Ising Hamiltonian factor.
66
+ """
67
+ fac = torch.zeros(3 if scale != 0.0 else 2, 2, 2, 1, dtype=dtype)
68
+ fac[0, :, :, 0] = iden_op
69
+ if scale != 0:
70
+ fac[2, :, :, 0] = scale * n_op
71
+
72
+ return fac
73
+
74
+
75
+ def _last_factor_xy(scale: float | complex) -> torch.Tensor:
76
+ """
77
+ Creates the last XY Hamiltonian factor.
78
+ """
79
+ fac = torch.zeros(4 if scale != 0.0 else 2, 2, 2, 1, dtype=dtype)
80
+ fac[0, :, :, 0] = iden_op
81
+ if scale != 0:
82
+ fac[2, :, :, 0] = scale * creation_op.T
83
+ fac[3, :, :, 0] = scale * creation_op
84
+
85
+ return fac
86
+
87
+
88
+ def _left_factor_rydberg(
89
+ scales: torch.Tensor,
90
+ left_interactions: torch.Tensor,
91
+ right_interactions: torch.Tensor,
92
+ ) -> torch.Tensor:
93
+ """
94
+ Creates the Ising Hamiltonian factors in the left half of the MPS, excepted the first factor.
95
+ """
96
+ index = len(scales)
97
+ fac = torch.zeros(index + 2, 2, 2, index + 3, dtype=dtype)
98
+ fac[2 : scales.shape[0] + 2, :, :, 0] = (
99
+ scales.reshape(-1, 1, 1) * n_op
100
+ ) # interaction with previous qubits
101
+ fac[1, :, :, index + 2] = n_op # interaction with next qubits
102
+ for i in range(index + 2):
103
+ fac[i, :, :, i] = iden_op # identity matrix to carry the gates of other qubits
104
+
105
+ return truncate_factor(
106
+ fac,
107
+ left_interactions,
108
+ right_interactions,
109
+ hamiltonian_type=HamiltonianType.Rydberg,
110
+ )
111
+
112
+
113
+ def _left_factor_xy(
114
+ scales: torch.Tensor,
115
+ left_interactions: torch.Tensor,
116
+ right_interactions: torch.Tensor,
117
+ ) -> torch.Tensor:
118
+ """
119
+ Creates the XY Hamiltonian factors in the left half of the MPS, excepted the first factor.
120
+ """
121
+ index = len(scales)
122
+ fac = torch.zeros(2 * index + 2, 2, 2, 2 * index + 4, dtype=dtype)
123
+
124
+ fac[2 : 2 * scales.shape[0] + 2 : 2, :, :, 0] = (
125
+ scales.reshape(-1, 1, 1) * creation_op.T
126
+ ) # sigma-
127
+ fac[3 : 2 * scales.shape[0] + 3 : 2, :, :, 0] = (
128
+ scales.reshape(-1, 1, 1) * creation_op
129
+ ) # sigma+
130
+ fac[1, :, :, -2] = creation_op
131
+ fac[1, :, :, -1] = creation_op.T
132
+ for i in range(2 * index + 2):
133
+ fac[i, :, :, i] = iden_op # identity to carry the gates of other qubits
134
+
135
+ # duplicate each bool, because each interaction term occurs twice
136
+ return truncate_factor(
137
+ fac, left_interactions, right_interactions, hamiltonian_type=HamiltonianType.XY
138
+ )
139
+
140
+
141
+ def _right_factor_rydberg(
142
+ scales: torch.Tensor,
143
+ left_interactions: torch.Tensor,
144
+ right_interactions: torch.Tensor,
145
+ ) -> torch.Tensor:
146
+ """
147
+ Creates the Ising Hamiltonian factors in the right half of the MPS, excepted the last factor.
148
+ """
149
+ index = len(scales)
150
+ fac = torch.zeros(index + 3, 2, 2, index + 2, dtype=dtype)
151
+ fac[1, :, :, 2 : scales.shape[0] + 2] = scales * n_op.reshape(
152
+ 2, 2, 1
153
+ ) # XY interaction with previous qubits
154
+ fac[2, :, :, 0] = n_op # XY interaction with next qubits
155
+ for i in range(2, index + 2):
156
+ fac[i + 1, :, :, i] = iden_op
157
+ fac[0, :, :, 0] = iden_op # identity to carry the next gates to the previous qubits
158
+ fac[1, :, :, 1] = iden_op # identity to carry previous gates to next qubits
159
+
160
+ return truncate_factor(
161
+ fac,
162
+ left_interactions,
163
+ right_interactions,
164
+ hamiltonian_type=HamiltonianType.Rydberg,
165
+ )
166
+
167
+
168
+ def _right_factor_xy(
169
+ scales: torch.Tensor,
170
+ left_interactions: torch.Tensor,
171
+ right_interactions: torch.Tensor,
172
+ ) -> torch.Tensor:
173
+ """
174
+ Creates the XY Hamiltonian factors in the right half of the MPS, excepted the last factor.
175
+ """
176
+ index = len(scales)
177
+ fac = torch.zeros(2 * index + 4, 2, 2, 2 * index + 2, dtype=dtype)
178
+ fac[1, :, :, 2 : 2 * scales.shape[0] + 2 : 2] = scales * creation_op.reshape(
179
+ 2, 2, 1
180
+ ) # XY interaction with previous qubits
181
+ fac[1, :, :, 3 : 2 * scales.shape[0] + 3 : 2] = scales * creation_op.T.reshape(
182
+ 2, 2, 1
183
+ )
184
+ fac[2, :, :, 0] = creation_op.T # s- with next qubits
185
+ fac[3, :, :, 0] = creation_op # s+ with next qubits
186
+ for i in range(2, index + 2):
187
+ fac[2 * i, :, :, 2 * i - 2] = iden_op
188
+ fac[2 * i + 1, :, :, 2 * i - 1] = iden_op
189
+
190
+ # identity to carry the next gates to the previous qubits
191
+ fac[0, :, :, 0] = iden_op
192
+ # identity to carry previous gates to next qubits
193
+ fac[1, :, :, 1] = iden_op
194
+
195
+ # duplicate each bool, because each interaction term occurs twice
196
+ return truncate_factor(
197
+ fac, left_interactions, right_interactions, hamiltonian_type=HamiltonianType.XY
198
+ )
199
+
200
+
201
+ def _middle_factor_rydberg(
202
+ scales_l: torch.Tensor,
203
+ scales_r: torch.Tensor,
204
+ scales_mat: torch.Tensor,
205
+ left_interactions: torch.Tensor,
206
+ right_interactions: torch.Tensor,
207
+ ) -> torch.Tensor:
208
+ """
209
+ Creates the Ising Hamiltonian factor at index ⌊n/2⌋ of the n-qubit MPO.
210
+ """
211
+ assert len(scales_mat) == len(scales_l)
212
+ assert all(len(x) == len(scales_r) for x in scales_mat)
213
+
214
+ fac = torch.zeros(len(scales_l) + 2, 2, 2, len(scales_r) + 2, dtype=dtype)
215
+ fac[1, :, :, 2 : scales_r.shape[0] + 2] = scales_r * n_op.reshape(
216
+ 2, 2, 1
217
+ ) # rydberg interaction with previous qubits
218
+ fac[2 : scales_l.shape[0] + 2, :, :, 0] = (
219
+ scales_l.reshape(-1, 1, 1) * n_op
220
+ ) # rydberg interaction with next qubits
221
+ x_shape, y_shape = scales_mat.shape
222
+ fac[2 : x_shape + 2, :, :, 2 : y_shape + 2] = scales_mat.reshape(
223
+ x_shape, 1, 1, y_shape
224
+ ) * iden_op.reshape(
225
+ 1, 2, 2, 1
226
+ ) # rydberg interaction of previous with next qubits
227
+ fac[0, :, :, 0] = iden_op # identity to carry the next gates to the previous qubits
228
+ fac[1, :, :, 1] = iden_op # identity to carry previous gates to next qubits
229
+
230
+ return truncate_factor(
231
+ fac,
232
+ left_interactions,
233
+ right_interactions,
234
+ hamiltonian_type=HamiltonianType.Rydberg,
235
+ )
236
+
237
+
238
+ def _middle_factor_xy(
239
+ scales_l: torch.Tensor,
240
+ scales_r: torch.Tensor,
241
+ scales_mat: torch.Tensor,
242
+ left_interactions: torch.Tensor,
243
+ right_interactions: torch.Tensor,
244
+ ) -> torch.Tensor:
245
+ """
246
+ Creates the XY Hamiltonian factor at index ⌊n/2⌋ of the n-qubit MPO.
247
+ """
248
+ assert len(scales_mat) == len(scales_l)
249
+ assert all(len(x) == len(scales_r) for x in scales_mat)
250
+
251
+ fac = torch.zeros(2 * len(scales_l) + 2, 2, 2, 2 * len(scales_r) + 2, dtype=dtype)
252
+ fac[1, :, :, 2 : 2 * scales_r.shape[0] + 2 : 2] = scales_r * creation_op.reshape(
253
+ 2, 2, 1
254
+ ) # XY interaction with previous qubits
255
+ fac[1, :, :, 3 : 2 * scales_r.shape[0] + 3 : 2] = scales_r * creation_op.T.reshape(
256
+ 2, 2, 1
257
+ ) # XY interaction with previous qubits
258
+ fac[2 : 2 * scales_l.shape[0] + 2 : 2, :, :, 0] = (
259
+ scales_l.reshape(-1, 1, 1) * creation_op.T
260
+ ) # XY interaction with next qubits
261
+ fac[3 : 2 * scales_l.shape[0] + 3 : 2, :, :, 0] = (
262
+ scales_l.reshape(-1, 1, 1) * creation_op
263
+ ) # XY interaction with next qubits
264
+ x_shape, y_shape = scales_mat.shape
265
+ fac[2 : 2 * x_shape + 2 : 2, :, :, 2 : 2 * y_shape + 2 : 2] = scales_mat.reshape(
266
+ x_shape, 1, 1, y_shape
267
+ ) * iden_op.reshape(
268
+ 1, 2, 2, 1
269
+ ) # XY interaction of previous with next qubits
270
+ fac[3 : 2 * x_shape + 3 : 2, :, :, 3 : 2 * y_shape + 3 : 2] = scales_mat.reshape(
271
+ x_shape, 1, 1, y_shape
272
+ ) * iden_op.reshape(
273
+ 1, 2, 2, 1
274
+ ) # XY interaction of previous with next qubits
275
+ fac[0, :, :, 0] = iden_op # identity to carry the next gates to the previous qubits
276
+ fac[1, :, :, 1] = iden_op # identity to carry previous gates to next qubits
277
+
278
+ return truncate_factor(
279
+ fac, left_interactions, right_interactions, hamiltonian_type=HamiltonianType.XY
280
+ )
281
+
282
+
283
+ def _get_interactions_to_keep(interaction_matrix: torch.Tensor) -> list[torch.Tensor]:
284
+ """
285
+ returns a list of bool valued tensors,
286
+ indicating which interaction terms to keep for each bond in the MPO
287
+ """
288
+ nqubits = interaction_matrix.size(dim=1)
289
+ middle = nqubits // 2
290
+ interaction_matrix += torch.eye(
291
+ nqubits, nqubits, dtype=interaction_matrix.dtype
292
+ ) # below line fails on all zeros
293
+ interaction_boundaries = torch.tensor(
294
+ [torch.max(torch.nonzero(interaction_matrix[i])) for i in range(middle)]
295
+ )
296
+ interactions_to_keep = [interaction_boundaries[: i + 1] > i for i in range(middle)]
297
+
298
+ interaction_boundaries = torch.tensor(
299
+ [
300
+ torch.min(torch.nonzero(interaction_matrix[j]))
301
+ for j in range(middle + 1, nqubits)
302
+ ]
303
+ )
304
+ interactions_to_keep += [
305
+ interaction_boundaries[i - middle :] <= i for i in range(middle, nqubits - 1)
306
+ ]
307
+ return interactions_to_keep
308
+
309
+
310
+ def make_H(
311
+ *,
312
+ interaction_matrix: torch.Tensor, # depends on Hamiltonian Type
313
+ hamiltonian_type: HamiltonianType,
314
+ num_gpus_to_use: int | None,
315
+ ) -> MPO:
316
+ r"""
317
+ Constructs and returns a Matrix Product Operator (MPO) representing the
318
+ neutral atoms Hamiltonian, parameterized by `omega`, `delta`, and `phi`.
319
+
320
+ The Hamiltonian H is given by:
321
+ H = ∑ⱼΩⱼ[cos(ϕⱼ)σˣⱼ + sin(ϕⱼ)σʸⱼ] - ∑ⱼΔⱼnⱼ + ∑ᵢ﹥ⱼC⁶/rᵢⱼ⁶ nᵢnⱼ
322
+
323
+ If noise is considered, the Hamiltonian includes an additional term to support
324
+ the Monte Carlo WaveFunction algorithm:
325
+ H = ∑ⱼΩⱼ[cos(ϕⱼ)σˣⱼ + sin(ϕⱼ)σʸⱼ] - ∑ⱼΔⱼnⱼ + ∑ᵢ﹥ⱼC⁶/rᵢⱼ⁶ nᵢnⱼ - 0.5i∑ₘ ∑ᵤ Lₘᵘ⁺ Lₘᵘ
326
+ where Lₘᵘ are the Lindblad operators representing the noise, m for noise channel
327
+ and u for the number of atoms
328
+
329
+ make_H constructs an MPO of the appropriate size, but the single qubit terms are left at zero.
330
+ To fill in the appropriate values, call update_H
331
+
332
+ Args:
333
+ interaction_matrix (torch.Tensor): The interaction matrix describing the interactions
334
+ between qubits.
335
+ num_gpus_to_use (int): how many gpus to put the Hamiltonian on. See utils.assign_devices
336
+ Returns:
337
+ MPO: A Matrix Product Operator (MPO) representing the specified Hamiltonian.
338
+
339
+ Note:
340
+ For more information about the Hamiltonian and its usage, refer to the
341
+ [Pulser documentation](https://pulser.readthedocs.io/en/stable/conventions.html#hamiltonians).
342
+
343
+ """
344
+
345
+ if hamiltonian_type == HamiltonianType.Rydberg:
346
+ _first_factor = _first_factor_rydberg
347
+ _last_factor = _last_factor_rydberg
348
+ _left_factor = _left_factor_rydberg
349
+ _right_factor = _right_factor_rydberg
350
+ _middle_factor = _middle_factor_rydberg
351
+ elif hamiltonian_type == HamiltonianType.XY:
352
+ _first_factor = _first_factor_xy
353
+ _last_factor = _last_factor_xy
354
+ _left_factor = _left_factor_xy
355
+ _right_factor = _right_factor_xy
356
+ _middle_factor = _middle_factor_xy
357
+ else:
358
+ raise ValueError(f"Unsupported hamiltonian type {hamiltonian_type}")
359
+
360
+ nqubits = interaction_matrix.size(dim=1)
361
+ middle = nqubits // 2
362
+
363
+ interactions_to_keep = _get_interactions_to_keep(interaction_matrix)
364
+
365
+ cores = [_first_factor(interactions_to_keep[0].item())]
366
+
367
+ if nqubits > 2:
368
+ for i in range(1, middle):
369
+ cores.append(
370
+ _left_factor(
371
+ interaction_matrix[:i, i],
372
+ left_interactions=interactions_to_keep[i - 1],
373
+ right_interactions=interactions_to_keep[i],
374
+ )
375
+ )
376
+
377
+ i = middle
378
+ cores.append(
379
+ _middle_factor(
380
+ interaction_matrix[:i, i],
381
+ interaction_matrix[i, i + 1 :],
382
+ interaction_matrix[:i, i + 1 :],
383
+ interactions_to_keep[i - 1],
384
+ interactions_to_keep[i],
385
+ )
386
+ )
387
+
388
+ for i in range(middle + 1, nqubits - 1):
389
+ cores.append(
390
+ _right_factor(
391
+ interaction_matrix[i, i + 1 :],
392
+ interactions_to_keep[i - 1],
393
+ interactions_to_keep[i],
394
+ )
395
+ )
396
+ if nqubits == 2:
397
+ scale = interaction_matrix[0, 1]
398
+ elif interactions_to_keep[-1][0]:
399
+ scale = 1.0
400
+ else:
401
+ scale = 0.0
402
+ cores.append(
403
+ _last_factor(
404
+ scale,
405
+ )
406
+ )
407
+ return MPO(cores, num_gpus_to_use=num_gpus_to_use)
408
+
409
+
410
+ def update_H(
411
+ hamiltonian: MPO,
412
+ omega: torch.Tensor,
413
+ delta: torch.Tensor,
414
+ phi: torch.Tensor,
415
+ noise: torch.Tensor = torch.zeros(2, 2),
416
+ ) -> None:
417
+ """
418
+ The single qubit operators in the Hamiltonian,
419
+ corresponding to the omega, delta, phi parameters and the aggregated Lindblad operators
420
+ have a well-determined position in the factors of the Hamiltonian.
421
+ This function updates this part of the factors to update the
422
+ Hamiltonian with new parameters without rebuilding the entire thing.
423
+ See make_H for details about the Hamiltonian.
424
+
425
+ This is an in-place operation, so this function returns nothing.
426
+
427
+ Args:
428
+ omega (torch.Tensor): Rabi frequency Ωⱼ for each qubit.
429
+ delta (torch.Tensor): The detuning value Δⱼ for each qubit.
430
+ phi (torch.Tensor): The phase ϕⱼ corresponding to each qubit.
431
+ noise (torch.Tensor, optional): The single-qubit noise
432
+ term -0.5i∑ⱼLⱼ†Lⱼ applied to all qubits.
433
+ This can be computed using the `compute_noise_from_lindbladians` function.
434
+ Defaults to a zero tensor.
435
+ """
436
+
437
+ assert noise.shape == (2, 2)
438
+ nqubits = omega.size(dim=0)
439
+
440
+ a = torch.tensordot(omega * torch.cos(phi), sx, dims=0)
441
+ c = torch.tensordot(delta, pu, dims=0)
442
+ b = torch.tensordot(omega * torch.sin(phi), sy, dims=0)
443
+
444
+ single_qubit_terms = a + b - c + noise
445
+ factors = hamiltonian.factors
446
+
447
+ factors[0][0, :, :, 0] = single_qubit_terms[0]
448
+ for i in range(1, nqubits):
449
+ factors[i][1, :, :, 0] = single_qubit_terms[i]