py-aisim 5.0.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.
- aisim/__init__.py +45 -0
- aisim/atoms.py +362 -0
- aisim/beam.py +329 -0
- aisim/convert.py +159 -0
- aisim/det.py +130 -0
- aisim/dist.py +104 -0
- aisim/prop.py +283 -0
- aisim/zern.py +304 -0
- py_aisim-5.0.1.dist-info/METADATA +784 -0
- py_aisim-5.0.1.dist-info/RECORD +13 -0
- py_aisim-5.0.1.dist-info/WHEEL +5 -0
- py_aisim-5.0.1.dist-info/licenses/LICENSE +674 -0
- py_aisim-5.0.1.dist-info/top_level.txt +1 -0
aisim/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Python package for simulating light-pulse atom interferometers."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from .atoms import AtomicEnsemble, create_random_ensemble
|
|
6
|
+
from .beam import IntensityProfile, Wavefront, Wavevectors, gen_wavefront
|
|
7
|
+
from .convert import cart2pol, phase_error_to_grav, pol2cart, temp, vel_from_temp
|
|
8
|
+
from .det import Detector, PolarDetector, SphericalDetector
|
|
9
|
+
from .dist import velocity_dist_for_box_pulse_velsel, velocity_dist_for_gaussian_velsel
|
|
10
|
+
from .prop import (
|
|
11
|
+
FreePropagator,
|
|
12
|
+
Propagator,
|
|
13
|
+
SpatialSuperpositionTransitionPropagator,
|
|
14
|
+
TwoLevelTransitionPropagator,
|
|
15
|
+
)
|
|
16
|
+
from .zern import ZernikeNorm, ZernikeOrder, ZernikePolynomial, j_to_n_m
|
|
17
|
+
|
|
18
|
+
__version__ = version("py-aisim")
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AtomicEnsemble",
|
|
22
|
+
"create_random_ensemble",
|
|
23
|
+
"IntensityProfile",
|
|
24
|
+
"Wavefront",
|
|
25
|
+
"Wavevectors",
|
|
26
|
+
"gen_wavefront",
|
|
27
|
+
"cart2pol",
|
|
28
|
+
"phase_error_to_grav",
|
|
29
|
+
"pol2cart",
|
|
30
|
+
"temp",
|
|
31
|
+
"vel_from_temp",
|
|
32
|
+
"Detector",
|
|
33
|
+
"PolarDetector",
|
|
34
|
+
"SphericalDetector",
|
|
35
|
+
"FreePropagator",
|
|
36
|
+
"Propagator",
|
|
37
|
+
"SpatialSuperpositionTransitionPropagator",
|
|
38
|
+
"TwoLevelTransitionPropagator",
|
|
39
|
+
"ZernikeNorm",
|
|
40
|
+
"ZernikeOrder",
|
|
41
|
+
"ZernikePolynomial",
|
|
42
|
+
"j_to_n_m",
|
|
43
|
+
"velocity_dist_for_box_pulse_velsel",
|
|
44
|
+
"velocity_dist_for_gaussian_velsel",
|
|
45
|
+
]
|
aisim/atoms.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Classes and functions related to the atomic cloud."""
|
|
2
|
+
|
|
3
|
+
from functools import partial
|
|
4
|
+
from typing import Callable, Literal
|
|
5
|
+
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import numpy as np
|
|
8
|
+
import scipy.linalg as splin
|
|
9
|
+
|
|
10
|
+
from .dist import position_dist_gaussian, velocity_dist_from_temp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AtomicEnsemble:
|
|
14
|
+
"""
|
|
15
|
+
Represents an atomic ensemble consisting of n atoms.
|
|
16
|
+
|
|
17
|
+
Each atom is is defined by its phase space vector (x0, y0, z0, vx, vy, vz) at time
|
|
18
|
+
t=0. From this phase space vector the position at later times can be calculated.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
phase_space_vectors : ndarray
|
|
23
|
+
n x 6 dimensional array representing the phase space vectors
|
|
24
|
+
(x0, y0, z0, vx, vy, vz) of the atoms in an atomic ensemble
|
|
25
|
+
state_kets : m x 1 or n x m x 1 array or list, optional
|
|
26
|
+
vector(s) representing the `m` internal degrees of freedom of the atoms. If the
|
|
27
|
+
list or array is one-dimensional, all atoms are initialized in the same internal
|
|
28
|
+
state. Alternatively, each atom can be initialized with a different state vector
|
|
29
|
+
by passing an array of state vectors for every atom. E.g. to initialize all
|
|
30
|
+
atoms in the ground state of a two-level system, pass `[1, 0]` which is the
|
|
31
|
+
default.
|
|
32
|
+
time : float, optional
|
|
33
|
+
the initial time (default 0) when the phase space and state vectors are
|
|
34
|
+
initialized
|
|
35
|
+
|
|
36
|
+
Attributes
|
|
37
|
+
----------
|
|
38
|
+
phase_space_vectors : ndarray
|
|
39
|
+
n x 6 dimensional array representing the phase space vectors
|
|
40
|
+
(x0, y0, z0, vx, vy, vz) of the atoms in an atomic ensemble
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, phase_space_vectors, state_kets=[1, 0], time: float = 0.0):
|
|
44
|
+
assert phase_space_vectors.shape[1] == 6
|
|
45
|
+
self.phase_space_vectors = phase_space_vectors
|
|
46
|
+
self.state_kets = state_kets
|
|
47
|
+
self.time = time
|
|
48
|
+
# for the future when we might implement forces
|
|
49
|
+
self.initial_position = self.phase_space_vectors[:, 0:3]
|
|
50
|
+
self.initial_velocity = self.phase_space_vectors[:, 3:6]
|
|
51
|
+
|
|
52
|
+
def __getitem__(self, key):
|
|
53
|
+
"""Select certain atoms "from the ensemble.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
key : int or slice or bool map
|
|
58
|
+
for example 2, 1:15 or a boolean map
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
new_instance : AtomicEnsemble
|
|
63
|
+
a new instance of the atomic ensemble only containing the selected atoms
|
|
64
|
+
"""
|
|
65
|
+
phase_space_vectors = self.phase_space_vectors[key][:]
|
|
66
|
+
state_kets = self.state_kets[key][:]
|
|
67
|
+
if isinstance(key, int):
|
|
68
|
+
# retain correct shape in case of only one atom is selected
|
|
69
|
+
phase_space_vectors = phase_space_vectors.reshape(1, 6)
|
|
70
|
+
state_kets = state_kets.reshape(1, len(state_kets))
|
|
71
|
+
new_instance = AtomicEnsemble(
|
|
72
|
+
phase_space_vectors=phase_space_vectors,
|
|
73
|
+
state_kets=state_kets,
|
|
74
|
+
)
|
|
75
|
+
return new_instance
|
|
76
|
+
|
|
77
|
+
def __len__(self):
|
|
78
|
+
"""Return the number of atoms in the ensemble."""
|
|
79
|
+
return self.phase_space_vectors.shape[0]
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def time(self):
|
|
83
|
+
"""float: Time changes when propagating the atomic ensemble."""
|
|
84
|
+
return self._time
|
|
85
|
+
|
|
86
|
+
@time.setter
|
|
87
|
+
def time(self, value):
|
|
88
|
+
self._time = value
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def state_kets(self):
|
|
92
|
+
"""(n x m x 1) array: The ket vectors of the m level system."""
|
|
93
|
+
return self._state_kets
|
|
94
|
+
|
|
95
|
+
@state_kets.setter
|
|
96
|
+
def state_kets(self, new_state_kets):
|
|
97
|
+
if isinstance(new_state_kets, list):
|
|
98
|
+
new_state_kets = np.array([new_state_kets]).T
|
|
99
|
+
if new_state_kets.ndim == 2:
|
|
100
|
+
# state vector is the same for all atoms
|
|
101
|
+
self._state_kets = np.repeat([new_state_kets], len(self), axis=0)
|
|
102
|
+
else:
|
|
103
|
+
# there has to be a state vector for every atom in the ensemble
|
|
104
|
+
assert new_state_kets.shape[0] == len(self)
|
|
105
|
+
self._state_kets = new_state_kets
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def state_bras(self):
|
|
109
|
+
"""(n x 1 x m) array: The bra vectors of the m level system."""
|
|
110
|
+
# exchange second and third index, then complex conjugate
|
|
111
|
+
return np.conjugate(np.einsum("ijk->ikj", self.state_kets))
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def density_matrices(self):
|
|
115
|
+
"""
|
|
116
|
+
(n x m x m) array: Density matrix of the m level system of the n atoms.
|
|
117
|
+
|
|
118
|
+
These are pure states.
|
|
119
|
+
"""
|
|
120
|
+
# |Psi><Psi|
|
|
121
|
+
return np.einsum("ijk,ijk->ijk", self.state_kets, self.state_bras)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def density_matrix(self):
|
|
125
|
+
"""(m x m) array: Density matrix of the ensemble's m level system."""
|
|
126
|
+
pure_dm = self.density_matrices
|
|
127
|
+
n_atoms = self.state_kets.shape[0]
|
|
128
|
+
# sum over pure |Psi><Psi| and divide by N
|
|
129
|
+
return 1 / n_atoms * np.einsum("ijk->jk", pure_dm)
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def position(self):
|
|
133
|
+
"""(n x 3) array: Positions (x, y, z) of the atoms in the ensemble."""
|
|
134
|
+
return self.phase_space_vectors[:, 0:3]
|
|
135
|
+
|
|
136
|
+
@position.setter
|
|
137
|
+
def position(self, new_position):
|
|
138
|
+
self.phase_space_vectors[:, 0:3] = new_position
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def velocity(self):
|
|
142
|
+
"""n x 3) array: Velocities (vx, vy, vz) of the atoms in the ensemble."""
|
|
143
|
+
return self.phase_space_vectors[:, 3:6]
|
|
144
|
+
|
|
145
|
+
@velocity.setter
|
|
146
|
+
def velocity(self, new_velocity):
|
|
147
|
+
self.phase_space_vectors[:, 3:6] = new_velocity
|
|
148
|
+
|
|
149
|
+
def calc_position(self, t):
|
|
150
|
+
"""
|
|
151
|
+
Calculate the positions (x, y, z) of the atoms after propagation.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
t : float
|
|
156
|
+
time when the positions should be calculated
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
pos : array
|
|
161
|
+
n x 3 dimensional array of the positions (x, y, z)
|
|
162
|
+
"""
|
|
163
|
+
return self.initial_position + self.initial_velocity * t
|
|
164
|
+
|
|
165
|
+
def state_occupation(self, state):
|
|
166
|
+
"""
|
|
167
|
+
Calculate the state population of each atom in the ensemble.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
state : int or list_like
|
|
172
|
+
Specifies which state population should be calculated. E.g. the excited
|
|
173
|
+
state of a two-level system can be calculated by passing either 1 or [0, 1].
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
occupation : array
|
|
178
|
+
n dimensional array of the state population of each of the n atom
|
|
179
|
+
"""
|
|
180
|
+
# create bra on which the kets of the atomic ensemble are projected
|
|
181
|
+
if isinstance(state, (int, np.integer)):
|
|
182
|
+
# list of zeros
|
|
183
|
+
zeros = [0] * self.state_kets.shape[1]
|
|
184
|
+
# set entry to 1, overwrite state variable
|
|
185
|
+
zeros[state] = 1
|
|
186
|
+
state = zeros
|
|
187
|
+
if isinstance(state, list):
|
|
188
|
+
# check that bases match
|
|
189
|
+
assert len(state) == self.state_kets.shape[1]
|
|
190
|
+
state = np.array(state)
|
|
191
|
+
projection_bras = np.repeat(np.array([[state]]), len(self), axis=0)
|
|
192
|
+
# |<i|Psi>|^2
|
|
193
|
+
occupation = np.abs(np.matmul(projection_bras, self.state_kets)) ** 2
|
|
194
|
+
return occupation.flatten()
|
|
195
|
+
|
|
196
|
+
def fidelity(self, rho_target):
|
|
197
|
+
"""
|
|
198
|
+
Calculate fidelity of ensemble's density matrix and target matrix [1].
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
rho_target : array
|
|
203
|
+
target density matrix as m x m array
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
fidelity : float
|
|
208
|
+
fidelity of AtomicEnsemble's compared to target density matrix
|
|
209
|
+
|
|
210
|
+
References
|
|
211
|
+
----------
|
|
212
|
+
[1] https://en.wikipedia.org/wiki/Fidelity_of_quantum_states
|
|
213
|
+
"""
|
|
214
|
+
return _fidelity(self.density_matrix, rho_target)
|
|
215
|
+
|
|
216
|
+
def plot(
|
|
217
|
+
self,
|
|
218
|
+
ax: plt.Axes | None = None,
|
|
219
|
+
view_from: Literal["x", "y", "z"] = "z",
|
|
220
|
+
bins: int = 50,
|
|
221
|
+
**kwargs,
|
|
222
|
+
) -> tuple[plt.Figure, plt.Axes]:
|
|
223
|
+
"""Plot the positions of the atoms in the ensemble.
|
|
224
|
+
|
|
225
|
+
ax : Axis, optional
|
|
226
|
+
If axis is provided, they will be used for the plot. if not provided, a new
|
|
227
|
+
plot will automatically be created.
|
|
228
|
+
view_from : str
|
|
229
|
+
View from which direction the plot is created. Options are "x", "y", "z".
|
|
230
|
+
bins : int
|
|
231
|
+
Number of bins for the histogram
|
|
232
|
+
**kwargs
|
|
233
|
+
Additional keyword arguments for the plot function
|
|
234
|
+
|
|
235
|
+
Returns
|
|
236
|
+
-------
|
|
237
|
+
fig, ax : tuple of plt.Figure and plt.Axes
|
|
238
|
+
The figure and axis of the plot
|
|
239
|
+
"""
|
|
240
|
+
if ax is None:
|
|
241
|
+
fig, ax = plt.subplots(subplot_kw=dict(projection="polar"))
|
|
242
|
+
else:
|
|
243
|
+
fig = ax.figure
|
|
244
|
+
|
|
245
|
+
view = {
|
|
246
|
+
"x": (1, 2),
|
|
247
|
+
"y": (0, 2),
|
|
248
|
+
"z": (0, 1),
|
|
249
|
+
}
|
|
250
|
+
ax.hist2d(
|
|
251
|
+
self.position[:, view[view_from][0]],
|
|
252
|
+
self.position[:, view[view_from][1]],
|
|
253
|
+
bins=bins,
|
|
254
|
+
**kwargs,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return fig, ax
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def create_random_ensemble(
|
|
261
|
+
n_samples: int,
|
|
262
|
+
*,
|
|
263
|
+
mean_x: float = 0.0,
|
|
264
|
+
mean_y: float = 0.0,
|
|
265
|
+
mean_z: float = 0.0,
|
|
266
|
+
mean_vx: float = 0.0,
|
|
267
|
+
mean_vy: float = 0.0,
|
|
268
|
+
mean_vz: float = 0.0,
|
|
269
|
+
x_dist: Callable[[int], np.ndarray] = partial(position_dist_gaussian, std=1.5e-3),
|
|
270
|
+
y_dist: Callable[[int], np.ndarray] = partial(position_dist_gaussian, std=1.5e-3),
|
|
271
|
+
z_dist: Callable[[int], np.ndarray] = partial(position_dist_gaussian, std=1.5e-3),
|
|
272
|
+
vx_dist: Callable[[int], np.ndarray] = partial(
|
|
273
|
+
velocity_dist_from_temp, temperature=3e-6
|
|
274
|
+
),
|
|
275
|
+
vy_dist: Callable[[int], np.ndarray] = partial(
|
|
276
|
+
velocity_dist_from_temp, temperature=3e-6
|
|
277
|
+
),
|
|
278
|
+
vz_dist: Callable[[int], np.ndarray] = partial(
|
|
279
|
+
velocity_dist_from_temp, temperature=160e-9
|
|
280
|
+
),
|
|
281
|
+
seed: int | None = None,
|
|
282
|
+
**kwargs,
|
|
283
|
+
) -> AtomicEnsemble:
|
|
284
|
+
"""Create a random atomic ensemble from given distributions.
|
|
285
|
+
|
|
286
|
+
Have a look at the example notebooks to see how to use `partial` to create
|
|
287
|
+
distributions with different user defined parameters.
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
n_samples : int
|
|
292
|
+
number of random samples
|
|
293
|
+
mean_x, mean_y, mean_z : float, optional
|
|
294
|
+
mean position of the atomic ensemble in meters (default 0.0)
|
|
295
|
+
mean_vx, mean_vy, mean_vz : float, optional
|
|
296
|
+
mean velocity of the atomic ensemble in meters per second (default 0.0)
|
|
297
|
+
x_dist, y_dist, z_dist : callable
|
|
298
|
+
Function that returns a random position distribution. The function takes a
|
|
299
|
+
single integer argument `n` and returns a n-dimensional array of random samples.
|
|
300
|
+
To be used with `partial` to set the parameters of the distributions from the
|
|
301
|
+
`dist` module.
|
|
302
|
+
vx_dist, vy_dist, vz_dist : callable
|
|
303
|
+
Function that returns a random velocity distribution. The function takes a
|
|
304
|
+
single integer argument `n` and returns a n-dimensional array of random samples.
|
|
305
|
+
To be used with `partial` to set the parameters of the distributions from the
|
|
306
|
+
`dist` module.
|
|
307
|
+
seed : int or 1-d array_like, optional
|
|
308
|
+
Set the seed of the random number generator to get predictable samples. If set,
|
|
309
|
+
this number is passed to `numpy.random.seed`.
|
|
310
|
+
**kwargs :
|
|
311
|
+
Optional keyworded arguments passed to `AtomicEnsemble`
|
|
312
|
+
|
|
313
|
+
Returns
|
|
314
|
+
-------
|
|
315
|
+
ensemble : AtomicEnsemble
|
|
316
|
+
Atomic ensemble containing the generated phase space vectors.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
if seed is not None:
|
|
320
|
+
np.random.seed(seed)
|
|
321
|
+
|
|
322
|
+
# initialize vector with phase-space entries and fill them
|
|
323
|
+
phase_space_vectors = np.zeros((n_samples, 6))
|
|
324
|
+
phase_space_vectors[:, 0] = x_dist(n_samples) + mean_x
|
|
325
|
+
phase_space_vectors[:, 1] = y_dist(n_samples) + mean_y
|
|
326
|
+
phase_space_vectors[:, 2] = z_dist(n_samples) + mean_z
|
|
327
|
+
phase_space_vectors[:, 3] = vx_dist(n_samples) + mean_vx
|
|
328
|
+
phase_space_vectors[:, 4] = vy_dist(n_samples) + mean_vy
|
|
329
|
+
phase_space_vectors[:, 5] = vz_dist(n_samples) + mean_vz
|
|
330
|
+
ensemble = AtomicEnsemble(phase_space_vectors, **kwargs)
|
|
331
|
+
return ensemble
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _fidelity(rho_a, rho_b):
|
|
335
|
+
"""
|
|
336
|
+
Calculate the fidelity of two density matrices [1, 2].
|
|
337
|
+
|
|
338
|
+
Parameters
|
|
339
|
+
----------
|
|
340
|
+
rho_a : array
|
|
341
|
+
density matrix as m x m array
|
|
342
|
+
rho_b : array
|
|
343
|
+
density matrix as m x m array
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
fidelity : float
|
|
348
|
+
fidelity of both density matrices
|
|
349
|
+
|
|
350
|
+
References
|
|
351
|
+
----------
|
|
352
|
+
[1] https://en.wikipedia.org/wiki/Fidelity_of_quantum_states
|
|
353
|
+
[2] http://qutip.org/docs/4.0.2/modules/qutip/metrics.html
|
|
354
|
+
"""
|
|
355
|
+
assert rho_a.shape == rho_b.shape
|
|
356
|
+
sqrt_rho_a = splin.sqrtm(rho_a) # matrix square root
|
|
357
|
+
# Implementation used in qutip"s fidelity calculation: calculating the eigenvalues
|
|
358
|
+
# and taking it's square root instead of matrix square root and taking the trace.
|
|
359
|
+
# It's faster.
|
|
360
|
+
eig_vals = np.linalg.eigvals(sqrt_rho_a @ rho_b @ sqrt_rho_a)
|
|
361
|
+
fidelity = np.real(np.sum(np.sqrt(eig_vals))) ** 2
|
|
362
|
+
return fidelity
|