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 +38 -0
- emu_mps/algebra.py +151 -0
- emu_mps/hamiltonian.py +449 -0
- emu_mps/mpo.py +243 -0
- emu_mps/mps.py +528 -0
- emu_mps/mps_backend.py +35 -0
- emu_mps/mps_backend_impl.py +525 -0
- emu_mps/mps_config.py +64 -0
- emu_mps/noise.py +29 -0
- emu_mps/tdvp.py +209 -0
- emu_mps/utils.py +258 -0
- emu_mps-1.2.1.dist-info/METADATA +133 -0
- emu_mps-1.2.1.dist-info/RECORD +14 -0
- emu_mps-1.2.1.dist-info/WHEEL +4 -0
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]
|