lockkernel 1.1.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.
- lockkernel/__init__.py +15 -0
- lockkernel/cumulant.py +399 -0
- lockkernel/ensemble.py +105 -0
- lockkernel/exact.py +243 -0
- lockkernel/kernels.py +214 -0
- lockkernel/lineshapes.py +148 -0
- lockkernel/measured.py +271 -0
- lockkernel/parametric.py +266 -0
- lockkernel-1.1.0.dist-info/METADATA +207 -0
- lockkernel-1.1.0.dist-info/RECORD +14 -0
- lockkernel-1.1.0.dist-info/WHEEL +5 -0
- lockkernel-1.1.0.dist-info/licenses/LICENSE +202 -0
- lockkernel-1.1.0.dist-info/licenses/NOTICE +17 -0
- lockkernel-1.1.0.dist-info/top_level.txt +1 -0
lockkernel/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Locking kernel universality: thresholds and exponents.
|
|
2
|
+
|
|
3
|
+
The package is organised around one object, the locking kernel W(u), and one
|
|
4
|
+
identity, the exact parametric solution of the locking self consistency in
|
|
5
|
+
`lockkernel.parametric`. The `lockkernel.measured` module is the lab half:
|
|
6
|
+
it fits a MEASURED synchronization branch for the threshold and the exponent,
|
|
7
|
+
with error bars and refusals, and reads the kernel tail back off the exponent
|
|
8
|
+
through the same dictionary the theory half proves.
|
|
9
|
+
"""
|
|
10
|
+
__version__ = "1.1.0"
|
|
11
|
+
|
|
12
|
+
from . import kernels, lineshapes, measured, parametric # noqa: F401
|
|
13
|
+
from .measured import (BranchFit, beta_relative_sigma, # noqa: F401
|
|
14
|
+
fit_branch, kernel_tail_from_beta,
|
|
15
|
+
points_for_beta)
|
lockkernel/cumulant.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Second order cumulant dynamics of the conservative model.
|
|
2
|
+
|
|
3
|
+
MODEL. Spin one half emitters with uniform all to all exchange and an
|
|
4
|
+
inhomogeneous distribution of detunings,
|
|
5
|
+
|
|
6
|
+
H = chi1 sum_{j,k} sigma+_j sigma-_k + sum_j (delta_j / 2) sigma z_j,
|
|
7
|
+
chi1 = chiN / N.
|
|
8
|
+
|
|
9
|
+
There is no damping, no drive and no thermal bath: the evolution is unitary.
|
|
10
|
+
This is the quantum model whose mean field limit obeys the locking law of
|
|
11
|
+
`lockkernel.parametric`.
|
|
12
|
+
|
|
13
|
+
VARIABLES. Emitters are grouped into classes of equal detuning. Within a
|
|
14
|
+
class every emitter is equivalent, so the state is carried by the class means
|
|
15
|
+
|
|
16
|
+
s_m = <sigma+_a>, z_m = <sigma z_a>, a in class m,
|
|
17
|
+
|
|
18
|
+
and by the CONNECTED pair correlations of two DISTINCT emitters a in class m
|
|
19
|
+
and b in class n,
|
|
20
|
+
|
|
21
|
+
P_mn = <sigma+_a sigma-_b> - s_m conj(s_n),
|
|
22
|
+
Q_mn = <sigma+_a sigma+_b> - s_m s_n,
|
|
23
|
+
R_mn = <sigma z_a sigma+_b> - z_m s_n,
|
|
24
|
+
Z_mn = <sigma z_a sigma z_b> - z_m z_n.
|
|
25
|
+
|
|
26
|
+
Connected correlations are of order 1/N while the raw moments are of order
|
|
27
|
+
one, so evolving them directly avoids the cancellation that makes raw moments
|
|
28
|
+
useless beyond N of order 1e8. The hierarchy is closed at third order by the
|
|
29
|
+
Gaussian rule <XYB>_c = <X><YB>_c + <Y><XB>_c.
|
|
30
|
+
|
|
31
|
+
FAR DETUNED EMITTERS. Emitters whose detuning is far outside the locking
|
|
32
|
+
bandwidth are propagated exactly as free spins rather than integrated. Their
|
|
33
|
+
population is retained in full, so the line is never truncated; only their
|
|
34
|
+
interaction, whose relative size is (chiN/delta)^2, is dropped. This removes
|
|
35
|
+
the stiffness of the far tail from the differential equation.
|
|
36
|
+
|
|
37
|
+
VALIDATION. `tests/test_cumulant.py` checks this solver against exact
|
|
38
|
+
diagonalisation of the full Hamiltonian for small emitter numbers with several
|
|
39
|
+
distinct detunings, and against the closed form of one axis twisting on
|
|
40
|
+
resonance.
|
|
41
|
+
"""
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import dataclasses
|
|
45
|
+
|
|
46
|
+
import numpy as np
|
|
47
|
+
from scipy.integrate import solve_ivp
|
|
48
|
+
|
|
49
|
+
__all__ = ["System", "State", "coherent_state_x", "evolve", "evolve_meanfield",
|
|
50
|
+
"wineland_xi2", "coherence", "collective_moments"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclasses.dataclass
|
|
54
|
+
class System:
|
|
55
|
+
"""Classes, populations and coupling.
|
|
56
|
+
|
|
57
|
+
Attributes
|
|
58
|
+
----------
|
|
59
|
+
delta : (M,) float
|
|
60
|
+
Detuning of each interacting class.
|
|
61
|
+
n : (M,) float
|
|
62
|
+
Population of each interacting class.
|
|
63
|
+
chiN : float
|
|
64
|
+
Coupling per emitter times the total emitter number.
|
|
65
|
+
spec_delta, spec_n : (K,) float
|
|
66
|
+
Detuning and population of the free, far detuned classes.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
delta: np.ndarray
|
|
70
|
+
n: np.ndarray
|
|
71
|
+
chiN: float
|
|
72
|
+
spec_delta: np.ndarray = None
|
|
73
|
+
spec_n: np.ndarray = None
|
|
74
|
+
|
|
75
|
+
def __post_init__(self):
|
|
76
|
+
self.delta = np.asarray(self.delta, float)
|
|
77
|
+
self.n = np.asarray(self.n, float)
|
|
78
|
+
if self.spec_delta is None:
|
|
79
|
+
self.spec_delta = np.zeros(0)
|
|
80
|
+
self.spec_n = np.zeros(0)
|
|
81
|
+
self.spec_delta = np.asarray(self.spec_delta, float)
|
|
82
|
+
self.spec_n = np.asarray(self.spec_n, float)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def M(self) -> int:
|
|
86
|
+
return len(self.delta)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def K(self) -> int:
|
|
90
|
+
return len(self.spec_delta)
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def N(self) -> float:
|
|
94
|
+
"""Total emitter number, interacting plus free."""
|
|
95
|
+
return float(self.n.sum() + self.spec_n.sum())
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def chi1(self) -> float:
|
|
99
|
+
return float(self.chiN) / self.N
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclasses.dataclass
|
|
103
|
+
class State:
|
|
104
|
+
"""Class means and connected pair correlations."""
|
|
105
|
+
|
|
106
|
+
s: np.ndarray
|
|
107
|
+
z: np.ndarray
|
|
108
|
+
P: np.ndarray
|
|
109
|
+
Q: np.ndarray
|
|
110
|
+
R: np.ndarray
|
|
111
|
+
Z: np.ndarray
|
|
112
|
+
vs: np.ndarray = None # (K,3) Bloch vectors of the free classes
|
|
113
|
+
|
|
114
|
+
def __post_init__(self):
|
|
115
|
+
if self.vs is None:
|
|
116
|
+
self.vs = np.zeros((0, 3))
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def M(self):
|
|
120
|
+
return len(self.s)
|
|
121
|
+
|
|
122
|
+
def pack(self):
|
|
123
|
+
return np.concatenate([self.s, self.z.astype(complex),
|
|
124
|
+
self.P.ravel(), self.Q.ravel(),
|
|
125
|
+
self.R.ravel(), self.Z.ravel()])
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def unpack(cls, y, M, vs=None):
|
|
129
|
+
b = y[2 * M:].reshape(4, M, M)
|
|
130
|
+
return cls(s=y[:M], z=y[M:2 * M], P=b[0], Q=b[1], R=b[2], Z=b[3], vs=vs)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def coherent_state_x(sys: System) -> State:
|
|
134
|
+
"""All emitters along +x: a coherent spin state, the standard start."""
|
|
135
|
+
M, K = sys.M, sys.K
|
|
136
|
+
zero = np.zeros((M, M), complex)
|
|
137
|
+
return State(np.full(M, 0.5, complex), np.zeros(M, complex),
|
|
138
|
+
zero.copy(), zero.copy(), zero.copy(), zero.copy(),
|
|
139
|
+
np.tile(np.array([1.0, 0.0, 0.0]), (K, 1)))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ---------------------------------------------------------------------------
|
|
143
|
+
# Equations of motion
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def _rhs(t, y, delta, n, chi1):
|
|
147
|
+
"""Right hand side in connected variables.
|
|
148
|
+
|
|
149
|
+
Every sum over partners excludes the emitters that already carry a free
|
|
150
|
+
index, which is what the `_ex` helper subtracts.
|
|
151
|
+
"""
|
|
152
|
+
M = len(delta)
|
|
153
|
+
st = State.unpack(y, M)
|
|
154
|
+
s, z, P, Q, R, Z = st.s, st.z, st.P, st.Q, st.R, st.Z
|
|
155
|
+
c = 1j * chi1
|
|
156
|
+
cc = -1j * chi1
|
|
157
|
+
A = 1j * (delta + chi1) # free precession of sigma-
|
|
158
|
+
Ac = np.conj(A)
|
|
159
|
+
sc = np.conj(s)
|
|
160
|
+
Rc = np.conj(R)
|
|
161
|
+
ns = n @ s # sum_p n_p s_p
|
|
162
|
+
|
|
163
|
+
# ---- first moments -----------------------------------------------------
|
|
164
|
+
S_zp = (R @ n - np.diag(R)) + z * (ns - s)
|
|
165
|
+
ds = -Ac * s + cc * S_zp
|
|
166
|
+
S_pm = (P @ n - np.diag(P)) + s * np.conj(ns - s)
|
|
167
|
+
dz = -4.0 * np.real(c * S_pm)
|
|
168
|
+
|
|
169
|
+
# ---- shorthands --------------------------------------------------------
|
|
170
|
+
zm, zn = z[:, None], z[None, :]
|
|
171
|
+
sm, sn = s[:, None], s[None, :]
|
|
172
|
+
scm, scn = sc[:, None], sc[None, :]
|
|
173
|
+
|
|
174
|
+
def _ex(full, at_m, at_n):
|
|
175
|
+
"""sum_p n_p T(p) with the terms p = m and p = n removed."""
|
|
176
|
+
return full - at_m - at_n
|
|
177
|
+
|
|
178
|
+
# ---- P -----------------------------------------------------------------
|
|
179
|
+
S1 = _ex(zm * (n @ P)[None, :] + Rc * ns,
|
|
180
|
+
zm * P + Rc * sm,
|
|
181
|
+
zm * np.diag(P)[None, :] + Rc * sn)
|
|
182
|
+
S2 = _ex(zn * (P @ n)[:, None] + R.T * np.conj(ns),
|
|
183
|
+
zn * np.diag(P)[:, None] + R.T * scm,
|
|
184
|
+
zn * P + R.T * scn)
|
|
185
|
+
src_a = 0.5 * (zm + zm * zn + Z) - R * scn - zm * (sn * scn)
|
|
186
|
+
src_b = 0.5 * (zn + zm * zn + Z) - sm * np.conj(R.T) - zn * (sm * scm)
|
|
187
|
+
dP = (-(Ac[:, None] + A[None, :]) * P
|
|
188
|
+
+ cc * (src_a + S1)
|
|
189
|
+
+ c * (src_b + S2))
|
|
190
|
+
|
|
191
|
+
# ---- Q -----------------------------------------------------------------
|
|
192
|
+
S3 = _ex(zm * (n @ Q)[None, :] + R * ns,
|
|
193
|
+
zm * Q + R * sm,
|
|
194
|
+
zm * np.diag(Q)[None, :] + R * sn)
|
|
195
|
+
S4 = _ex(zn * (Q @ n)[:, None] + R.T * ns,
|
|
196
|
+
zn * np.diag(Q)[:, None] + R.T * sm,
|
|
197
|
+
zn * Q + R.T * sn)
|
|
198
|
+
dQ = (-(Ac[:, None] + Ac[None, :]) * Q
|
|
199
|
+
+ cc * (-(R + zm * sn) * sn + S3)
|
|
200
|
+
+ cc * (-sm * (R.T + zn * sm) + S4))
|
|
201
|
+
|
|
202
|
+
# ---- R -----------------------------------------------------------------
|
|
203
|
+
S5 = _ex(c * (sm * (P @ n)[None, :] + Q * np.conj(ns))
|
|
204
|
+
+ cc * (P.T * ns + scm * (n @ Q)[None, :]),
|
|
205
|
+
c * (sm * P.T + Q * scm) + cc * (P.T * sm + scm * Q),
|
|
206
|
+
c * (sm * np.diag(P)[None, :] + Q * scn)
|
|
207
|
+
+ cc * (P.T * sn + scm * np.diag(Q)[None, :]))
|
|
208
|
+
S7 = _ex(zn * (R @ n)[:, None] + Z * ns,
|
|
209
|
+
zn * np.diag(R)[:, None] + Z * sm,
|
|
210
|
+
zn * R + Z * sn)
|
|
211
|
+
P_mn = P + sm * scn
|
|
212
|
+
P_nm = P.T + sn * scm
|
|
213
|
+
src_za = c * (0.5 * (sm - R.T - sm * zn) - P_mn * sn) + cc * (-P_nm * sn)
|
|
214
|
+
dR = (-2.0 * src_za
|
|
215
|
+
- 2.0 * S5
|
|
216
|
+
- Ac[None, :] * R
|
|
217
|
+
+ cc * (1.0 - zm) * (R.T + sm * zn)
|
|
218
|
+
+ cc * S7)
|
|
219
|
+
|
|
220
|
+
# ---- Z -----------------------------------------------------------------
|
|
221
|
+
S8 = _ex(c * (sm * (Rc @ n)[None, :] + R.T * np.conj(ns))
|
|
222
|
+
+ cc * (Rc.T * ns + scm * (R @ n)[None, :]),
|
|
223
|
+
c * (sm * Rc.T + R.T * scm) + cc * (Rc.T * sm + scm * R.T),
|
|
224
|
+
c * (sm * np.diag(Rc)[None, :] + R.T * scn)
|
|
225
|
+
+ cc * (Rc.T * sn + scm * np.diag(R)[None, :]))
|
|
226
|
+
S9 = _ex(c * (sn * (Rc @ n)[:, None] + R * np.conj(ns))
|
|
227
|
+
+ cc * (Rc * ns + scn * (R @ n)[:, None]),
|
|
228
|
+
c * (sn * np.diag(Rc)[:, None] + R * scm)
|
|
229
|
+
+ cc * (Rc * sm + scn * np.diag(R)[:, None]),
|
|
230
|
+
c * (sn * Rc + R * scn) + cc * (Rc * sn + scn * R))
|
|
231
|
+
src_zz_a = c * P_mn * (1.0 - zn) - cc * P_nm * (1.0 + zn)
|
|
232
|
+
src_zz_b = -c * P_nm * (1.0 + zm) + cc * P_mn * (1.0 - zm)
|
|
233
|
+
dZ = (-2.0 * (src_zz_a + src_zz_b) - 2.0 * S8 - 2.0 * S9)
|
|
234
|
+
|
|
235
|
+
return State(ds, dz, dP, dQ, dR, dZ).pack()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _rhs_meanfield(t, y, delta, n, chi1):
|
|
239
|
+
"""Mean field limit: the same equations with all correlations set to zero."""
|
|
240
|
+
M = len(delta)
|
|
241
|
+
s, z = y[:M], y[M:]
|
|
242
|
+
c = 1j * chi1
|
|
243
|
+
A = 1j * (delta + chi1)
|
|
244
|
+
ns = n @ s
|
|
245
|
+
ds = -np.conj(A) * s + np.conj(c) * z * (ns - s)
|
|
246
|
+
dz = -4.0 * np.real(c * s * np.conj(ns - s))
|
|
247
|
+
return np.concatenate([ds, dz])
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _free(vs, spec_delta, tau):
|
|
251
|
+
"""Exact free precession of the far detuned classes."""
|
|
252
|
+
if vs.shape[0] == 0:
|
|
253
|
+
return vs
|
|
254
|
+
ph = np.asarray(spec_delta) * tau
|
|
255
|
+
x, y, z = vs[:, 0], vs[:, 1], vs[:, 2]
|
|
256
|
+
return np.stack([x * np.cos(ph) - y * np.sin(ph),
|
|
257
|
+
y * np.cos(ph) + x * np.sin(ph), z], axis=1)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def evolve(st: State, sys: System, t: float, t_eval=None, rtol=1e-8,
|
|
261
|
+
atol=None, method="DOP853"):
|
|
262
|
+
"""Evolve the cumulant state. Returns a State, or a list at `t_eval`."""
|
|
263
|
+
if atol is None:
|
|
264
|
+
atol = 1e-10 / max(sys.N, 1.0)
|
|
265
|
+
args = (sys.delta, sys.n, sys.chi1)
|
|
266
|
+
sol = solve_ivp(_rhs, (0.0, t), st.pack(), args=args, t_eval=t_eval,
|
|
267
|
+
rtol=rtol, atol=atol, method=method)
|
|
268
|
+
if not sol.success:
|
|
269
|
+
raise RuntimeError(sol.message)
|
|
270
|
+
if t_eval is None:
|
|
271
|
+
return State.unpack(sol.y[:, -1], sys.M, _free(st.vs, sys.spec_delta, t))
|
|
272
|
+
return [State.unpack(sol.y[:, k], sys.M,
|
|
273
|
+
_free(st.vs, sys.spec_delta, sol.t[k]))
|
|
274
|
+
for k in range(sol.y.shape[1])]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def evolve_meanfield(sys: System, t: float, t_eval=None, rtol=1e-8, atol=1e-11,
|
|
278
|
+
method="DOP853"):
|
|
279
|
+
"""Mean field evolution from the coherent spin state along +x.
|
|
280
|
+
|
|
281
|
+
Returns (s, z) arrays with a column per requested time.
|
|
282
|
+
"""
|
|
283
|
+
y0 = np.concatenate([np.full(sys.M, 0.5, complex), np.zeros(sys.M, complex)])
|
|
284
|
+
sol = solve_ivp(_rhs_meanfield, (0.0, t), y0, args=(sys.delta, sys.n, sys.chi1),
|
|
285
|
+
t_eval=t_eval, rtol=rtol, atol=atol, method=method)
|
|
286
|
+
if not sol.success:
|
|
287
|
+
raise RuntimeError(sol.message)
|
|
288
|
+
return sol.y[:sys.M, :], sol.y[sys.M:, :]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
# ---------------------------------------------------------------------------
|
|
292
|
+
# Collective observables
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
def _cartesian(st: State):
|
|
296
|
+
"""Bloch vectors and the connected correlation tensor in Cartesian form."""
|
|
297
|
+
s, z, P, Q, R, Z = st.s, st.z, st.P, st.Q, st.R, st.Z
|
|
298
|
+
v = np.stack([2 * s.real, 2 * s.imag, z.real], axis=1)
|
|
299
|
+
M = st.M
|
|
300
|
+
C = np.empty((M, M, 3, 3), complex)
|
|
301
|
+
Qc, PT = np.conj(Q), P.T
|
|
302
|
+
C[:, :, 0, 0] = Q + P + PT + Qc
|
|
303
|
+
C[:, :, 0, 1] = -1j * (Q - P + PT - Qc)
|
|
304
|
+
C[:, :, 1, 0] = -1j * (Q + P - PT - Qc)
|
|
305
|
+
C[:, :, 1, 1] = -(Q - P - PT + Qc)
|
|
306
|
+
C[:, :, 0, 2] = 2 * R.T.real
|
|
307
|
+
C[:, :, 1, 2] = 2 * R.T.imag
|
|
308
|
+
C[:, :, 2, 0] = 2 * R.real
|
|
309
|
+
C[:, :, 2, 1] = 2 * R.imag
|
|
310
|
+
C[:, :, 2, 2] = Z
|
|
311
|
+
return v, C
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def collective_moments(st: State, sys: System):
|
|
315
|
+
"""Mean and symmetrised covariance of J = (1/2) sum_a sigma_a."""
|
|
316
|
+
n = sys.n
|
|
317
|
+
v, C = _cartesian(st)
|
|
318
|
+
J = 0.5 * (n @ v)
|
|
319
|
+
pair = np.einsum("m,n,mnab->ab", n, n, C) - np.einsum("m,mmab->ab", n, C)
|
|
320
|
+
same = np.sum(n) * np.eye(3) - np.einsum("m,ma,mb->ab", n, v, v)
|
|
321
|
+
Cov = 0.25 * (pair.real + same)
|
|
322
|
+
S1 = float(n.sum())
|
|
323
|
+
if sys.K:
|
|
324
|
+
sn = sys.spec_n
|
|
325
|
+
J = J + 0.5 * (sn @ st.vs)
|
|
326
|
+
Cov = Cov + 0.25 * (np.sum(sn) * np.eye(3)
|
|
327
|
+
- np.einsum("k,ka,kb->ab", sn, st.vs, st.vs))
|
|
328
|
+
S1 += float(sn.sum())
|
|
329
|
+
return J, 0.5 * (Cov + Cov.T), S1
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _transverse(J, Cov):
|
|
333
|
+
"""Smallest and largest variance in the plane perpendicular to J."""
|
|
334
|
+
Jn = np.linalg.norm(J)
|
|
335
|
+
e3 = J / Jn
|
|
336
|
+
trial = np.array([0.0, 0.0, 1.0]) if abs(e3[2]) < 0.9 else np.array([1.0, 0.0, 0.0])
|
|
337
|
+
e1 = np.cross(e3, trial)
|
|
338
|
+
e1 /= np.linalg.norm(e1)
|
|
339
|
+
e2 = np.cross(e3, e1)
|
|
340
|
+
B = np.stack([e1, e2], axis=1)
|
|
341
|
+
vals = np.linalg.eigvalsh(B.T @ Cov @ B)
|
|
342
|
+
return vals[0], vals[1], Jn
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def wineland_xi2(st: State, sys: System):
|
|
346
|
+
"""Wineland spin squeezing parameter xi_R^2 = N var_min / |<J>|^2.
|
|
347
|
+
|
|
348
|
+
Equal to one for a coherent spin state. A value below one certifies
|
|
349
|
+
metrological entanglement and a phase sensitivity beyond the standard
|
|
350
|
+
quantum limit.
|
|
351
|
+
"""
|
|
352
|
+
J, Cov, S1 = collective_moments(st, sys)
|
|
353
|
+
if np.linalg.norm(J) == 0:
|
|
354
|
+
return np.inf
|
|
355
|
+
vmin, _, Jn = _transverse(J, Cov)
|
|
356
|
+
return float(vmin * S1 / Jn ** 2)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def coherence(st: State, sys: System) -> float:
|
|
360
|
+
"""Transverse contrast 2 |<J_perp>| / N, the order parameter R."""
|
|
361
|
+
J, _, S1 = collective_moments(st, sys)
|
|
362
|
+
return float(2.0 * np.hypot(J[0], J[1]) / S1)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def physicality(st: State, sys: System):
|
|
366
|
+
"""Diagnostics that say whether the truncated state is still physical.
|
|
367
|
+
|
|
368
|
+
A second order cumulant expansion is not guaranteed to stay inside the set
|
|
369
|
+
of physical states, and in an undamped model the truncated hierarchy is
|
|
370
|
+
secularly unstable: the connected correlations, which should stay of order
|
|
371
|
+
1/N, eventually grow and the reconstructed covariance loses positivity.
|
|
372
|
+
Two exact properties of any true state are therefore monitored.
|
|
373
|
+
|
|
374
|
+
* The collective covariance must be positive semidefinite.
|
|
375
|
+
* Every class Bloch vector must lie inside the unit sphere.
|
|
376
|
+
|
|
377
|
+
Returns (min_eigenvalue_of_covariance, max_bloch_length). The state is
|
|
378
|
+
treated as physical while the first is not negative and the second does
|
|
379
|
+
not exceed one.
|
|
380
|
+
"""
|
|
381
|
+
_, Cov, _ = collective_moments(st, sys)
|
|
382
|
+
v, _ = _cartesian(st)
|
|
383
|
+
return (float(np.linalg.eigvalsh(Cov)[0]),
|
|
384
|
+
float(np.max(np.linalg.norm(v.real, axis=1))))
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def valid_window(states, sys: System, bloch_tol=1e-6):
|
|
388
|
+
"""Index of the last state that is still physical, walking forward in time.
|
|
389
|
+
|
|
390
|
+
Once the truncation has failed it does not recover, so the window is the
|
|
391
|
+
leading run of physical states rather than the set of all of them.
|
|
392
|
+
"""
|
|
393
|
+
last = -1
|
|
394
|
+
for k, st in enumerate(states):
|
|
395
|
+
mev, vmax = physicality(st, sys)
|
|
396
|
+
if mev < 0.0 or vmax > 1.0 + bloch_tol:
|
|
397
|
+
break
|
|
398
|
+
last = k
|
|
399
|
+
return last
|
lockkernel/ensemble.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Discretisation of a line into detuning classes.
|
|
2
|
+
|
|
3
|
+
The dynamics is solved class by class, so a continuous line has to be replaced
|
|
4
|
+
by a finite set of detunings carrying the right populations. Two features of
|
|
5
|
+
the problem dictate the construction.
|
|
6
|
+
|
|
7
|
+
* The line can have ALGEBRAIC TAILS. A uniform grid, or a grid truncated at a
|
|
8
|
+
few widths, changes the density at the centre once it is renormalised, and
|
|
9
|
+
the threshold depends on that density directly. The construction below
|
|
10
|
+
therefore keeps the entire population: nothing is discarded.
|
|
11
|
+
|
|
12
|
+
* The far tail is STIFF and IRRELEVANT. An emitter detuned by a thousand
|
|
13
|
+
linewidths precesses a thousand times faster than anything else in the
|
|
14
|
+
problem, which forces the integrator to tiny steps, while its interaction
|
|
15
|
+
with the mean field is smaller by (chiN/delta)^2. Such emitters are
|
|
16
|
+
therefore removed from the differential equation and propagated exactly as
|
|
17
|
+
free spins, with their population fully retained.
|
|
18
|
+
|
|
19
|
+
The three regions are
|
|
20
|
+
|
|
21
|
+
core |delta| < core_edge M_core equal probability bins
|
|
22
|
+
tail core_edge < |delta| < free M_tail logarithmic bins
|
|
23
|
+
free |delta| > free M_spec logarithmic bins, exact
|
|
24
|
+
|
|
25
|
+
with the node of each bin placed at its median, so that the first moment of
|
|
26
|
+
the population within the bin is represented rather than its edge.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import numpy as np
|
|
31
|
+
|
|
32
|
+
from .cumulant import System
|
|
33
|
+
from .lineshapes import LineShape
|
|
34
|
+
|
|
35
|
+
__all__ = ["build_system", "class_table"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _log_bins(dist, lo, hi, M, lump_last):
|
|
39
|
+
"""Logarithmic bins on [lo, hi] of the positive half line."""
|
|
40
|
+
edges = np.geomspace(lo, hi, M + 1)
|
|
41
|
+
nodes, mass = [], []
|
|
42
|
+
for k in range(M):
|
|
43
|
+
a, b = edges[k], edges[k + 1]
|
|
44
|
+
m = float(1.0 - dist.cdf(a)) if (lump_last and k == M - 1) \
|
|
45
|
+
else float(dist.cdf(b) - dist.cdf(a))
|
|
46
|
+
if not np.isfinite(m) or m <= 1e-14:
|
|
47
|
+
continue
|
|
48
|
+
node = float(dist.ppf(dist.cdf(a) + 0.5 * m))
|
|
49
|
+
if not np.isfinite(node):
|
|
50
|
+
node = float(np.sqrt(a * b))
|
|
51
|
+
nodes.append(node)
|
|
52
|
+
mass.append(m)
|
|
53
|
+
return np.array(nodes), np.array(mass)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def class_table(line: LineShape, M_core: int = 64, M_tail: int = 14,
|
|
57
|
+
M_spec: int = 8, core_edge: float = None,
|
|
58
|
+
free_beyond: float = None, delta_max: float = None):
|
|
59
|
+
"""Return (delta, n, spec_delta, spec_n) with populations summing to one."""
|
|
60
|
+
dist = line.frozen
|
|
61
|
+
if dist is None:
|
|
62
|
+
raise ValueError(f"{line.name} has no float distribution to discretise")
|
|
63
|
+
scale = float(line.scale)
|
|
64
|
+
core_edge = 3.0 * scale if core_edge is None else float(core_edge)
|
|
65
|
+
delta_max = 1000.0 * scale if delta_max is None else float(delta_max)
|
|
66
|
+
tail_end = delta_max if free_beyond is None else max(float(free_beyond),
|
|
67
|
+
core_edge * 1.001)
|
|
68
|
+
|
|
69
|
+
c_lo, c_hi = float(dist.cdf(-core_edge)), float(dist.cdf(core_edge))
|
|
70
|
+
q = c_lo + (c_hi - c_lo) * (np.arange(M_core) + 0.5) / M_core
|
|
71
|
+
d_core = np.asarray(dist.ppf(q), float)
|
|
72
|
+
n_core = np.full(M_core, (c_hi - c_lo) / M_core)
|
|
73
|
+
|
|
74
|
+
lump = free_beyond is None
|
|
75
|
+
d_t, n_t = _log_bins(dist, core_edge, tail_end, M_tail, lump)
|
|
76
|
+
delta = np.concatenate([-d_t[::-1], d_core, d_t])
|
|
77
|
+
n = np.concatenate([n_t[::-1], n_core, n_t])
|
|
78
|
+
|
|
79
|
+
if free_beyond is None:
|
|
80
|
+
s_d, s_n = np.zeros(0), np.zeros(0)
|
|
81
|
+
else:
|
|
82
|
+
d_s, n_s = _log_bins(dist, tail_end, delta_max, M_spec, True)
|
|
83
|
+
s_d = np.concatenate([-d_s[::-1], d_s])
|
|
84
|
+
s_n = np.concatenate([n_s[::-1], n_s])
|
|
85
|
+
|
|
86
|
+
total = n.sum() + s_n.sum()
|
|
87
|
+
return delta, n / total, s_d, s_n / total
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def build_system(line: LineShape, N: float, chiN: float, M_core: int = 64,
|
|
91
|
+
M_tail: int = 14, M_spec: int = 8, core_edge: float = None,
|
|
92
|
+
free_factor: float = 8.0, delta_max: float = None) -> System:
|
|
93
|
+
"""Build a `System` for a line, an emitter number and a coupling.
|
|
94
|
+
|
|
95
|
+
The boundary of the free region is placed at `free_factor` times the
|
|
96
|
+
larger of the coupling and twice the line width, so that it always sits
|
|
97
|
+
well outside the locking bandwidth. Convergence in `free_factor` is
|
|
98
|
+
checked in `scripts/vlasov_check.py`.
|
|
99
|
+
"""
|
|
100
|
+
scale = float(line.scale)
|
|
101
|
+
free = free_factor * max(abs(float(chiN)), 2.0 * scale)
|
|
102
|
+
delta, n, s_d, s_n = class_table(line, M_core, M_tail, M_spec,
|
|
103
|
+
core_edge, free, delta_max)
|
|
104
|
+
return System(delta=delta, n=n * N, chiN=float(chiN),
|
|
105
|
+
spec_delta=s_d, spec_n=s_n * N)
|