emu-sv 2.0.4__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_sv/__init__.py +3 -1
- emu_sv/custom_callback_implementations.py +64 -15
- emu_sv/density_matrix_state.py +2 -1
- emu_sv/lindblad_operator.py +13 -4
- emu_sv/state_vector.py +3 -3
- emu_sv/sv_backend.py +7 -123
- emu_sv/sv_backend_impl.py +241 -0
- emu_sv/sv_config.py +33 -6
- emu_sv/time_evolution.py +356 -23
- {emu_sv-2.0.4.dist-info → emu_sv-2.2.0.dist-info}/METADATA +3 -3
- emu_sv-2.2.0.dist-info/RECORD +15 -0
- emu_sv-2.0.4.dist-info/RECORD +0 -14
- {emu_sv-2.0.4.dist-info → emu_sv-2.2.0.dist-info}/WHEEL +0 -0
emu_sv/__init__.py
CHANGED
|
@@ -15,6 +15,7 @@ from pulser.backend import (
|
|
|
15
15
|
from .dense_operator import DenseOperator
|
|
16
16
|
from .sv_backend import SVBackend, SVConfig
|
|
17
17
|
from .state_vector import StateVector, inner
|
|
18
|
+
from .density_matrix_state import DensityMatrix
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
__all__ = [
|
|
@@ -34,6 +35,7 @@ __all__ = [
|
|
|
34
35
|
"StateResult",
|
|
35
36
|
"StateVector",
|
|
36
37
|
"inner",
|
|
38
|
+
"DensityMatrix",
|
|
37
39
|
]
|
|
38
40
|
|
|
39
|
-
__version__ = "2.0
|
|
41
|
+
__version__ = "2.2.0"
|
|
@@ -8,11 +8,13 @@ from pulser.backend import (
|
|
|
8
8
|
Occupation,
|
|
9
9
|
Energy,
|
|
10
10
|
)
|
|
11
|
-
|
|
11
|
+
from emu_sv.density_matrix_state import DensityMatrix
|
|
12
12
|
from emu_sv.state_vector import StateVector
|
|
13
13
|
from emu_sv.dense_operator import DenseOperator
|
|
14
14
|
from emu_sv.hamiltonian import RydbergHamiltonian
|
|
15
15
|
|
|
16
|
+
dtype = torch.float64
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
def qubit_occupation_sv_impl(
|
|
18
20
|
self: Occupation,
|
|
@@ -25,7 +27,7 @@ def qubit_occupation_sv_impl(
|
|
|
25
27
|
Custom implementation of the occupation ❬ψ|nᵢ|ψ❭ for the state vector solver.
|
|
26
28
|
"""
|
|
27
29
|
nqubits = state.n_qudits
|
|
28
|
-
occupation = torch.zeros(nqubits, dtype=
|
|
30
|
+
occupation = torch.zeros(nqubits, dtype=dtype, device=state.vector.device)
|
|
29
31
|
for i in range(nqubits):
|
|
30
32
|
state_tensor = state.vector.view(2**i, 2, -1)
|
|
31
33
|
# nᵢ is a projector and therefore nᵢ == nᵢnᵢ
|
|
@@ -34,6 +36,33 @@ def qubit_occupation_sv_impl(
|
|
|
34
36
|
return occupation.cpu()
|
|
35
37
|
|
|
36
38
|
|
|
39
|
+
def qubit_occupation_sv_den_mat_impl(
|
|
40
|
+
self: Occupation,
|
|
41
|
+
*,
|
|
42
|
+
config: EmulationConfig,
|
|
43
|
+
state: DensityMatrix,
|
|
44
|
+
hamiltonian: DenseOperator,
|
|
45
|
+
) -> torch.Tensor:
|
|
46
|
+
"""
|
|
47
|
+
Custom implementation of the occupation nᵢ observable for density matrix.
|
|
48
|
+
The observable nᵢ is given by: I ⊗ ... ⊗ nᵢ ⊗ ... ⊗I
|
|
49
|
+
where nᵢ is the occupation operator for qubit i.
|
|
50
|
+
The expectation value is given by: <nᵢ> = Tr(ρ nᵢ).
|
|
51
|
+
|
|
52
|
+
The output will be a tensor of size (nqubits,), where each element will be the
|
|
53
|
+
expectation value of the occupation operator for each qubit.
|
|
54
|
+
In case of 3 atoms, the output will be a tensor of size (3,), where each element
|
|
55
|
+
will be <nᵢ> = Tr(ρnᵢ), or [ <n₁>, <n₂>, <n₃> ].
|
|
56
|
+
"""
|
|
57
|
+
nqubits = state.n_qudits
|
|
58
|
+
occupation = torch.zeros(nqubits, dtype=dtype, device=state.matrix.device)
|
|
59
|
+
diag_state_tensor = state.matrix.diagonal()
|
|
60
|
+
for i in range(nqubits):
|
|
61
|
+
state_tensor = diag_state_tensor.view(2**i, 2, 2 ** (nqubits - i - 1))[:, 1, :]
|
|
62
|
+
occupation[i] = state_tensor.sum().real
|
|
63
|
+
return occupation.cpu()
|
|
64
|
+
|
|
65
|
+
|
|
37
66
|
def correlation_matrix_sv_impl(
|
|
38
67
|
self: CorrelationMatrix,
|
|
39
68
|
*,
|
|
@@ -47,24 +76,44 @@ def correlation_matrix_sv_impl(
|
|
|
47
76
|
TODO: extend to arbitrary two-point correlation ❬ψ|AᵢBⱼ|ψ❭
|
|
48
77
|
"""
|
|
49
78
|
nqubits = state.n_qudits
|
|
50
|
-
correlation = torch.zeros(
|
|
51
|
-
nqubits, nqubits, dtype=torch.float64, device=state.vector.device
|
|
52
|
-
)
|
|
79
|
+
correlation = torch.zeros(nqubits, nqubits, dtype=dtype, device=state.vector.device)
|
|
53
80
|
|
|
54
81
|
for i in range(nqubits):
|
|
55
82
|
select_i = state.vector.view(2**i, 2, -1)
|
|
56
83
|
select_i = select_i[:, 1]
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
correlation[i, j] = value
|
|
66
|
-
correlation[j, i] = value
|
|
84
|
+
correlation[i, i] = torch.linalg.vector_norm(select_i) ** 2
|
|
85
|
+
for j in range(i + 1, nqubits): # select the upper triangle
|
|
86
|
+
select_i = select_i.view(2**i, 2 ** (j - i - 1), 2, -1)
|
|
87
|
+
select_ij = select_i[:, :, 1, :]
|
|
88
|
+
correlation[i, j] = torch.linalg.vector_norm(select_ij) ** 2
|
|
89
|
+
correlation[j, i] = correlation[i, j]
|
|
90
|
+
|
|
91
|
+
return correlation.cpu()
|
|
67
92
|
|
|
93
|
+
|
|
94
|
+
def correlation_matrix_sv_den_mat_impl(
|
|
95
|
+
self: CorrelationMatrix,
|
|
96
|
+
*,
|
|
97
|
+
config: EmulationConfig,
|
|
98
|
+
state: DensityMatrix,
|
|
99
|
+
hamiltonian: DenseOperator,
|
|
100
|
+
) -> torch.Tensor:
|
|
101
|
+
"""
|
|
102
|
+
Custom implementation of the density-density correlation <nᵢnⱼ> = Tr(ρ nᵢnⱼ)
|
|
103
|
+
in the case of Lindblad noise
|
|
104
|
+
"""
|
|
105
|
+
nqubits = state.n_qudits
|
|
106
|
+
correlation = torch.zeros(nqubits, nqubits, dtype=dtype)
|
|
107
|
+
state_diag_matrix = state.matrix.diagonal()
|
|
108
|
+
for i in range(nqubits): # applying ni
|
|
109
|
+
shapei = (2**i, 2, 2 ** (nqubits - i - 1))
|
|
110
|
+
state_diag_ni = state_diag_matrix.view(*shapei)[:, 1, :]
|
|
111
|
+
correlation[i, i] = state_diag_ni.sum().real # diagonal
|
|
112
|
+
for j in range(i + 1, nqubits):
|
|
113
|
+
shapeij = (2**i, 2 ** (j - i - 1), 2, 2 ** (nqubits - 1 - j))
|
|
114
|
+
state_diag_ni_nj = state_diag_ni.view(*shapeij)[:, :, 1, :]
|
|
115
|
+
correlation[i, j] = state_diag_ni_nj.sum().real
|
|
116
|
+
correlation[j, i] = correlation[i, j]
|
|
68
117
|
return correlation.cpu()
|
|
69
118
|
|
|
70
119
|
|
emu_sv/density_matrix_state.py
CHANGED
|
@@ -111,6 +111,7 @@ class DensityMatrix(State[complex, torch.Tensor]):
|
|
|
111
111
|
cls: Type[DensityMatrixType],
|
|
112
112
|
*,
|
|
113
113
|
eigenstates: Sequence[Eigenstate],
|
|
114
|
+
n_qudits: int,
|
|
114
115
|
amplitudes: Mapping[str, complex],
|
|
115
116
|
) -> tuple[DensityMatrix, Mapping[str, complex]]:
|
|
116
117
|
"""Transforms a state given by a string into a density matrix.
|
|
@@ -140,7 +141,7 @@ class DensityMatrix(State[complex, torch.Tensor]):
|
|
|
140
141
|
"""
|
|
141
142
|
|
|
142
143
|
state_vector, amplitudes = StateVector._from_state_amplitudes(
|
|
143
|
-
eigenstates=eigenstates, amplitudes=amplitudes
|
|
144
|
+
eigenstates=eigenstates, n_qudits=n_qudits, amplitudes=amplitudes
|
|
144
145
|
)
|
|
145
146
|
|
|
146
147
|
return DensityMatrix.from_state_vector(state_vector), amplitudes
|
emu_sv/lindblad_operator.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import torch
|
|
2
|
-
from emu_base
|
|
2
|
+
from emu_base import compute_noise_from_lindbladians, matmul_2x2_with_batched
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
dtype = torch.complex128
|
|
@@ -17,6 +17,10 @@ class RydbergLindbladian:
|
|
|
17
17
|
where A_k is a jump operator and H is the Rydberg Hamiltonian.
|
|
18
18
|
The complex -𝑖, will be multiplied in the evolution.
|
|
19
19
|
|
|
20
|
+
Only works with effective noise channels, i.e., the jump or collapse
|
|
21
|
+
operators. For more information, see:
|
|
22
|
+
https://pulser.readthedocs.io/en/stable/tutorials/effective_noise.html
|
|
23
|
+
|
|
20
24
|
Attributes:
|
|
21
25
|
nqubits (int): number of qubits in the system.
|
|
22
26
|
omegas (torch.Tensor): amplited frequencies Ωⱼ for each qubit, divided by 2.
|
|
@@ -96,7 +100,10 @@ class RydbergLindbladian:
|
|
|
96
100
|
|
|
97
101
|
orignal_shape = density_matrix.shape
|
|
98
102
|
density_matrix = density_matrix.view(2**target_qubit, 2, -1)
|
|
99
|
-
|
|
103
|
+
if density_matrix.is_cpu:
|
|
104
|
+
density_matrix = local_op @ density_matrix
|
|
105
|
+
else:
|
|
106
|
+
density_matrix = matmul_2x2_with_batched(local_op, density_matrix)
|
|
100
107
|
|
|
101
108
|
return density_matrix.view(orignal_shape)
|
|
102
109
|
|
|
@@ -114,9 +121,11 @@ class RydbergLindbladian:
|
|
|
114
121
|
"""
|
|
115
122
|
|
|
116
123
|
orignal_shape = density_matrix.shape
|
|
117
|
-
|
|
118
124
|
density_matrix = density_matrix.view(2 ** (target_qubit + self.nqubits), 2, -1)
|
|
119
|
-
density_matrix
|
|
125
|
+
if density_matrix.is_cpu:
|
|
126
|
+
density_matrix = local_op.conj() @ density_matrix
|
|
127
|
+
else:
|
|
128
|
+
density_matrix = matmul_2x2_with_batched(local_op.conj(), density_matrix)
|
|
120
129
|
|
|
121
130
|
return density_matrix.view(orignal_shape)
|
|
122
131
|
|
emu_sv/state_vector.py
CHANGED
|
@@ -51,7 +51,7 @@ class StateVector(State[complex, torch.Tensor]):
|
|
|
51
51
|
@property
|
|
52
52
|
def n_qudits(self) -> int:
|
|
53
53
|
"""The number of qudits in the state."""
|
|
54
|
-
nqudits = math.log2(self.vector.
|
|
54
|
+
nqudits = math.log2(self.vector.view(-1).shape[0])
|
|
55
55
|
return int(nqudits)
|
|
56
56
|
|
|
57
57
|
def _normalize(self) -> None:
|
|
@@ -220,6 +220,7 @@ class StateVector(State[complex, torch.Tensor]):
|
|
|
220
220
|
cls: Type[StateVectorType],
|
|
221
221
|
*,
|
|
222
222
|
eigenstates: Sequence[Eigenstate],
|
|
223
|
+
n_qudits: int,
|
|
223
224
|
amplitudes: Mapping[str, complex],
|
|
224
225
|
) -> tuple[StateVector, Mapping[str, complex]]:
|
|
225
226
|
"""Transforms a state given by a string into a state vector.
|
|
@@ -257,8 +258,7 @@ class StateVector(State[complex, torch.Tensor]):
|
|
|
257
258
|
else:
|
|
258
259
|
raise ValueError("Unsupported basis provided")
|
|
259
260
|
|
|
260
|
-
|
|
261
|
-
accum_state = StateVector.zero(num_sites=nqubits, eigenstates=eigenstates)
|
|
261
|
+
accum_state = StateVector.zero(num_sites=n_qudits, eigenstates=eigenstates)
|
|
262
262
|
|
|
263
263
|
for state, amplitude in amplitudes.items():
|
|
264
264
|
bin_to_int = int(
|
emu_sv/sv_backend.py
CHANGED
|
@@ -1,23 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
from
|
|
3
|
-
import time
|
|
4
|
-
import typing
|
|
5
|
-
|
|
6
|
-
from pulser.backend import EmulatorBackend, Results, Observable, State, EmulationConfig
|
|
7
|
-
|
|
8
|
-
from emu_base import PulserData
|
|
9
|
-
|
|
10
|
-
from emu_sv.state_vector import StateVector
|
|
1
|
+
from pulser.backend import EmulatorBackend
|
|
2
|
+
from pulser.backend import Results
|
|
11
3
|
from emu_sv.sv_config import SVConfig
|
|
12
|
-
from emu_sv.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
_TIME_CONVERSION_COEFF = 0.001 # Omega and delta are given in rad/μs, dt in ns
|
|
4
|
+
from emu_sv.sv_backend_impl import create_impl
|
|
16
5
|
|
|
17
6
|
|
|
18
7
|
class SVBackend(EmulatorBackend):
|
|
19
8
|
"""
|
|
20
9
|
A backend for emulating Pulser sequences using state vectors and sparse matrices.
|
|
10
|
+
Noisy simulation is supported by solving the Lindblad equation and using effective
|
|
11
|
+
noise channel or jump operators
|
|
21
12
|
"""
|
|
22
13
|
|
|
23
14
|
default_config = SVConfig()
|
|
@@ -31,112 +22,5 @@ class SVBackend(EmulatorBackend):
|
|
|
31
22
|
"""
|
|
32
23
|
assert isinstance(self._config, SVConfig)
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
self.target_times = pulser_data.target_times
|
|
38
|
-
self.time = time.time()
|
|
39
|
-
omega, delta, phi = pulser_data.omega, pulser_data.delta, pulser_data.phi
|
|
40
|
-
|
|
41
|
-
nsteps = omega.shape[0]
|
|
42
|
-
nqubits = omega.shape[1]
|
|
43
|
-
|
|
44
|
-
self.results = Results(atom_order=(), total_duration=self.target_times[-1])
|
|
45
|
-
self.statistics = Statistics(
|
|
46
|
-
evaluation_times=[t / self.target_times[-1] for t in self.target_times],
|
|
47
|
-
data=[],
|
|
48
|
-
timestep_count=nsteps,
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
if self._config.initial_state is not None:
|
|
52
|
-
state = self._config.initial_state
|
|
53
|
-
state = StateVector(state.vector.clone(), gpu=state.vector.is_cuda)
|
|
54
|
-
else:
|
|
55
|
-
state = StateVector.make(nqubits, gpu=self._config.gpu)
|
|
56
|
-
|
|
57
|
-
for step in range(nsteps):
|
|
58
|
-
dt = self.target_times[step + 1] - self.target_times[step]
|
|
59
|
-
|
|
60
|
-
state.vector, H = do_time_step(
|
|
61
|
-
dt * _TIME_CONVERSION_COEFF,
|
|
62
|
-
omega[step],
|
|
63
|
-
delta[step],
|
|
64
|
-
phi[step],
|
|
65
|
-
pulser_data.full_interaction_matrix,
|
|
66
|
-
state.vector,
|
|
67
|
-
self._config.krylov_tolerance,
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
# callbacks in observables and self.statistics in H
|
|
71
|
-
# have "# type: ignore[arg-type]" because H has it's own type
|
|
72
|
-
# meaning H is not inherited from Operator class.
|
|
73
|
-
# We decided that ignore[arg-type] is better compared to
|
|
74
|
-
# having many unused NotImplemented methods
|
|
75
|
-
for callback in self._config.observables:
|
|
76
|
-
callback(
|
|
77
|
-
self._config,
|
|
78
|
-
self.target_times[step + 1] / self.target_times[-1],
|
|
79
|
-
state,
|
|
80
|
-
H, # type: ignore[arg-type]
|
|
81
|
-
self.results,
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
self.statistics.data.append(time.time() - self.time)
|
|
85
|
-
self.statistics(
|
|
86
|
-
self._config,
|
|
87
|
-
self.target_times[step + 1] / self.target_times[-1],
|
|
88
|
-
state,
|
|
89
|
-
H, # type: ignore[arg-type]
|
|
90
|
-
self.results,
|
|
91
|
-
)
|
|
92
|
-
self.time = time.time()
|
|
93
|
-
del H
|
|
94
|
-
|
|
95
|
-
return self.results
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
class Statistics(Observable):
|
|
99
|
-
def __init__(
|
|
100
|
-
self,
|
|
101
|
-
evaluation_times: typing.Sequence[float] | None,
|
|
102
|
-
data: list[float],
|
|
103
|
-
timestep_count: int,
|
|
104
|
-
):
|
|
105
|
-
super().__init__(evaluation_times=evaluation_times)
|
|
106
|
-
self.data = data
|
|
107
|
-
self.timestep_count = timestep_count
|
|
108
|
-
|
|
109
|
-
@property
|
|
110
|
-
def _base_tag(self) -> str:
|
|
111
|
-
return "statistics"
|
|
112
|
-
|
|
113
|
-
def apply(
|
|
114
|
-
self,
|
|
115
|
-
*,
|
|
116
|
-
config: EmulationConfig,
|
|
117
|
-
state: State,
|
|
118
|
-
**kwargs: typing.Any,
|
|
119
|
-
) -> dict:
|
|
120
|
-
"""Calculates the observable to store in the Results."""
|
|
121
|
-
assert isinstance(state, StateVector)
|
|
122
|
-
assert isinstance(config, SVConfig)
|
|
123
|
-
duration = self.data[-1]
|
|
124
|
-
if state.vector.is_cuda:
|
|
125
|
-
max_mem_per_device = (
|
|
126
|
-
torch.cuda.max_memory_allocated(device) * 1e-6
|
|
127
|
-
for device in range(torch.cuda.device_count())
|
|
128
|
-
)
|
|
129
|
-
max_mem = max(max_mem_per_device)
|
|
130
|
-
else:
|
|
131
|
-
max_mem = getrusage(RUSAGE_SELF).ru_maxrss * 1e-3
|
|
132
|
-
|
|
133
|
-
config.logger.info(
|
|
134
|
-
f"step = {len(self.data)}/{self.timestep_count}, "
|
|
135
|
-
+ f"RSS = {max_mem:.3f} MB, "
|
|
136
|
-
+ f"Δt = {duration:.3f} s"
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
"RSS": max_mem,
|
|
141
|
-
"duration": duration,
|
|
142
|
-
}
|
|
25
|
+
impl = create_impl(self._sequence, self._config)
|
|
26
|
+
return impl._run()
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
import time
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
from emu_sv.hamiltonian import RydbergHamiltonian
|
|
6
|
+
from emu_sv.lindblad_operator import RydbergLindbladian
|
|
7
|
+
from pulser import Sequence
|
|
8
|
+
import torch
|
|
9
|
+
from resource import RUSAGE_SELF, getrusage
|
|
10
|
+
|
|
11
|
+
from pulser.backend import Results, Observable, State, EmulationConfig
|
|
12
|
+
from emu_base import PulserData
|
|
13
|
+
|
|
14
|
+
from emu_sv.state_vector import StateVector
|
|
15
|
+
from emu_sv.density_matrix_state import DensityMatrix
|
|
16
|
+
from emu_sv.sv_config import SVConfig
|
|
17
|
+
from emu_sv.time_evolution import EvolveStateVector, EvolveDensityMatrix
|
|
18
|
+
|
|
19
|
+
_TIME_CONVERSION_COEFF = 0.001 # Omega and delta are given in rad/μs, dt in ns
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Statistics(Observable):
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
evaluation_times: typing.Sequence[float] | None,
|
|
26
|
+
data: list[float],
|
|
27
|
+
timestep_count: int,
|
|
28
|
+
):
|
|
29
|
+
super().__init__(evaluation_times=evaluation_times)
|
|
30
|
+
self.data = data
|
|
31
|
+
self.timestep_count = timestep_count
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def _base_tag(self) -> str:
|
|
35
|
+
return "statistics"
|
|
36
|
+
|
|
37
|
+
def apply(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
config: EmulationConfig,
|
|
41
|
+
state: State,
|
|
42
|
+
**kwargs: typing.Any,
|
|
43
|
+
) -> dict:
|
|
44
|
+
"""Calculates the observable to store in the Results."""
|
|
45
|
+
assert isinstance(state, StateVector | DensityMatrix)
|
|
46
|
+
assert isinstance(config, SVConfig)
|
|
47
|
+
duration = self.data[-1]
|
|
48
|
+
if isinstance(state, StateVector) and state.vector.is_cuda:
|
|
49
|
+
max_mem_per_device = (
|
|
50
|
+
torch.cuda.max_memory_allocated(device) * 1e-6
|
|
51
|
+
for device in range(torch.cuda.device_count())
|
|
52
|
+
)
|
|
53
|
+
max_mem = max(max_mem_per_device)
|
|
54
|
+
elif isinstance(state, DensityMatrix) and state.matrix.is_cuda:
|
|
55
|
+
max_mem_per_device = (
|
|
56
|
+
torch.cuda.max_memory_allocated(device) * 1e-6
|
|
57
|
+
for device in range(torch.cuda.device_count())
|
|
58
|
+
)
|
|
59
|
+
max_mem = max(max_mem_per_device)
|
|
60
|
+
else:
|
|
61
|
+
max_mem = getrusage(RUSAGE_SELF).ru_maxrss * 1e-3
|
|
62
|
+
|
|
63
|
+
config.logger.info(
|
|
64
|
+
f"step = {len(self.data)}/{self.timestep_count}, "
|
|
65
|
+
+ f"RSS = {max_mem:.3f} MB, "
|
|
66
|
+
+ f"Δt = {duration:.3f} s"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"RSS": max_mem,
|
|
71
|
+
"duration": duration,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class BaseSVBackendImpl:
|
|
76
|
+
"""
|
|
77
|
+
This class is used to handle the state vector and density matrix evolution.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, config: SVConfig, pulser_data: PulserData):
|
|
81
|
+
self._config = config
|
|
82
|
+
self._pulser_data = pulser_data
|
|
83
|
+
self.target_times = pulser_data.target_times
|
|
84
|
+
self.omega = pulser_data.omega
|
|
85
|
+
self.delta = pulser_data.delta
|
|
86
|
+
self.phi = pulser_data.phi
|
|
87
|
+
self.nsteps = pulser_data.omega.shape[0]
|
|
88
|
+
self.nqubits = pulser_data.omega.shape[1]
|
|
89
|
+
self.state: State
|
|
90
|
+
self.time = time.time()
|
|
91
|
+
self.results = Results(atom_order=(), total_duration=self.target_times[-1])
|
|
92
|
+
self.statistics = Statistics(
|
|
93
|
+
evaluation_times=[t / self.target_times[-1] for t in self.target_times],
|
|
94
|
+
data=[],
|
|
95
|
+
timestep_count=self.nsteps,
|
|
96
|
+
)
|
|
97
|
+
self._current_H: None | RydbergLindbladian | RydbergHamiltonian = None
|
|
98
|
+
|
|
99
|
+
if self._config.initial_state is not None and (
|
|
100
|
+
self._config.initial_state.n_qudits != self.nqubits
|
|
101
|
+
):
|
|
102
|
+
raise ValueError(
|
|
103
|
+
"Mismatch in number of atoms: initial state has "
|
|
104
|
+
f"{self._config.initial_state.n_qudits} and the sequence has {self.nqubits}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def step(self, step_idx: int) -> None:
|
|
108
|
+
"""One step of the evolution"""
|
|
109
|
+
dt = self._compute_dt(step_idx)
|
|
110
|
+
self._evolve_step(dt, step_idx)
|
|
111
|
+
self._apply_observables(step_idx)
|
|
112
|
+
self._save_statistics(step_idx)
|
|
113
|
+
|
|
114
|
+
def _compute_dt(self, step_idx: int) -> float:
|
|
115
|
+
return self.target_times[step_idx + 1] - self.target_times[step_idx]
|
|
116
|
+
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def _evolve_step(self, dt: float, step_idx: int) -> None:
|
|
119
|
+
"""One step evolution"""
|
|
120
|
+
|
|
121
|
+
def _apply_observables(self, step_idx: int) -> None:
|
|
122
|
+
norm_time = self.target_times[step_idx + 1] / self.target_times[-1]
|
|
123
|
+
for callback in self._config.observables:
|
|
124
|
+
callback(
|
|
125
|
+
self._config,
|
|
126
|
+
norm_time,
|
|
127
|
+
self.state,
|
|
128
|
+
self._current_H, # type: ignore[arg-type]
|
|
129
|
+
self.results,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _save_statistics(self, step_idx: int) -> None:
|
|
133
|
+
norm_time = self.target_times[step_idx + 1] / self.target_times[-1]
|
|
134
|
+
self.statistics.data.append(time.time() - self.time)
|
|
135
|
+
self.statistics(
|
|
136
|
+
self._config,
|
|
137
|
+
norm_time,
|
|
138
|
+
self.state,
|
|
139
|
+
self._current_H, # type: ignore[arg-type]
|
|
140
|
+
self.results,
|
|
141
|
+
)
|
|
142
|
+
self.time = time.time()
|
|
143
|
+
self._current_H = None
|
|
144
|
+
|
|
145
|
+
def _run(self) -> Results:
|
|
146
|
+
for step in range(self.nsteps):
|
|
147
|
+
self.step(step)
|
|
148
|
+
|
|
149
|
+
return self.results
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class SVBackendImpl(BaseSVBackendImpl):
|
|
153
|
+
|
|
154
|
+
def __init__(self, config: SVConfig, pulser_data: PulserData):
|
|
155
|
+
"""
|
|
156
|
+
For running sequences without noise. The state will evolve accoring
|
|
157
|
+
to e^(-iH t)
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
config: The configuration for the emulator.
|
|
161
|
+
pulser_data: The data for the sequence to be emulated.
|
|
162
|
+
"""
|
|
163
|
+
super().__init__(config, pulser_data)
|
|
164
|
+
|
|
165
|
+
self.state: StateVector = (
|
|
166
|
+
StateVector.make(self.nqubits, gpu=self._config.gpu)
|
|
167
|
+
if self._config.initial_state is None
|
|
168
|
+
else StateVector(
|
|
169
|
+
self._config.initial_state.vector.clone(),
|
|
170
|
+
gpu=self._config.gpu,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
self.stepper = EvolveStateVector.apply
|
|
175
|
+
|
|
176
|
+
def _evolve_step(self, dt: float, step_idx: int) -> None:
|
|
177
|
+
self.state.vector, self._current_H = self.stepper(
|
|
178
|
+
dt * _TIME_CONVERSION_COEFF,
|
|
179
|
+
self.omega[step_idx],
|
|
180
|
+
self.delta[step_idx],
|
|
181
|
+
self.phi[step_idx],
|
|
182
|
+
self._pulser_data.full_interaction_matrix,
|
|
183
|
+
self.state.vector,
|
|
184
|
+
self._config.krylov_tolerance,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class NoisySVBackendImpl(BaseSVBackendImpl):
|
|
189
|
+
|
|
190
|
+
def __init__(self, config: SVConfig, pulser_data: PulserData):
|
|
191
|
+
"""
|
|
192
|
+
Initializes the NoisySVBackendImpl, master equation version.
|
|
193
|
+
This class handles the Lindblad operators and
|
|
194
|
+
solves the Lindblad master equation
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
config: The configuration for the emulator.
|
|
198
|
+
pulser_data: The data for the sequence to be emulated.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
super().__init__(config, pulser_data)
|
|
202
|
+
|
|
203
|
+
self.pulser_lindblads = pulser_data.lindblad_ops
|
|
204
|
+
|
|
205
|
+
self.state: DensityMatrix = (
|
|
206
|
+
DensityMatrix.make(self.nqubits, gpu=self._config.gpu)
|
|
207
|
+
if self._config.initial_state is None
|
|
208
|
+
else DensityMatrix(
|
|
209
|
+
self._config.initial_state.matrix.clone(), gpu=self._config.gpu
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def _evolve_step(self, dt: float, step_idx: int) -> None:
|
|
214
|
+
self.state.matrix, self._current_H = EvolveDensityMatrix.evolve(
|
|
215
|
+
dt * _TIME_CONVERSION_COEFF,
|
|
216
|
+
self.omega[step_idx],
|
|
217
|
+
self.delta[step_idx],
|
|
218
|
+
self.phi[step_idx],
|
|
219
|
+
self._pulser_data.full_interaction_matrix,
|
|
220
|
+
self.state.matrix,
|
|
221
|
+
self._config.krylov_tolerance,
|
|
222
|
+
self.pulser_lindblads,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def create_impl(sequence: Sequence, config: SVConfig) -> BaseSVBackendImpl:
|
|
227
|
+
"""
|
|
228
|
+
Creates the backend implementation for the given sequence and config.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
sequence: The sequence to be emulated.
|
|
232
|
+
config: configuration for the emulator.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
An instance of SVBackendImpl.
|
|
236
|
+
"""
|
|
237
|
+
pulse_data = PulserData(sequence=sequence, config=config, dt=config.dt)
|
|
238
|
+
if pulse_data.has_lindblad_noise:
|
|
239
|
+
return NoisySVBackendImpl(config, pulse_data)
|
|
240
|
+
else:
|
|
241
|
+
return SVBackendImpl(config, pulse_data)
|
emu_sv/sv_config.py
CHANGED
|
@@ -7,9 +7,11 @@ from typing import Any, ClassVar
|
|
|
7
7
|
|
|
8
8
|
from emu_sv.custom_callback_implementations import (
|
|
9
9
|
correlation_matrix_sv_impl,
|
|
10
|
+
correlation_matrix_sv_den_mat_impl,
|
|
10
11
|
energy_second_moment_sv_impl,
|
|
11
12
|
energy_variance_sv_impl,
|
|
12
13
|
qubit_occupation_sv_impl,
|
|
14
|
+
qubit_occupation_sv_den_mat_impl,
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
from pulser.backend import (
|
|
@@ -37,6 +39,10 @@ class SVConfig(EmulationConfig):
|
|
|
37
39
|
the Lanczos algorithm uses this as the convergence tolerance
|
|
38
40
|
gpu: Use 1 gpu if True, and a GPU is available, otherwise, cpu.
|
|
39
41
|
Will cause errors if True when a gpu is not available
|
|
42
|
+
interaction_cutoff: Set interaction coefficients below this value to `0`.
|
|
43
|
+
Potentially improves runtime and memory consumption.
|
|
44
|
+
log_level: How much to log. Set to `logging.WARN` to get rid of the timestep info.
|
|
45
|
+
log_file: If specified, log to this file rather than stout.
|
|
40
46
|
kwargs: arguments that are passed to the base class
|
|
41
47
|
|
|
42
48
|
Examples:
|
|
@@ -97,6 +103,10 @@ class SVConfig(EmulationConfig):
|
|
|
97
103
|
"Warning: The runs and samples_per_run "
|
|
98
104
|
"values of the NoiseModel are ignored!"
|
|
99
105
|
)
|
|
106
|
+
if "SPAM" in self.noise_model.noise_types:
|
|
107
|
+
raise NotImplementedError(
|
|
108
|
+
"SPAM errors are currently not supported in emu-sv."
|
|
109
|
+
)
|
|
100
110
|
|
|
101
111
|
def _expected_kwargs(self) -> set[str]:
|
|
102
112
|
return super()._expected_kwargs() | {
|
|
@@ -111,24 +121,41 @@ class SVConfig(EmulationConfig):
|
|
|
111
121
|
|
|
112
122
|
def monkeypatch_observables(self) -> None:
|
|
113
123
|
obs_list = []
|
|
124
|
+
|
|
114
125
|
for _, obs in enumerate(self.observables): # monkey patch
|
|
115
126
|
obs_copy = copy.deepcopy(obs)
|
|
127
|
+
|
|
116
128
|
if isinstance(obs, Occupation):
|
|
117
129
|
obs_copy.apply = MethodType( # type: ignore[method-assign]
|
|
118
|
-
|
|
130
|
+
(
|
|
131
|
+
qubit_occupation_sv_impl
|
|
132
|
+
if self.noise_model.noise_types == ()
|
|
133
|
+
else qubit_occupation_sv_den_mat_impl
|
|
134
|
+
),
|
|
135
|
+
obs_copy,
|
|
136
|
+
)
|
|
137
|
+
if isinstance(obs, CorrelationMatrix):
|
|
138
|
+
obs_copy.apply = MethodType( # type: ignore[method-assign]
|
|
139
|
+
(
|
|
140
|
+
correlation_matrix_sv_impl
|
|
141
|
+
if self.noise_model.noise_types == ()
|
|
142
|
+
else correlation_matrix_sv_den_mat_impl
|
|
143
|
+
),
|
|
144
|
+
obs_copy,
|
|
119
145
|
)
|
|
120
|
-
|
|
146
|
+
if isinstance(obs, EnergyVariance):
|
|
147
|
+
if self.noise_model.noise_types != ():
|
|
148
|
+
raise Exception("Not implemented for density matrix")
|
|
121
149
|
obs_copy.apply = MethodType( # type: ignore[method-assign]
|
|
122
150
|
energy_variance_sv_impl, obs_copy
|
|
123
151
|
)
|
|
124
152
|
elif isinstance(obs, EnergySecondMoment):
|
|
153
|
+
if self.noise_model.noise_types != ():
|
|
154
|
+
raise Exception("Not implemented for density matrix")
|
|
155
|
+
|
|
125
156
|
obs_copy.apply = MethodType( # type: ignore[method-assign]
|
|
126
157
|
energy_second_moment_sv_impl, obs_copy
|
|
127
158
|
)
|
|
128
|
-
elif isinstance(obs, CorrelationMatrix):
|
|
129
|
-
obs_copy.apply = MethodType( # type: ignore[method-assign]
|
|
130
|
-
correlation_matrix_sv_impl, obs_copy
|
|
131
|
-
)
|
|
132
159
|
obs_list.append(obs_copy)
|
|
133
160
|
self.observables = tuple(obs_list)
|
|
134
161
|
|
emu_sv/time_evolution.py
CHANGED
|
@@ -1,33 +1,366 @@
|
|
|
1
1
|
import torch
|
|
2
|
-
|
|
2
|
+
from typing import Any, no_type_check
|
|
3
3
|
from emu_base.math.krylov_exp import krylov_exp
|
|
4
|
+
from emu_base.math.double_krylov import double_krylov
|
|
4
5
|
from emu_sv.hamiltonian import RydbergHamiltonian
|
|
6
|
+
from emu_sv.lindblad_operator import RydbergLindbladian
|
|
7
|
+
|
|
5
8
|
|
|
9
|
+
def _apply_omega_real(
|
|
10
|
+
result: torch.Tensor,
|
|
11
|
+
i: int,
|
|
12
|
+
inds: torch.Tensor,
|
|
13
|
+
source: torch.Tensor,
|
|
14
|
+
alpha: complex,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""Accumulate to `result` the application of ασˣᵢ on `source`"""
|
|
17
|
+
result.index_add_(i, inds, source, alpha=alpha)
|
|
6
18
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
|
|
20
|
+
def _apply_omega_complex(
|
|
21
|
+
result: torch.Tensor,
|
|
22
|
+
i: int,
|
|
23
|
+
inds: torch.Tensor,
|
|
24
|
+
source: torch.Tensor,
|
|
25
|
+
alpha: complex,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Accumulate to `result` the application of ασ⁺ᵢ + α*σ⁻ᵢ on `source`"""
|
|
28
|
+
result.index_add_(i, inds[0], source.select(i, 0).unsqueeze(i), alpha=alpha)
|
|
29
|
+
result.index_add_(
|
|
30
|
+
i,
|
|
31
|
+
inds[1],
|
|
32
|
+
source.select(i, 1).unsqueeze(2),
|
|
33
|
+
alpha=alpha.conjugate(),
|
|
22
34
|
)
|
|
23
|
-
op = lambda x: -1j * dt * (ham * x)
|
|
24
35
|
|
|
25
|
-
|
|
26
|
-
|
|
36
|
+
|
|
37
|
+
class DHDOmegaSparse:
|
|
38
|
+
"""
|
|
39
|
+
Derivative of the RydbergHamiltonian respect to Omega.
|
|
40
|
+
∂H/∂Ωₖ = 0.5[cos(ϕₖ)σˣₖ + sin(ϕₖ)σʸₖ]
|
|
41
|
+
|
|
42
|
+
If ϕₖ=0, simplifies to ∂H/∂Ωₖ = 0.5σˣₖ
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, index: int, device: torch.device, nqubits: int, phi: torch.Tensor):
|
|
46
|
+
self.index = index
|
|
47
|
+
self.shape = (2**index, 2, 2 ** (nqubits - index - 1))
|
|
48
|
+
self.inds = torch.tensor([1, 0], device=device) # flips the state, for 𝜎ₓ
|
|
49
|
+
self.alpha = 0.5 * torch.exp(1j * phi).item()
|
|
50
|
+
if phi.is_nonzero():
|
|
51
|
+
self._apply_sigmas = _apply_omega_complex
|
|
52
|
+
else: # ∂H/∂Ωₖ = 0.5σˣₖ
|
|
53
|
+
self._apply_sigmas = _apply_omega_real
|
|
54
|
+
|
|
55
|
+
def __matmul__(self, vec: torch.Tensor) -> torch.Tensor:
|
|
56
|
+
vec = vec.view(vec.shape[0], *self.shape) # add batch dimension
|
|
57
|
+
result = torch.zeros_like(vec)
|
|
58
|
+
self._apply_sigmas(result, 2, self.inds, vec, alpha=self.alpha)
|
|
59
|
+
return result.view(vec.shape[0], -1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DHDPhiSparse:
|
|
63
|
+
"""
|
|
64
|
+
Derivative of the RydbergHamiltonian respect to Phi.
|
|
65
|
+
∂H/∂ϕₖ = 0.5Ωₖ[cos(ϕₖ+π/2)σˣₖ + sin(ϕₖ+π/2)σʸₖ]
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
index: int,
|
|
71
|
+
device: torch.device,
|
|
72
|
+
nqubits: int,
|
|
73
|
+
omega: torch.Tensor,
|
|
74
|
+
phi: torch.Tensor,
|
|
75
|
+
):
|
|
76
|
+
self.index = index
|
|
77
|
+
self.shape = (2**index, 2, 2 ** (nqubits - index - 1))
|
|
78
|
+
self.alpha = 0.5 * (omega * torch.exp(1j * (phi + torch.pi / 2))).item()
|
|
79
|
+
self.inds = torch.tensor([1, 0], device=device) # flips the state, for 𝜎ₓ
|
|
80
|
+
|
|
81
|
+
def __matmul__(self, vec: torch.Tensor) -> torch.Tensor:
|
|
82
|
+
vec = vec.view(vec.shape[0], *self.shape) # add batch dimension
|
|
83
|
+
result = torch.zeros_like(vec)
|
|
84
|
+
_apply_omega_complex(result, 2, self.inds, vec, alpha=self.alpha)
|
|
85
|
+
return result.view(vec.shape[0], -1)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class DHDDeltaSparse:
|
|
89
|
+
"""
|
|
90
|
+
Derivative of the Rydberg Hamiltonian respect to Delta:
|
|
91
|
+
∂H/∂Δᵢ = -nᵢ
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, i: int, nqubits: int):
|
|
95
|
+
self.nqubits = nqubits
|
|
96
|
+
self.shape = (2**i, 2, 2 ** (nqubits - i - 1))
|
|
97
|
+
|
|
98
|
+
def __matmul__(self, vec: torch.Tensor) -> torch.Tensor:
|
|
99
|
+
result = vec.clone()
|
|
100
|
+
result = result.view(vec.shape[0], *self.shape)
|
|
101
|
+
result[:, :, 0] = 0.0
|
|
102
|
+
return -result.view(vec.shape[0], 2**self.nqubits)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DHDUSparse:
|
|
106
|
+
"""
|
|
107
|
+
Derivative of the Rydberg Hamiltonian respect to the interaction matrix:
|
|
108
|
+
∂H/∂Uᵢⱼ = nᵢnⱼ
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, i: int, j: int, nqubits: int):
|
|
112
|
+
self.shape = (2**i, 2, 2 ** (j - i - 1), 2, 2 ** (nqubits - j - 1))
|
|
113
|
+
self.nqubits = nqubits
|
|
114
|
+
|
|
115
|
+
def __matmul__(self, vec: torch.Tensor) -> torch.Tensor:
|
|
116
|
+
result = vec.clone()
|
|
117
|
+
result = result.view(vec.shape[0], *self.shape)
|
|
118
|
+
result[:, :, 0] = 0.0
|
|
119
|
+
result[:, :, 1, :, 0] = 0.0
|
|
120
|
+
return result.view(vec.shape[0], 2**self.nqubits)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class EvolveStateVector(torch.autograd.Function):
|
|
124
|
+
"""Custom autograd implementation of a step in the time evolution."""
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def evolve(
|
|
128
|
+
dt: float,
|
|
129
|
+
omegas: torch.Tensor,
|
|
130
|
+
deltas: torch.Tensor,
|
|
131
|
+
phis: torch.Tensor,
|
|
132
|
+
interaction_matrix: torch.Tensor,
|
|
133
|
+
state: torch.Tensor,
|
|
134
|
+
krylov_tolerance: float,
|
|
135
|
+
) -> tuple[torch.Tensor, RydbergHamiltonian]:
|
|
136
|
+
ham = RydbergHamiltonian(
|
|
137
|
+
omegas=omegas,
|
|
138
|
+
deltas=deltas,
|
|
139
|
+
phis=phis,
|
|
140
|
+
interaction_matrix=interaction_matrix,
|
|
141
|
+
device=state.device,
|
|
142
|
+
)
|
|
143
|
+
op = lambda x: -1j * dt * (ham * x)
|
|
144
|
+
res = krylov_exp(
|
|
27
145
|
op,
|
|
28
|
-
|
|
146
|
+
state,
|
|
29
147
|
norm_tolerance=krylov_tolerance,
|
|
30
148
|
exp_tolerance=krylov_tolerance,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
149
|
+
is_hermitian=True,
|
|
150
|
+
)
|
|
151
|
+
return res, ham
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def forward(
|
|
155
|
+
ctx: Any,
|
|
156
|
+
dt: float,
|
|
157
|
+
omegas: torch.Tensor,
|
|
158
|
+
deltas: torch.Tensor,
|
|
159
|
+
phis: torch.Tensor,
|
|
160
|
+
interaction_matrix: torch.Tensor,
|
|
161
|
+
state: torch.Tensor,
|
|
162
|
+
krylov_tolerance: float,
|
|
163
|
+
) -> tuple[torch.Tensor, RydbergHamiltonian]:
|
|
164
|
+
"""
|
|
165
|
+
Returns the time evolved state
|
|
166
|
+
|ψ(t+dt)〉= exp(-i dt H)|ψ(t)〉
|
|
167
|
+
under the Hamiltonian H built from the input Tensor parameters, omegas, deltas, phis and
|
|
168
|
+
the interaction matrix.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
ctx (Any): context object to stash information for backward computation.
|
|
172
|
+
dt (float): timestep
|
|
173
|
+
omegas (torch.Tensor): 1D tensor of driving strengths for each qubit.
|
|
174
|
+
deltas (torch.Tensor): 1D tensor of detuning values for each qubit.
|
|
175
|
+
phis (torch.Tensor): 1D tensor of phase values for each qubit.
|
|
176
|
+
interaction_matrix (torch.Tensor): matrix representing the interaction
|
|
177
|
+
strengths between each pair of qubits.
|
|
178
|
+
state (Tensor): input state to be evolved
|
|
179
|
+
krylov_tolerance (float):
|
|
180
|
+
"""
|
|
181
|
+
res, ham = EvolveStateVector.evolve(
|
|
182
|
+
dt, omegas, deltas, phis, interaction_matrix, state, krylov_tolerance
|
|
183
|
+
)
|
|
184
|
+
ctx.save_for_backward(omegas, deltas, phis, interaction_matrix, state)
|
|
185
|
+
ctx.dt = dt
|
|
186
|
+
ctx.tolerance = krylov_tolerance
|
|
187
|
+
return res, ham
|
|
188
|
+
|
|
189
|
+
# mypy complains and I don't know why
|
|
190
|
+
# backward expects same number of gradients as output of forward, gham is unused
|
|
191
|
+
@no_type_check
|
|
192
|
+
@staticmethod
|
|
193
|
+
def backward(ctx: Any, grad_state_out: torch.Tensor, gham: None) -> tuple[
|
|
194
|
+
None,
|
|
195
|
+
torch.Tensor | None,
|
|
196
|
+
torch.Tensor | None,
|
|
197
|
+
torch.Tensor | None,
|
|
198
|
+
torch.Tensor | None,
|
|
199
|
+
torch.Tensor | None,
|
|
200
|
+
None,
|
|
201
|
+
]:
|
|
202
|
+
"""
|
|
203
|
+
In the backward pass we receive a Tensor containing the gradient of the loss L
|
|
204
|
+
with respect to the output
|
|
205
|
+
|gψ(t+dt)〉= ∂L/∂|ψ(t+dt)〉,
|
|
206
|
+
and return the gradients of the loss with respect to the input tensors parameters
|
|
207
|
+
- gΩⱼ = ∂L/∂Ωⱼ =〈gψ(t+dt)|dU(H,∂H/∂Ωⱼ)|ψ(t)〉
|
|
208
|
+
- gΔⱼ = ∂L/∂Δⱼ = ...
|
|
209
|
+
- |gψ(t)〉= ∂L/∂|ψ(t)〉= exp(i dt H)|gψ(t+dt)〉
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
ctx (Any): context object to stash information for backward computation.
|
|
213
|
+
grad_state_out (torch.Tensor): |gψ(t+dt)〉
|
|
214
|
+
|
|
215
|
+
Return:
|
|
216
|
+
grad_omegas (torch.Tensor): 1D tensor of gradients with respect to Ωⱼ for each qubit.
|
|
217
|
+
grad_deltas (torch.Tensor): 1D tensor of gradients with respect to Δⱼ for each qubit.
|
|
218
|
+
grad_phis (torch.Tensor): 1D tensor of gradients with respect to φⱼ for each qubit.
|
|
219
|
+
grad_state_in (torch.Tensor): 1D tensor gradient with respect to the input state.
|
|
220
|
+
|
|
221
|
+
Notes:
|
|
222
|
+
Gradients are obtained by matching the total variations
|
|
223
|
+
〈gψ(t+dt)|d|ψ(t+dt)〉= ∑ⱼgΔⱼ*dΔⱼ + ∑ⱼgΩⱼ*dΩⱼ + ∑ⱼgφ*dφⱼ +〈gψ(t)|d|ψ(t)〉 (1)
|
|
224
|
+
|
|
225
|
+
For the exponential map U = exp(-i dt H), differentiating reads:
|
|
226
|
+
d|ψ(t+dt)〉= dU|ψ(t)〉+ Ud|ψ(t)〉
|
|
227
|
+
dU = ∑ⱼdU(H,∂H/∂Δⱼ) + ∑ⱼdU(H,∂H/∂Ωⱼ) + ∑ⱼdU(H,∂H/∂φⱼ) (2)
|
|
228
|
+
|
|
229
|
+
where dU(H,E) is the Fréchet derivative of the exponential map
|
|
230
|
+
along the direction E:
|
|
231
|
+
- https://eprints.maths.manchester.ac.uk/1218/1/covered/MIMS_ep2008_26.pdf
|
|
232
|
+
- https://en.wikipedia.org/wiki/Derivative_of_the_exponential_map
|
|
233
|
+
|
|
234
|
+
Substituting (2) into (1) leads to the expressions of the gradients
|
|
235
|
+
with respect to the input tensors above.
|
|
236
|
+
|
|
237
|
+
Variations with respect to the Hamiltonian parameters are computed as
|
|
238
|
+
gΩ = 〈gψ(t+dt)|dU(H,∂H/∂Ω)|ψ(t)〉
|
|
239
|
+
= Tr( -i dt ∂H/∂Ω @ dU(H,|ψ(t)〉〈gψ(t+dt)|) ),
|
|
240
|
+
where under the trace sign, ∂H/∂Ω and |ψ(t)〉〈gψ(t+dt)| can be switched.
|
|
241
|
+
|
|
242
|
+
- The Fréchet derivative is computed in a Arnoldi-Gram-Schmidt
|
|
243
|
+
decomposition in the `double_krylov` method:
|
|
244
|
+
dU(H,|a〉〈b|) = Va @ dS @ Vb*
|
|
245
|
+
where Va,Vb are orthogonal Krylov basis associated
|
|
246
|
+
with |a〉and |b〉respectively.
|
|
247
|
+
|
|
248
|
+
- The action of the derivatives of the Hamiltonian with
|
|
249
|
+
respect to the input parameters are implemented separately in
|
|
250
|
+
- ∂H/∂Ω: `DHDOmegaSparse`
|
|
251
|
+
- ∂H/∂Δ: `DHDDeltaSparse`
|
|
252
|
+
- ∂H/∂φ: `DHDPhiSparse`
|
|
253
|
+
- ∂H/∂Uᵢⱼ `DHDUSparse`
|
|
254
|
+
|
|
255
|
+
Then, the resulting gradient respect to a generic parameter reads:
|
|
256
|
+
gΩ = Tr( -i dt ∂H/∂Ω @ Vs @ dS @ Vg* )
|
|
257
|
+
"""
|
|
258
|
+
omegas, deltas, phis, interaction_matrix, state = ctx.saved_tensors
|
|
259
|
+
dt = ctx.dt
|
|
260
|
+
tolerance = ctx.tolerance
|
|
261
|
+
nqubits = len(omegas)
|
|
262
|
+
|
|
263
|
+
grad_omegas, grad_deltas, grad_phis = None, None, None
|
|
264
|
+
grad_int_mat = None
|
|
265
|
+
grad_state_in = None
|
|
266
|
+
|
|
267
|
+
ham = RydbergHamiltonian(
|
|
268
|
+
omegas=omegas,
|
|
269
|
+
deltas=deltas,
|
|
270
|
+
phis=phis,
|
|
271
|
+
interaction_matrix=interaction_matrix,
|
|
272
|
+
device=state.device,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if any(ctx.needs_input_grad[1:5]):
|
|
276
|
+
op = lambda x: -1j * dt * (ham * x)
|
|
277
|
+
lanczos_vectors_state, dS, lanczos_vectors_grad = double_krylov(
|
|
278
|
+
op, state, grad_state_out, tolerance
|
|
279
|
+
)
|
|
280
|
+
# TODO: explore returning directly the basis in matrix form
|
|
281
|
+
Vs = torch.stack(lanczos_vectors_state)
|
|
282
|
+
del lanczos_vectors_state
|
|
283
|
+
Vg = torch.stack(lanczos_vectors_grad)
|
|
284
|
+
del lanczos_vectors_grad
|
|
285
|
+
e_l = dS.mT @ Vs
|
|
286
|
+
|
|
287
|
+
if ctx.needs_input_grad[1]:
|
|
288
|
+
grad_omegas = torch.zeros_like(omegas)
|
|
289
|
+
for i in range(nqubits):
|
|
290
|
+
# dh as per the docstring
|
|
291
|
+
dho = DHDOmegaSparse(i, e_l.device, nqubits, phis[i])
|
|
292
|
+
# compute the trace
|
|
293
|
+
v = dho @ e_l
|
|
294
|
+
grad_omegas[i] = (-1j * dt * torch.tensordot(Vg.conj(), v)).real
|
|
295
|
+
|
|
296
|
+
if ctx.needs_input_grad[2]:
|
|
297
|
+
grad_deltas = torch.zeros_like(deltas)
|
|
298
|
+
for i in range(nqubits):
|
|
299
|
+
dhd = DHDDeltaSparse(i, nqubits)
|
|
300
|
+
v = dhd @ e_l
|
|
301
|
+
grad_deltas[i] = (-1j * dt * torch.tensordot(Vg.conj(), v)).real
|
|
302
|
+
|
|
303
|
+
if ctx.needs_input_grad[3]:
|
|
304
|
+
grad_phis = torch.zeros_like(phis)
|
|
305
|
+
for i in range(nqubits):
|
|
306
|
+
dhp = DHDPhiSparse(i, e_l.device, nqubits, omegas[i], phis[i])
|
|
307
|
+
v = dhp @ e_l
|
|
308
|
+
grad_phis[i] = (-1j * dt * torch.tensordot(Vg.conj(), v)).real
|
|
309
|
+
|
|
310
|
+
if ctx.needs_input_grad[4]:
|
|
311
|
+
grad_int_mat = torch.zeros_like(interaction_matrix)
|
|
312
|
+
for i in range(nqubits):
|
|
313
|
+
for j in range(i + 1, nqubits):
|
|
314
|
+
dhu = DHDUSparse(i, j, nqubits)
|
|
315
|
+
v = dhu @ e_l
|
|
316
|
+
grad_int_mat[i, j] = (-1j * dt * torch.tensordot(Vg.conj(), v)).real
|
|
317
|
+
|
|
318
|
+
if ctx.needs_input_grad[5]:
|
|
319
|
+
op = lambda x: (1j * dt) * (ham * x)
|
|
320
|
+
grad_state_in = krylov_exp(op, grad_state_out.detach(), tolerance, tolerance)
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
None,
|
|
324
|
+
grad_omegas,
|
|
325
|
+
grad_deltas,
|
|
326
|
+
grad_phis,
|
|
327
|
+
grad_int_mat,
|
|
328
|
+
grad_state_in,
|
|
329
|
+
None,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class EvolveDensityMatrix:
|
|
334
|
+
"""Evolution of a density matrix under a Lindbladian operator."""
|
|
335
|
+
|
|
336
|
+
@staticmethod
|
|
337
|
+
def evolve(
|
|
338
|
+
dt: float,
|
|
339
|
+
omegas: torch.Tensor,
|
|
340
|
+
deltas: torch.Tensor,
|
|
341
|
+
phis: torch.Tensor,
|
|
342
|
+
full_interaction_matrix: torch.Tensor,
|
|
343
|
+
density_matrix: torch.Tensor,
|
|
344
|
+
krylov_tolerance: float,
|
|
345
|
+
pulser_lindblads: list[torch.Tensor],
|
|
346
|
+
) -> tuple[torch.Tensor, RydbergLindbladian]:
|
|
347
|
+
ham = RydbergLindbladian(
|
|
348
|
+
omegas=omegas,
|
|
349
|
+
deltas=deltas,
|
|
350
|
+
phis=phis,
|
|
351
|
+
pulser_linblads=pulser_lindblads,
|
|
352
|
+
interaction_matrix=full_interaction_matrix,
|
|
353
|
+
device=density_matrix.device,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
op = lambda x: -1j * dt * (ham @ x)
|
|
357
|
+
return (
|
|
358
|
+
krylov_exp(
|
|
359
|
+
op,
|
|
360
|
+
density_matrix,
|
|
361
|
+
norm_tolerance=krylov_tolerance,
|
|
362
|
+
exp_tolerance=krylov_tolerance,
|
|
363
|
+
is_hermitian=False,
|
|
364
|
+
),
|
|
365
|
+
ham,
|
|
366
|
+
)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: emu-sv
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Pasqal State Vector based pulse emulator built on PyTorch
|
|
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: emu-base==2.0
|
|
28
|
+
Requires-Dist: emu-base==2.2.0
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
|
|
31
31
|
<div align="center">
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
emu_sv/__init__.py,sha256=vVU7zIUsHPgFXUBWMn1lUtkWUg-avdFRqLUrtbmPvDI,771
|
|
2
|
+
emu_sv/custom_callback_implementations.py,sha256=j_G5x-xSnFSyKE81QN_DKoiTaF9JvNWaM1N6k2_t0Oo,5501
|
|
3
|
+
emu_sv/dense_operator.py,sha256=NfgzVpnNitc5ZSM4RlfpAc5Ls2wFPNsTxdeFdhJSg1o,6909
|
|
4
|
+
emu_sv/density_matrix_state.py,sha256=5W_UKIAYHb0k3ryRLQ2dbFUgrb5ju5jceDGAekM2gNE,7035
|
|
5
|
+
emu_sv/hamiltonian.py,sha256=CqNGuWJlO2ZljK47wt130s-5uKiOldQUsC3tjwk1mKA,6106
|
|
6
|
+
emu_sv/lindblad_operator.py,sha256=Rlxh24dXVUAutSrSacNO2ilNVlxKix8pfFt7h2CfVOg,7893
|
|
7
|
+
emu_sv/state_vector.py,sha256=zKHCdgl_eRIOPE4qVKO53ig9UyYTQ7a_guNFXgynU7g,9753
|
|
8
|
+
emu_sv/sv_backend.py,sha256=-soOkSEzEBK1dCKnYnbtvYjmNZtZra1_4jP3H1ROOtM,737
|
|
9
|
+
emu_sv/sv_backend_impl.py,sha256=mdPWBLDwH0q7EEwQTmLNLLx5tycMmsCQbUifIHvciMk,8059
|
|
10
|
+
emu_sv/sv_config.py,sha256=ixMTgDXKll4bXsYtAe4a_9Gng0bhwCgS42KKMwZCFHI,6308
|
|
11
|
+
emu_sv/time_evolution.py,sha256=_VH4f2RF6lGKzO08WxTYJ5Aw8_pTTMRKcyMnIuxH03I,13382
|
|
12
|
+
emu_sv/utils.py,sha256=-axfQ2tqw0C7I9yw-28g7lytyk373DNBjDALh4kLBrM,302
|
|
13
|
+
emu_sv-2.2.0.dist-info/METADATA,sha256=aw71GDNhy2P9-zjXjXszkLyaOY95qCpPkOmG2lAmYZg,3595
|
|
14
|
+
emu_sv-2.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
15
|
+
emu_sv-2.2.0.dist-info/RECORD,,
|
emu_sv-2.0.4.dist-info/RECORD
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
emu_sv/__init__.py,sha256=Tzc6RlABZ1ZVKt1mPUes9djq0eiK6FTgPagEHuFhF6Q,702
|
|
2
|
-
emu_sv/custom_callback_implementations.py,sha256=zvsSiDIc56gwybKq87VFZyKsniTDye6-oFd2-R0shpg,3447
|
|
3
|
-
emu_sv/dense_operator.py,sha256=NfgzVpnNitc5ZSM4RlfpAc5Ls2wFPNsTxdeFdhJSg1o,6909
|
|
4
|
-
emu_sv/density_matrix_state.py,sha256=6UBLUXaJaUdzOhflrKolcnH8737JszX7sry1WmbyakI,6993
|
|
5
|
-
emu_sv/hamiltonian.py,sha256=CqNGuWJlO2ZljK47wt130s-5uKiOldQUsC3tjwk1mKA,6106
|
|
6
|
-
emu_sv/lindblad_operator.py,sha256=KmaNCahpAi8SIXh-TrFD-ggmGpa1zklp8DMWVK9Y_J4,7433
|
|
7
|
-
emu_sv/state_vector.py,sha256=lqSbv4BMtDtgY0YUPuhIUNJxrlVa7vUWuN_XqwpG5sQ,9823
|
|
8
|
-
emu_sv/sv_backend.py,sha256=AkEtI6-SY20D0ORro3Kv8tHDRUc8gxejSiRa6d--vBE,4452
|
|
9
|
-
emu_sv/sv_config.py,sha256=QRy0VbCugmY6TQZ48nD6RxPJbpu0wzN7-E1Sud7YxLQ,5106
|
|
10
|
-
emu_sv/time_evolution.py,sha256=obV7DcHot0jtnEmjR1ilYiSyDcJ5rTvThRB8hFjP-2s,797
|
|
11
|
-
emu_sv/utils.py,sha256=-axfQ2tqw0C7I9yw-28g7lytyk373DNBjDALh4kLBrM,302
|
|
12
|
-
emu_sv-2.0.4.dist-info/METADATA,sha256=paHoVgW22OONoxvlwaypB-UF01zi0giZqUtvAz7fhmw,3513
|
|
13
|
-
emu_sv-2.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
-
emu_sv-2.0.4.dist-info/RECORD,,
|
|
File without changes
|