lockkernel 1.1.0__tar.gz → 1.1.1__tar.gz

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.
Files changed (29) hide show
  1. lockkernel-1.1.1/MANIFEST.in +1 -0
  2. lockkernel-1.1.1/PKG-INFO +679 -0
  3. lockkernel-1.1.1/README.md +649 -0
  4. {lockkernel-1.1.0 → lockkernel-1.1.1}/pyproject.toml +1 -1
  5. {lockkernel-1.1.0 → lockkernel-1.1.1}/src/lockkernel/__init__.py +1 -1
  6. {lockkernel-1.1.0 → lockkernel-1.1.1}/src/lockkernel/parametric.py +22 -11
  7. lockkernel-1.1.1/src/lockkernel.egg-info/PKG-INFO +679 -0
  8. {lockkernel-1.1.0 → lockkernel-1.1.1}/src/lockkernel.egg-info/SOURCES.txt +2 -0
  9. lockkernel-1.1.1/tests/conftest.py +23 -0
  10. {lockkernel-1.1.0 → lockkernel-1.1.1}/tests/test_measured.py +1 -1
  11. {lockkernel-1.1.0 → lockkernel-1.1.1}/tests/test_parametric.py +15 -0
  12. {lockkernel-1.1.0 → lockkernel-1.1.1}/tests/test_universality.py +6 -5
  13. lockkernel-1.1.0/PKG-INFO +0 -207
  14. lockkernel-1.1.0/README.md +0 -177
  15. lockkernel-1.1.0/src/lockkernel.egg-info/PKG-INFO +0 -207
  16. {lockkernel-1.1.0 → lockkernel-1.1.1}/LICENSE +0 -0
  17. {lockkernel-1.1.0 → lockkernel-1.1.1}/NOTICE +0 -0
  18. {lockkernel-1.1.0 → lockkernel-1.1.1}/setup.cfg +0 -0
  19. {lockkernel-1.1.0 → lockkernel-1.1.1}/src/lockkernel/cumulant.py +0 -0
  20. {lockkernel-1.1.0 → lockkernel-1.1.1}/src/lockkernel/ensemble.py +0 -0
  21. {lockkernel-1.1.0 → lockkernel-1.1.1}/src/lockkernel/exact.py +0 -0
  22. {lockkernel-1.1.0 → lockkernel-1.1.1}/src/lockkernel/kernels.py +0 -0
  23. {lockkernel-1.1.0 → lockkernel-1.1.1}/src/lockkernel/lineshapes.py +0 -0
  24. {lockkernel-1.1.0 → lockkernel-1.1.1}/src/lockkernel/measured.py +0 -0
  25. {lockkernel-1.1.0 → lockkernel-1.1.1}/src/lockkernel.egg-info/dependency_links.txt +0 -0
  26. {lockkernel-1.1.0 → lockkernel-1.1.1}/src/lockkernel.egg-info/requires.txt +0 -0
  27. {lockkernel-1.1.0 → lockkernel-1.1.1}/src/lockkernel.egg-info/top_level.txt +0 -0
  28. {lockkernel-1.1.0 → lockkernel-1.1.1}/tests/test_cumulant.py +0 -0
  29. {lockkernel-1.1.0 → lockkernel-1.1.1}/tests/test_meanfield.py +0 -0
