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 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