@@ -0,0 +1 @@
1
+ include tests/conftest.py
@@ -0,0 +1,679 @@
1
+ Metadata-Version: 2.4
2
+ Name: lockkernel
3
+ Version: 1.1.1
4
+ Summary: Locking kernel universality: exact thresholds and exponents of synchronization transitions, and fits of measured branches with error bars and refusals
5
+ Author: Tanvir Mahmud Mahim
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/TaN-MM-Org/lockkernel
8
+ Project-URL: Issues, https://github.com/TaN-MM-Org/lockkernel/issues
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering :: Physics
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ License-File: NOTICE
24
+ Requires-Dist: numpy>=1.22
25
+ Requires-Dist: scipy>=1.8
26
+ Requires-Dist: mpmath>=1.2
27
+ Provides-Extra: test
28
+ Requires-Dist: pytest>=7; extra == "test"
29
+ Dynamic: license-file
30
+
31
+ # lockkernel
32
+
33
+ [![PyPI](https://img.shields.io/pypi/v/lockkernel.svg)](https://pypi.org/project/lockkernel/) [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.22829483-blue)](https://doi.org/10.5281/zenodo.22829483) [![tests](https://github.com/TaN-MM-Org/lockkernel/actions/workflows/ci.yml/badge.svg)](https://github.com/TaN-MM-Org/lockkernel/actions)
34
+ [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
35
+
36
+ `lockkernel` is a Python package about **synchronization**: many
37
+ oscillators, each with a slightly different natural frequency, start to
38
+ move in step once the coupling between them is strong enough. Below a
39
+ certain coupling (the **threshold**) nothing happens; above it, a
40
+ collective motion appears and grows. The package answers the questions
41
+ one asks about that onset:
42
+
43
+ - At what coupling does synchronization start?
44
+ - How fast does it grow above that point? Near the threshold the growth
45
+ follows a power law, and the power is called the **exponent**
46
+ (`beta`).
47
+ - Is the onset smooth, or does the system jump and show hysteresis
48
+ (a first-order onset)?
49
+ - Given a **measured** onset, from an experiment or a simulation, what
50
+ are the threshold and the exponent, with error bars? What does the
51
+ exponent say about the system? How many measurements does a target
52
+ error bar need?
53
+
54
+ It has two halves. The **theory half** computes the onset exactly from
55
+ two inputs: the spread of natural frequencies, and the **locking
56
+ kernel**, a function that says how strongly an oscillator follows the
57
+ group as a function of how far its own frequency is from the group's.
58
+ The collective motion must be the one the oscillators themselves
59
+ produce (the "self-consistency condition"); one substitution turns
60
+ that condition into an exact formula (a "parametric solution") valid
61
+ for any frequency spread and any kernel. The exponent depends only on how fast the kernel falls off
62
+ far from the centre (its **tail**). The **lab half** (`lockkernel.measured`)
63
+ works the other way round: it fits measured points for the threshold
64
+ and the exponent, reads the kernel's tail back off the exponent, and
65
+ plans how many points a target error bar costs. When the data cannot
66
+ support the number asked for, it stops with an error message that says
67
+ why, rather than returning a number that looks fine but is not.
68
+
69
+ ## Contents
70
+
71
+ - [A short guide to the words used here](#a-short-guide-to-the-words-used-here)
72
+ - [Install and conventions](#install-and-conventions)
73
+ - [Examples](#examples) (each with the output it prints)
74
+ - [What is in the package](#what-is-in-the-package)
75
+ - [When it refuses, and why](#when-it-refuses-and-why)
76
+ - [How the results are checked](#how-the-results-are-checked)
77
+ - [Corrections in earlier versions](#corrections-in-earlier-versions)
78
+ - [Limits](#limits)
79
+ - [Where it comes from](#where-it-comes-from)
80
+ - [Citing, support and license](#citing-support-and-license)
81
+
82
+ ## A short guide to the words used here
83
+
84
+ - **Detuning** `delta` -- how far one oscillator's natural frequency
85
+ is from the centre of the group.
86
+ - **Line shape** `p(delta)` -- the distribution of detunings across
87
+ the group (the name comes from spectroscopy, where it is the shape
88
+ of a spectral line). The package ships a Lorentzian, a Gaussian, a
89
+ Student-t (heavy tails), a box (flat) and a two-peaked Gaussian, and
90
+ you can build your own `LineShape`.
91
+ - **Coupling** `chiN` -- the coupling per oscillator times the number
92
+ of oscillators. The **threshold** `chiN_c` is the coupling where
93
+ synchronization starts.
94
+ - **Order parameter** `R` -- how synchronized the group is: 0 for no
95
+ common motion, 1 for perfect step. It is zero below the threshold.
96
+ - **Reduced coupling** `eps = chiN/chiN_c - 1` -- the relative
97
+ distance above the threshold. Near the onset `R ~ A eps^beta`, with
98
+ **amplitude** `A` and **exponent** `beta`.
99
+ - **Locking bandwidth** `Omega = chiN * R` -- the range of detunings
100
+ over which an oscillator follows the group.
101
+ - **Locking kernel** `W(u)` -- the time-averaged share of an
102
+ oscillator that lines up with the group, as a function of its
103
+ detuning in units of the locking bandwidth, `u = delta/Omega`.
104
+ `W(0) = 1` (an oscillator at the centre follows perfectly) and `W`
105
+ is even. Its **mass** `m` is its area, `m = integral W(u) du`. Its
106
+ **tail exponent** `s` describes how fast it falls far out:
107
+ `W ~ |u|^-s`. The package's kernels are:
108
+ - *conservative* `W = 1/(1+u^2)`, for spins with no damping
109
+ (`s = 2`, mass `pi`);
110
+ - *Kuramoto* `W = sqrt(1-u^2)` for `|u| < 1` and 0 outside, for
111
+ overdamped phase oscillators, whose motion is dominated by friction
112
+ (the classic Kuramoto model; no tail at all, mass `pi/2`);
113
+ - the test family `W = 1/(1+|u|^s)`, and a Gaussian kernel;
114
+ - a kernel averaged over a broad, power-law spread of coupling
115
+ strengths between oscillators (`heterogeneous`).
116
+ - **The rule that links them** (from the `parametric` module's
117
+ derivation, and tested below): the threshold is
118
+ `chiN_c = 1/(p(0) m)`, and the exponent is
119
+ `beta = 1/(s-1)` for `1 < s < 3` and `beta = 1/2` for `s >= 3` or
120
+ for a kernel with no algebraic tail. So a measured `beta` above 1/2
121
+ names the tail, `s = 1 + 1/beta`, while `beta = 1/2` only says
122
+ "`s >= 3`" and cannot name one value.
123
+ - **Order of the onset** -- for the conservative kernel, the sign of
124
+ one number `c` (an integral over the line shape) decides it:
125
+ `c > 0` gives a smooth (continuous) onset, `c < 0` a jump with
126
+ hysteresis (first order).
127
+ - **Cumulant dynamics**, **Wineland parameter** -- terms from the
128
+ quantum-spin half of the package; see
129
+ [What is in the package](#what-is-in-the-package).
130
+
131
+ ## Install and conventions
132
+
133
+ ```
134
+ pip install lockkernel
135
+ ```
136
+
137
+ It needs Python 3.9 or newer, NumPy 1.22 or newer, SciPy 1.8 or newer
138
+ and mpmath 1.2 or newer, and nothing else.
139
+
140
+ - **Units.** Detunings, line widths, couplings `chiN` and the locking
141
+ bandwidth `Omega` share one frequency unit of your choice (the line
142
+ width is a natural one). In the dynamics modules, time is in the
143
+ inverse of that unit (the Hamiltonian is written with hbar = 1).
144
+ - **Precision.** The theory half (`parametric`, `kernels`,
145
+ `lineshapes`) works in mpmath's arbitrary precision and returns
146
+ mpmath numbers. Set the working precision with `mpmath.mp.dps`
147
+ (decimal digits). The lab half and the dynamics work in ordinary
148
+ floating point with NumPy.
149
+ - **Line shapes** are normalised probability densities; `lorentzian`
150
+ and `gaussian` take a full width at half maximum, `box` a half
151
+ width.
152
+
153
+ ## Examples
154
+
155
+ Each example below runs as written, and the output shown is what it
156
+ printed with lockkernel 1.1.1. Line widths, couplings and noise levels
157
+ are illustrative values, not taken from any experiment.
158
+
159
+ ### 1. Threshold and exponent from the theory
160
+
161
+ ```python
162
+ import mpmath as mp
163
+ from lockkernel.lineshapes import lorentzian, gaussian
164
+ from lockkernel.kernels import conservative, kuramoto, power_tail, predicted_beta
165
+ from lockkernel.parametric import threshold, extract_beta
166
+
167
+ mp.mp.dps = 25 # working precision of mpmath, in decimal digits
168
+
169
+ line = lorentzian(fwhm=1.0)
170
+ print("threshold, conservative kernel:", mp.nstr(threshold(line, conservative()), 15))
171
+ print("threshold, Kuramoto kernel: ", mp.nstr(threshold(line, kuramoto()), 15))
172
+
173
+ # Local slopes d log R / d log eps at Omega = 1e-3 ... 1e-6 (closest to onset last).
174
+ slopes = extract_beta(gaussian(1.0), power_tail(2.5), [-3, -4, -5, -6])
175
+ print("local exponents:", [mp.nstr(b, 8) for b in slopes])
176
+ print("predicted 1/(s-1):", predicted_beta(2.5))
177
+ ```
178
+
179
+ ```
180
+ threshold, conservative kernel: 0.5
181
+ threshold, Kuramoto kernel: 1.0
182
+ local exponents: ['0.67152587', '0.6681861', '0.66714445']
183
+ predicted 1/(s-1): 0.6666666666666666
184
+ ```
185
+
186
+ The threshold is `1/(p(0) m)`: the kernel enters only through its
187
+ mass, so the Kuramoto threshold (mass `pi/2`) is twice the
188
+ conservative one (mass `pi`) on every line. `extract_beta` steps the
189
+ exact branch towards the onset and returns the local slope between
190
+ neighbouring points; the list settling down is the evidence that the
191
+ exponent has been reached. List the exponents of `Omega` so that the
192
+ one closest to the onset comes last, because the last entry is the
193
+ one to quote. Here it is within 0.1 % of `2/3`.
194
+
195
+ ### 2. The exact branch against a closed form
196
+
197
+ ```python
198
+ import mpmath as mp
199
+ from lockkernel.lineshapes import lorentzian
200
+ from lockkernel.kernels import conservative
201
+ from lockkernel.parametric import branch_point
202
+
203
+ mp.mp.dps = 30
204
+ line, kern = lorentzian(fwhm=1.0), conservative()
205
+ a = mp.mpf(1) / 2 # half width of this line
206
+ for Omega in ["1e-2", "1e-4", "1e-6"]:
207
+ chiN, R, eps = branch_point(line, kern, mp.mpf(Omega))
208
+ gap = abs(R - (1 - a / chiN))
209
+ print(f"Omega={Omega}: chiN={mp.nstr(chiN, 10)} R={mp.nstr(R, 10)} "
210
+ f"eps={mp.nstr(eps, 6)} matches 1 - a/chiN to 1e-28: {gap < 1e-28}")
211
+ ```
212
+
213
+ ```
214
+ Omega=1e-2: chiN=0.51 R=0.01960784314 eps=0.02 matches 1 - a/chiN to 1e-28: True
215
+ Omega=1e-4: chiN=0.5001 R=0.000199960008 eps=0.0002 matches 1 - a/chiN to 1e-28: True
216
+ Omega=1e-6: chiN=0.500001 R=1.999996e-6 eps=2.0e-6 matches 1 - a/chiN to 1e-28: True
217
+ ```
218
+
219
+ The branch is traced by choosing the locking bandwidth `Omega` and
220
+ reading off the coupling and the order parameter. No equation is
221
+ solved, so nothing is lost to cancellation near the threshold. For
222
+ conservative spins on a Lorentzian line the answer is known in closed
223
+ form (`chiN = a + Omega`, `R = 1 - a/chiN`), and the package matches
224
+ it to 1e-28 at 30 digits.
225
+
226
+ ### 3. Amplitude, and whether the onset is smooth
227
+
228
+ ```python
229
+ import mpmath as mp
230
+ from lockkernel.lineshapes import lorentzian, gaussian, bimodal_gaussian
231
+ from lockkernel.parametric import amplitude, c_coefficient
232
+
233
+ for line in (lorentzian(1.0), gaussian(1.0)):
234
+ print(f"{line.name:10s} A = {mp.nstr(amplitude(line), 12)}")
235
+ print("pi/2 =", mp.nstr(mp.pi / 2, 12))
236
+
237
+ # Two Gaussian peaks (width 1) at +-sep/2: the sign of c decides the order.
238
+ for sep in (1.0, 4.0):
239
+ c = c_coefficient(bimodal_gaussian(sep))
240
+ kind = "continuous onset" if c > 0 else "first order (hysteretic) onset"
241
+ print(f"sep = {sep}: c = {mp.nstr(c, 6)} -> {kind}")
242
+ ```
243
+
244
+ ```
245
+ lorentzian A = 1.0
246
+ gaussian A = 1.57079632679
247
+ pi/2 = 1.57079632679
248
+ sep = 1.0: c = 0.245044 -> continuous onset
249
+ sep = 4.0: c = -0.0891192 -> first order (hysteretic) onset
250
+ ```
251
+
252
+ For conservative spins the exponent is 1, so near the onset
253
+ `R = A eps`, with `A = pi p(0)^2 / c`. This example runs at mpmath's
254
+ default 15 digits; before version 1.1.1 `amplitude` and
255
+ `c_coefficient` needed about 30 digits to be right (see
256
+ [Corrections](#corrections-in-earlier-versions)). The two-peaked line
257
+ is not covered by the tests; the sign rule it illustrates is the one
258
+ derived in the `parametric` module.
259
+
260
+ ### 4. Fit a measured branch
261
+
262
+ ```python
263
+ import numpy as np
264
+ from lockkernel import fit_branch, kernel_tail_from_beta
265
+ from lockkernel.lineshapes import lorentzian
266
+ from lockkernel.kernels import power_tail
267
+ from lockkernel.parametric import sweep, threshold
268
+
269
+ # Stand-in "measurement": 14 points of the exact branch for a kernel with
270
+ # tail s = 2.5 (so beta = 2/3), with 2 % seeded noise on R.
271
+ line, kern = lorentzian(1.0), power_tail(2.5)
272
+ pts = sweep(line, kern, np.linspace(-5.0, -2.4, 14))
273
+ chi = np.array([float(p[0]) for p in pts])
274
+ r = np.array([float(p[1]) for p in pts])
275
+ rng = np.random.default_rng(1)
276
+ r_meas = r * np.exp(rng.normal(0.0, 0.02, r.size))
277
+
278
+ fit = fit_branch(chi, r_meas, reference="illustrative: exact branch + 2 % noise",
279
+ sigma_r=0.02 * r_meas)
280
+ print(f"threshold chi_c = {fit.chi_c:.5f} +- {fit.sigma_chi_c:.1e}"
281
+ f" (exact {float(threshold(line, kern)):.5f})")
282
+ print(f"exponent beta = {fit.beta:.3f} +- {fit.sigma_beta:.3f} (exact 0.667)")
283
+ print(f"amplitude A = {fit.amplitude:.3f} +- {fit.sigma_amplitude:.3f}")
284
+ print(f"eps range {fit.eps_range[0]:.2e} .. {fit.eps_range[1]:.2e}, "
285
+ f"rms log residual {fit.residual_rms_log:.3f}")
286
+
287
+ s, s_err = kernel_tail_from_beta(fit.beta, fit.sigma_beta)
288
+ print(f"kernel tail s = {s:.2f} +- {s_err:.2f} (true 2.5)")
289
+ ```
290
+
291
+ ```
292
+ threshold chi_c = 0.59441 +- 3.5e-09 (exact 0.59441)
293
+ exponent beta = 0.673 +- 0.003 (exact 0.667)
294
+ amplitude A = 0.652 +- 0.019
295
+ eps range 1.56e-07 .. 1.09e-03, rms log residual 0.011
296
+ kernel tail s = 2.49 +- 0.01 (true 2.5)
297
+ ```
298
+
299
+ `fit_branch` fits `R = A (chi/chi_c - 1)^beta` for the threshold,
300
+ exponent and amplitude together. With your own data, pass your
301
+ couplings and order parameters, and a `reference` that says where
302
+ they come from (it is required). The error bars are the standard
303
+ asymptotic ones of a least-squares fit: they are right when the power
304
+ law holds over the fitted range and `sigma_r` is right. Without
305
+ `sigma_r`, the scatter of the points sets them. Here the fitted
306
+ exponent is 2.4 of its own error bars from the exact 2/3; the tests
307
+ allow four (see [How the results are checked](#how-the-results-are-checked)).
308
+ `kernel_tail_from_beta` turns the exponent into the kernel's tail
309
+ exponent `s = 1 + 1/beta`, with error `sigma_beta / beta^2`.
310
+
311
+ ### 5. Plan the measurement, and a refusal
312
+
313
+ ```python
314
+ from lockkernel import beta_relative_sigma, points_for_beta, kernel_tail_from_beta
315
+
316
+ # How many points, spread evenly in log(eps) over 2 decades, with 5 %
317
+ # scatter in R, for an error bar of 0.01 on beta (threshold known)?
318
+ n, achieved = points_for_beta(0.01, decades=2.0, sigma_log=0.05)
319
+ print(f"points needed: {n} (error bar {achieved:.5f}); "
320
+ f"with {n - 1}: {beta_relative_sigma(n - 1, 2.0, 0.05):.5f}")
321
+
322
+ # A fitted beta = 0.52 +- 0.02 cannot name a kernel tail:
323
+ try:
324
+ kernel_tail_from_beta(0.52, 0.02)
325
+ except ValueError as err:
326
+ print("refused:", err)
327
+ ```
328
+
329
+ ```
330
+ points needed: 12 (error bar 0.00999); with 11: 0.01035
331
+ refused: beta = 0.52 +- 0.02 is consistent with 1/2, which identifies only the CLASS s >= 3 (compact support or decay faster than |u|^-3); no single tail exponent can be named from it
332
+ ```
333
+
334
+ `beta_relative_sigma` gives the error bar of a straight-line slope on a
335
+ log-log plot. Despite its name, the result is the absolute error of
336
+ `beta`, not a relative one. It assumes the threshold is known, while
337
+ `fit_branch` also fits the threshold, which is harder, so plan with
338
+ some margin. `points_for_beta` finds the smallest number of points
339
+ that meets the target.
340
+
341
+ ### 6. A spread of coupling strengths changes the exponent
342
+
343
+ ```python
344
+ import mpmath as mp
345
+ from lockkernel.lineshapes import gaussian
346
+ from lockkernel.kernels import kuramoto, heterogeneous, predicted_beta
347
+ from lockkernel.parametric import extract_beta
348
+
349
+ mp.mp.dps = 25
350
+ # Kuramoto oscillators whose coupling weights k follow P(k) ~ k^-3.8 (k >= 1).
351
+ ker = heterogeneous(kuramoto(), degree_exponent=3.8)
352
+ print("tail exponent s of the averaged kernel:", round(ker.tail, 12))
353
+ print("predicted beta:", round(predicted_beta(ker.tail), 12))
354
+ beta = extract_beta(gaussian(1.0), ker, [-3, -4, -5, -6])[-1]
355
+ print("beta from the branch:", mp.nstr(beta, 6))
356
+ ```
357
+
358
+ ```
359
+ tail exponent s of the averaged kernel: 1.8
360
+ predicted beta: 1.25
361
+ beta from the branch: 1.24989
362
+ ```
363
+
364
+ When oscillators couple with different strengths `k`, drawn from a
365
+ power law `P(k) ~ k^-gamma`, the kernel that decides the onset is an
366
+ average over them. A strongly coupled oscillator stays locked far out
367
+ in detuning, so the averaged kernel gets a tail even when each
368
+ oscillator's own kernel has none: `s = min(s0, (gamma-2)/eta)`, with
369
+ `s0` the tail of the single-oscillator kernel (infinite for Kuramoto).
370
+ Here `s = 1.8`, so `beta = 1/(s-1) = 1.25`, which is `1/(gamma-3)`.
371
+
372
+ ### 7. The quantum spin model against exact diagonalisation
373
+
374
+ ```python
375
+ import numpy as np
376
+ from lockkernel.cumulant import System, coherent_state_x, evolve, coherence, wineland_xi2
377
+ from lockkernel.exact import full_exact
378
+
379
+ # 8 emitters, two at each of four detunings, all starting along +x.
380
+ deltas = np.array([-1.0, -1.0, -0.3, -0.3, 0.3, 0.3, 1.0, 1.0])
381
+ chiN, times = 1.5, np.array([0.05, 0.2, 1.0])
382
+
383
+ u, count = np.unique(deltas, return_counts=True)
384
+ sysm = System(delta=u, n=count.astype(float), chiN=chiN)
385
+ states = evolve(coherent_state_x(sysm), sysm, times[-1], t_eval=times,
386
+ rtol=1e-11, atol=1e-14)
387
+ R_ex, xi_ex = full_exact(deltas, chiN, times)
388
+ for t, st, r, xi in zip(times, states, R_ex, xi_ex):
389
+ print(f"t={t:4.2f} R: cumulant {coherence(st, sysm):.6f} exact {r:.6f} "
390
+ f"xi^2: cumulant {wineland_xi2(st, sysm):.4f} exact {xi:.4f}")
391
+ ```
392
+
393
+ ```
394
+ t=0.05 R: cumulant 0.999012 exact 0.999012 xi^2: cumulant 0.9374 exact 0.9374
395
+ t=0.20 R: cumulant 0.984309 exact 0.984316 xi^2: cumulant 0.7818 exact 0.7824
396
+ t=1.00 R: cumulant 0.674467 exact 0.677579 xi^2: cumulant 0.5328 exact 0.5771
397
+ ```
398
+
399
+ The dynamics modules treat the conservative case as a quantum model:
400
+ two-level emitters (spin one-half) with equal all-to-all exchange
401
+ coupling and spread detunings, with no damping. The **cumulant**
402
+ solver follows the average of each emitter group plus the pair
403
+ correlations between emitters, and drops higher correlations. That
404
+ makes it fast for large numbers of emitters, but it is an
405
+ approximation. For small groups `full_exact` solves the full quantum
406
+ problem by exact diagonalisation (computing the quantum states of the
407
+ whole group directly, which is only affordable for a few emitters).
408
+ The two agree at short times and drift apart later, as a truncation
409
+ should. `xi^2` is the **Wineland spin-squeezing parameter**:
410
+ 1 for uncorrelated spins, below 1 when the spins are entangled in a way
411
+ that improves phase measurements.
412
+
413
+ ## What is in the package
414
+
415
+ The top level imports the `measured` names below and the submodules
416
+ `lineshapes`, `kernels`, `parametric` and `measured`. Import
417
+ `lockkernel.cumulant`, `lockkernel.ensemble` and `lockkernel.exact`
418
+ directly.
419
+
420
+ **Line shapes and kernels** (`lockkernel.lineshapes`, `lockkernel.kernels`)
421
+
422
+ - `LineShape` -- a symmetric density with its value at the centre
423
+ (`p0()`), and, where available, a floating-point `cdf` and `ppf`.
424
+ Makers: `lorentzian`, `gaussian`, `student_t`, `box`,
425
+ `bimodal_gaussian`; `LINESHAPES` maps names to them.
426
+ - `Kernel` -- an even kernel with `W(0) = 1`, its tail exponent, its
427
+ support, and `mass()`. Makers: `conservative`, `kuramoto`,
428
+ `power_tail(s)`, `gaussian_kernel`; `KERNELS` maps names to them.
429
+ - `heterogeneous(base, degree_exponent, eta=1, k_min=1)` -- the kernel
430
+ averaged over a power-law spread of coupling strengths (example 6).
431
+ - `predicted_beta(s)` -- the exponent the rule gives for tail `s`
432
+ (`None` meaning no algebraic tail).
433
+
434
+ **The exact solution** (`lockkernel.parametric`, mpmath precision)
435
+
436
+ - `G_of_Omega`, `H_of_Omega` -- the two integrals the solution is built
437
+ on: `G(Omega) = integral p(Omega u) W(u) du` and
438
+ `H = Omega G`, so that `chiN = 1/G` and `R = H`.
439
+ - `threshold`, `branch_point`, `sweep`, `extract_beta` -- the
440
+ threshold, one point `(chiN, R, eps)` of the branch, the branch at
441
+ `Omega = 10^e` for a list of `e`, and the local exponents along it.
442
+ - `c_coefficient`, `amplitude` -- for the conservative kernel, the
443
+ number `c` whose sign sets the order of the onset, and the amplitude
444
+ `A = pi p(0)^2 / c`.
445
+ - `tail_integral`, `amplitude_general` -- the amplitude for a kernel
446
+ with tail exponent `1 < s < 3`.
447
+ - `fold_interval` -- samples the branch and, if it folds back, returns
448
+ the coupling range of the hysteresis and the jump in `R`
449
+ (`None` if the branch does not fold). Not covered by the tests.
450
+
451
+ **Fitting measured data** (`lockkernel.measured`, also at the top level)
452
+
453
+ - `fit_branch(chi, r, reference, sigma_r=None, min_decade=1.0)` --
454
+ returns a `BranchFit` with `chi_c`, `beta`, `amplitude`, their
455
+ errors, `n_points`, `eps_range`, `residual_rms_log` (the
456
+ root-mean-square misfit in `log R`) and `reference`.
457
+ - `kernel_tail_from_beta(beta, sigma_beta=0, n_sigma=2)` -- the tail
458
+ exponent `s` and its error.
459
+ - `beta_relative_sigma(n_points, decades, sigma_log)`,
460
+ `points_for_beta(target_sigma_beta, decades, sigma_log)` -- the
461
+ error bar of `beta` for a planned measurement, and the number of
462
+ points for a target error bar.
463
+
464
+ **Quantum spin dynamics** (`lockkernel.cumulant`, `lockkernel.ensemble`,
465
+ `lockkernel.exact`)
466
+
467
+ - `System`, `State`, `coherent_state_x`, `evolve` -- groups ("classes")
468
+ of emitters with equal detuning, the cumulant state, the start with
469
+ all spins along +x, and the time evolution. Emitters detuned far
470
+ outside the locking range can be kept as free spins that precess
471
+ exactly without being integrated, so the line's tails are not cut.
472
+ - `evolve_meanfield` -- the same with all correlations dropped.
473
+ - `coherence` (the order parameter `R`), `wineland_xi2`,
474
+ `collective_moments` -- what is read from a state;
475
+ `physicality` and `valid_window` flag when the truncated state stops
476
+ being physical.
477
+ - `class_table`, `build_system` -- turn a line shape into a finite
478
+ set of detuning classes that keeps the whole population.
479
+ - `symmetric_exact` (all detunings equal, any number of emitters),
480
+ `full_exact` (full quantum problem, up to about twelve emitters) and
481
+ `class_exact` (emitters grouped by detuning, a few tens) -- exact
482
+ references. `class_exact` is not covered by the tests.
483
+
484
+ Each function's docstring (`help(lockkernel.fit_branch)`, for example)
485
+ gives its inputs and conventions.
486
+
487
+ ## When it refuses, and why
488
+
489
+ `lockkernel` raises an error instead of guessing when:
490
+
491
+ - measured data come without a `reference` of at least 8 characters
492
+ saying where they come from;
493
+ - fewer than 6 points are given, or `chi` and `r` differ in length
494
+ (three parameters with error bars need more);
495
+ - the couplings are not finite and strictly increasing, or an order
496
+ parameter is not finite and positive (points below the threshold,
497
+ `R = 0`, carry no exponent information; drop them);
498
+ - `sigma_r` is not positive or not on the same grid as `r`;
499
+ - the fit does not converge, or its covariance is singular (the data
500
+ cannot pin down the three parameters separately);
501
+ - the fitted threshold is indistinguishable from the smallest
502
+ coupling: the data do not reach the onset;
503
+ - the fitted points span less than `min_decade` decades of `eps`
504
+ (default one): exponent and amplitude cannot be told apart on so
505
+ short a range;
506
+ - a fitted `beta` is consistent with 1/2 (only the class `s >= 3` is
507
+ identified), clearly below 1/2 (no kernel gives that; the fit has
508
+ probably left the near-threshold range), or not positive;
509
+ - a planned measurement has fewer than 3 points, non-positive range or
510
+ scatter, a non-positive target, or would need more than 10^7 points;
511
+ - `power_tail(s)` is asked for `s <= 1` (the kernel mass would be
512
+ infinite), or `heterogeneous` for `gamma <= 2` or a resulting tail
513
+ `s <= 1`;
514
+ - `tail_integral` is asked for `s` outside `1 < s < 3`, where it
515
+ diverges;
516
+ - a line shape without a floating-point distribution
517
+ (`bimodal_gaussian`) is asked for `cdf`, `ppf` or a class table;
518
+ - the solver of the differential equations fails in `evolve` or
519
+ `evolve_meanfield`.
520
+
521
+ ## How the results are checked
522
+
523
+ 65 automated tests run on every push and pull request, on Python 3.9
524
+ to 3.14, and once more on Python 3.10 with the oldest NumPy (1.22.0),
525
+ SciPy (1.8.0) and mpmath (1.2.1) the package allows. The numerical
526
+ checks compare the package with something independent of it: a closed
527
+ form, an exact identity, a second calculation done a different way,
528
+ exact diagonalisation, or seeded simulation. The rest check that the
529
+ refusals fire, plus one check of the version number and that importing
530
+ the package does not import matplotlib. The main checks:
531
+
532
+ **The exact solution** (30 digits unless stated)
533
+
534
+ - Kernel masses: `pi` (conservative) and `pi/2` (Kuramoto) to 1e-25;
535
+ `power_tail(s)` for `s` = 1.5, 2, 3, 4 against
536
+ `2 (pi/s) / sin(pi/s)` to 1e-14.
537
+ - The Kuramoto threshold is twice the conservative one to 1e-25, on
538
+ three lines.
539
+ - Conservative kernel, Lorentzian line: the branch matches
540
+ `chiN = a + Omega`, `R = 1 - a/chiN` to 1e-28 for `Omega` from 1e-1
541
+ to 1e-6. Kuramoto kernel, Lorentzian line: `R = sqrt(1 - chiN_c/chiN)`
542
+ to 1e-26 for `Omega` from 1e-1 to 1e-4.
543
+ - The amplitude takes the closed values 1, `pi/2`, 4/3 and `pi^2/4` on
544
+ Lorentzian, Gaussian, Student-t and box lines, to a relative 1e-9,
545
+ and (new in 1.1.1) 1 and `pi/2` to a relative 1e-12 at mpmath's
546
+ default 15 digits (Lorentzian lines of width 1 and 1000, and a
547
+ Gaussian).
548
+ - `c` from its integral equals the slope of `G(Omega)/pi` at
549
+ `Omega` = 1e-6 and 2e-6 to a relative 1e-9, on four lines; `R/eps` at `Omega = 1e-6` is
550
+ within a relative 1e-5 of the amplitude.
551
+
552
+ **The exponent rule** (25 digits, Gaussian line, `Omega` down to 1e-6)
553
+
554
+ - `beta = 1/(s-1)` or 1/2 for the kernel family with `s` = 1.5, 1.8,
555
+ 2.0, 2.2, 2.5, 4 and 6, to a relative 5e-3, 1e-3, 1e-4, 1e-3, 2e-3,
556
+ 1e-4 and 1e-6 respectively. At the borderline `s = 3` the local
557
+ exponent lies between 0.5 and 0.56.
558
+ - The conservative kernel gives 1, and the Kuramoto and Gaussian
559
+ kernels give 1/2, within 1e-4 on three lines; for `s = 2.5` three
560
+ lines agree within 1e-3.
561
+ - The general amplitude matches the branch at `Omega = 1e-7` for
562
+ `s` = 1.8, 2.0 and 2.2 (relative 1e-4, 1e-5, 1e-4) on three lines,
563
+ and equals the `s = 2` form within a relative 1e-6.
564
+ - The averaged kernel of example 6 has the tail `(gamma-2)/eta` or
565
+ `s0` in five cases (within 1.5e-2 from its measured slope), and
566
+ gives `beta = 1/(gamma-3)` within 5e-3 for `gamma = 3.8`.
567
+
568
+ **Fitting measured data** (the theory half checks the lab half)
569
+
570
+ - On exact branches with no noise, `fit_branch` recovers `beta = 1`
571
+ (conservative) and `beta = 2/3` (`s = 2.5`) within 0.02, and the
572
+ threshold within a relative 1e-3; `s` comes back within 0.1 of 2.5.
573
+ - With 2 % seeded noise, the fitted `beta` is within four of its own
574
+ error bars of 1, between 0.9 and 1.1, with an error bar below 0.05.
575
+ - `kernel_tail_from_beta(1.0, 0.02)` returns `s = 2` and error 0.02 to
576
+ 1e-12.
577
+ - The planning formula matches 6000 seeded simulated fits within 5 %,
578
+ and `points_for_beta` returns the smallest `n` that meets the target
579
+ (checked on both sides).
580
+
581
+ **Dynamics and discretisation**
582
+
583
+ - Cumulant solver, all detunings equal, against the exact result:
584
+ `xi^2` within 0.2, 0.015 and 0.0015 for 50, 200 and 1000 emitters
585
+ (and `R` within a quarter of that), with the error falling with `N`
586
+ (more than 50 times smaller at 1000 than at 50).
587
+ - Cumulant solver, 8 and 10 emitters at several detunings, against
588
+ full diagonalisation: `R` within 1e-7 and `xi^2` within 1e-4 at
589
+ `t = 0.05`; at `t = 0.2` the difference is larger but below 1e-2.
590
+ - The equations keep the exact symmetries of the pair correlations to
591
+ a relative 1e-12, and an uncorrelated start gives `xi^2 = 1` and
592
+ `R = 1` to 1e-12.
593
+ - The class table keeps the whole population (sums to 1 within 1e-12)
594
+ and reproduces the locking integral within 0.2 %.
595
+ - Mean-field dynamics on a Lorentzian line settles at `R = 1 - 1/r`
596
+ within 5e-3 for couplings `r` = 1.2, 2 and 3 times the threshold,
597
+ and stays below 0.05 at 0.6 times the threshold.
598
+
599
+ Not covered by tests: `fold_interval`, `bimodal_gaussian`,
600
+ `class_exact`, `physicality` and `valid_window`.
601
+
602
+ ## Corrections in earlier versions
603
+
604
+ **1.1.1 fixed the amplitude at ordinary precision.**
605
+ `c_coefficient` (and so `amplitude`) lost about 40 digits to a
606
+ cancellation near the centre of the line. At mpmath's default 15
607
+ digits, `amplitude(lorentzian())` returned about 0.0005 instead of 1;
608
+ at 20 digits it was off by about 0.16 %; a Lorentzian 1000 wide lost
609
+ digits even at 30. The tests ran at 30 digits with a unit-width line
610
+ and did not see it. The integral now runs with 25 extra digits, and a
611
+ test at 15 digits was added. If you computed amplitudes or `c` below
612
+ 30 digits with 1.1.0 or earlier, please re-run them.
613
+
614
+ 1.1.1 also corrects this README: several tolerances it quoted were not
615
+ the ones the tests use, it described `class_exact` as tested, and the
616
+ CI did not run Python 3.10. The full history is in
617
+ [CHANGELOG.md](CHANGELOG.md).
618
+
619
+ ## Limits
620
+
621
+ - The power law `R ~ eps^beta` is the leading behaviour near the
622
+ onset. Data taken far above it bend away and bias `beta`. The
623
+ one-decade rule and the residuals guard the fit, not your choice of
624
+ range.
625
+ - The fit's error bars are the standard asymptotic ones; they are
626
+ right when the power law holds and the noise estimate is right.
627
+ - `extract_beta` converges slowly near `s = 3`, where the exponent
628
+ carries logarithmic corrections.
629
+ - In the undamped spin model the truncated cumulant equations become
630
+ unstable at long times; check `physicality` / `valid_window` before
631
+ trusting a long run. The time-averaged kernel is an assumption,
632
+ checked against the full dynamics (the mean-field tests above), not
633
+ a theorem.
634
+ - `c_coefficient` and `amplitude` are good to about 20 significant
635
+ digits at most, however high `mp.dps` is set: the integral leaves
636
+ out the first 1e-20 line widths next to the centre, a piece of
637
+ relative size about 1e-20.
638
+ - `fold_interval` is slow (each branch point is a high-precision
639
+ integral, and it takes 400 by default) and is not covered by the
640
+ tests.
641
+ - `build_system`'s docstring refers to a convergence check in
642
+ `scripts/vlasov_check.py`; that script belongs to the research
643
+ repository and is not part of this package.
644
+
645
+ ## Where it comes from
646
+
647
+ This package is the maintained distribution of the reference
648
+ implementation for the locking-kernel universality study
649
+ ([Tanvir-Mahmud-Mahim/locking-kernel-universality](https://github.com/Tanvir-Mahmud-Mahim/locking-kernel-universality),
650
+ concept DOI
651
+ [10.5281/zenodo.22696369](https://doi.org/10.5281/zenodo.22696369)),
652
+ whose scripts, archived run records and figures remain with the
653
+ study. The core modules were carried over unchanged in v1.1.0 (1.1.1
654
+ changes only `c_coefficient`); the `measured` module and the
655
+ packaging are new here. Copyright as in [NOTICE](NOTICE).
656
+
657
+ ## Citing, support and license
658
+
659
+ If `lockkernel` helps your work, please cite it with the concept DOI
660
+ [10.5281/zenodo.22829483](https://doi.org/10.5281/zenodo.22829483),
661
+ which always resolves to the latest release; every release is archived
662
+ on Zenodo. [CITATION.cff](CITATION.cff) has the details.
663
+
664
+ Written and maintained by Tanvir Mahmud Mahim (Department of
665
+ Electrical and Electronic Engineering, BRAC University), who reviews
666
+ every change and takes the final decision on scope and releases.
667
+ Design questions are discussed in the open in issues and pull
668
+ requests, and the standing rule of
669
+ [CONTRIBUTING.md](CONTRIBUTING.md) binds the maintainer exactly as it
670
+ binds contributors: a change that touches the physics arrives with a
671
+ test, and a claim arrives with its source.
672
+
673
+ Support runs through the
674
+ [issue tracker](https://github.com/TaN-MM-Org/lockkernel/issues).
675
+ Usage questions are welcome alongside bug reports; a docstring that
676
+ left a unit or a convention unclear is treated as a documentation
677
+ bug, not user error.
678
+
679
+ Licensed under Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).