lockkernel 1.1.1__tar.gz → 1.2.0__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/src/lockkernel.egg-info → lockkernel-1.2.0}/PKG-INFO +267 -31
  2. {lockkernel-1.1.1 → lockkernel-1.2.0}/README.md +266 -30
  3. {lockkernel-1.1.1 → lockkernel-1.2.0}/pyproject.toml +1 -1
  4. {lockkernel-1.1.1 → lockkernel-1.2.0}/src/lockkernel/__init__.py +5 -4
  5. {lockkernel-1.1.1 → lockkernel-1.2.0}/src/lockkernel/kernels.py +25 -0
  6. {lockkernel-1.1.1 → lockkernel-1.2.0}/src/lockkernel/measured.py +162 -10
  7. lockkernel-1.2.0/src/lockkernel/parametric.py +461 -0
  8. {lockkernel-1.1.1 → lockkernel-1.2.0/src/lockkernel.egg-info}/PKG-INFO +267 -31
  9. {lockkernel-1.1.1 → lockkernel-1.2.0}/src/lockkernel.egg-info/SOURCES.txt +1 -0
  10. {lockkernel-1.1.1 → lockkernel-1.2.0}/tests/conftest.py +1 -0
  11. lockkernel-1.2.0/tests/test_measured.py +249 -0
  12. lockkernel-1.2.0/tests/test_onset.py +276 -0
  13. lockkernel-1.1.1/src/lockkernel/parametric.py +0 -277
  14. lockkernel-1.1.1/tests/test_measured.py +0 -132
  15. {lockkernel-1.1.1 → lockkernel-1.2.0}/LICENSE +0 -0
  16. {lockkernel-1.1.1 → lockkernel-1.2.0}/MANIFEST.in +0 -0
  17. {lockkernel-1.1.1 → lockkernel-1.2.0}/NOTICE +0 -0
  18. {lockkernel-1.1.1 → lockkernel-1.2.0}/setup.cfg +0 -0
  19. {lockkernel-1.1.1 → lockkernel-1.2.0}/src/lockkernel/cumulant.py +0 -0
  20. {lockkernel-1.1.1 → lockkernel-1.2.0}/src/lockkernel/ensemble.py +0 -0
  21. {lockkernel-1.1.1 → lockkernel-1.2.0}/src/lockkernel/exact.py +0 -0
  22. {lockkernel-1.1.1 → lockkernel-1.2.0}/src/lockkernel/lineshapes.py +0 -0
  23. {lockkernel-1.1.1 → lockkernel-1.2.0}/src/lockkernel.egg-info/dependency_links.txt +0 -0
  24. {lockkernel-1.1.1 → lockkernel-1.2.0}/src/lockkernel.egg-info/requires.txt +0 -0
  25. {lockkernel-1.1.1 → lockkernel-1.2.0}/src/lockkernel.egg-info/top_level.txt +0 -0
  26. {lockkernel-1.1.1 → lockkernel-1.2.0}/tests/test_cumulant.py +0 -0
  27. {lockkernel-1.1.1 → lockkernel-1.2.0}/tests/test_meanfield.py +0 -0
  28. {lockkernel-1.1.1 → lockkernel-1.2.0}/tests/test_parametric.py +0 -0
  29. {lockkernel-1.1.1 → lockkernel-1.2.0}/tests/test_universality.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lockkernel
3
- Version: 1.1.1
3
+ Version: 1.2.0
4
4
  Summary: Locking kernel universality: exact thresholds and exponents of synchronization transitions, and fits of measured branches with error bars and refusals
5
5
  Author: Tanvir Mahmud Mahim
6
6
  License: Apache-2.0
@@ -119,7 +119,12 @@ why, rather than returning a number that looks fine but is not.
119
119
  `beta = 1/(s-1)` for `1 < s < 3` and `beta = 1/2` for `s >= 3` or
120
120
  for a kernel with no algebraic tail. So a measured `beta` above 1/2
121
121
  names the tail, `s = 1 + 1/beta`, while `beta = 1/2` only says
122
- "`s >= 3`" and cannot name one value.
122
+ "`s >= 3`" and cannot name one value. The value 1/2 needs a line
123
+ with a rounded top at its centre (`p''(0) < 0`, true for every
124
+ shipped line except `box`). On a line that is flat at the centre,
125
+ such as `box`, a tail gives `beta = 1/(s-1)` for every `s > 1`
126
+ (so below 1/2 once `s > 3`), and a kernel with no tail gives a jump
127
+ instead of a power law (see [Limits](#limits)).
123
128
  - **Order of the onset** -- for the conservative kernel, the sign of
124
129
  one number `c` (an integral over the line shape) decides it:
125
130
  `c > 0` gives a smooth (continuous) onset, `c < 0` a jump with
@@ -144,7 +149,10 @@ and mpmath 1.2 or newer, and nothing else.
144
149
  - **Precision.** The theory half (`parametric`, `kernels`,
145
150
  `lineshapes`) works in mpmath's arbitrary precision and returns
146
151
  mpmath numbers. Set the working precision with `mpmath.mp.dps`
147
- (decimal digits). The lab half and the dynamics work in ordinary
152
+ (decimal digits). The reduced coupling `eps` keeps that precision
153
+ however close to the threshold it is (since 1.2.0), so the working
154
+ precision does not need to exceed the number of decades you step
155
+ towards the onset. The lab half and the dynamics work in ordinary
148
156
  floating point with NumPy.
149
157
  - **Line shapes** are normalised probability densities; `lorentzian`
150
158
  and `gaussian` take a full width at half maximum, `box` a half
@@ -153,7 +161,7 @@ and mpmath 1.2 or newer, and nothing else.
153
161
  ## Examples
154
162
 
155
163
  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
164
+ printed with lockkernel 1.2.0. Line widths, couplings and noise levels
157
165
  are illustrative values, not taken from any experiment.
158
166
 
159
167
  ### 1. Threshold and exponent from the theory
@@ -253,9 +261,12 @@ For conservative spins the exponent is 1, so near the onset
253
261
  `R = A eps`, with `A = pi p(0)^2 / c`. This example runs at mpmath's
254
262
  default 15 digits; before version 1.1.1 `amplitude` and
255
263
  `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.
264
+ [Corrections](#corrections-in-earlier-versions)). For two unit-width
265
+ Gaussian peaks at `+-a`, `c = (1 - 2x D(x))/pi` with `x = a/sqrt(2)`
266
+ and `D` Dawson's function, and the tests hold `c_coefficient` to this
267
+ formula. So the onset turns first order when the peaks are more than
268
+ 2.61386 widths apart, where Dawson's function has its maximum.
269
+ Example 8 follows the first-order case through its hysteresis loop.
259
270
 
260
271
  ### 4. Fit a measured branch
261
272
 
@@ -289,7 +300,7 @@ print(f"kernel tail s = {s:.2f} +- {s_err:.2f} (true 2.5)")
289
300
  ```
290
301
 
291
302
  ```
292
- threshold chi_c = 0.59441 +- 3.5e-09 (exact 0.59441)
303
+ threshold chi_c = 0.59441 +- 3.8e-09 (exact 0.59441)
293
304
  exponent beta = 0.673 +- 0.003 (exact 0.667)
294
305
  amplitude A = 0.652 +- 0.019
295
306
  eps range 1.56e-07 .. 1.09e-03, rms log residual 0.011
@@ -302,7 +313,9 @@ couplings and order parameters, and a `reference` that says where
302
313
  they come from (it is required). The error bars are the standard
303
314
  asymptotic ones of a least-squares fit: they are right when the power
304
315
  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
316
+ `sigma_r`, the scatter of the points sets them. Since 1.2.0 they are
317
+ built from exact derivatives; 1.1.1 printed `3.5e-09` for the
318
+ threshold here (see [Corrections](#corrections-in-earlier-versions)). Here the fitted
306
319
  exponent is 2.4 of its own error bars from the exact 2/3; the tests
307
320
  allow four (see [How the results are checked](#how-the-results-are-checked)).
308
321
  `kernel_tail_from_beta` turns the exponent into the kernel's tail
@@ -311,7 +324,8 @@ exponent `s = 1 + 1/beta`, with error `sigma_beta / beta^2`.
311
324
  ### 5. Plan the measurement, and a refusal
312
325
 
313
326
  ```python
314
- from lockkernel import beta_relative_sigma, points_for_beta, kernel_tail_from_beta
327
+ from lockkernel import (beta_relative_sigma, points_for_beta, kernel_tail_from_beta,
328
+ plan_fit, points_for_fit)
315
329
 
316
330
  # How many points, spread evenly in log(eps) over 2 decades, with 5 %
317
331
  # scatter in R, for an error bar of 0.01 on beta (threshold known)?
@@ -319,6 +333,15 @@ n, achieved = points_for_beta(0.01, decades=2.0, sigma_log=0.05)
319
333
  print(f"points needed: {n} (error bar {achieved:.5f}); "
320
334
  f"with {n - 1}: {beta_relative_sigma(n - 1, 2.0, 0.05):.5f}")
321
335
 
336
+ # The same range (eps = 1e-4 .. 1e-2), but with the threshold fitted too,
337
+ # as fit_branch does it:
338
+ plan = plan_fit(n, 1e-4, 1e-2, beta=2/3, sigma_log=0.05)
339
+ print(f"{n} points, threshold fitted: beta +- {plan.sigma_beta:.4f}, "
340
+ f"chi_c +- {plan.sigma_chi_c_rel:.1e} (relative)")
341
+ n_fit, plan = points_for_fit(0.01, 1e-4, 1e-2, beta=2/3, sigma_log=0.05)
342
+ print(f"points needed with the threshold fitted: {n_fit} "
343
+ f"(error bar {plan.sigma_beta:.5f})")
344
+
322
345
  # A fitted beta = 0.52 +- 0.02 cannot name a kernel tail:
323
346
  try:
324
347
  kernel_tail_from_beta(0.52, 0.02)
@@ -328,15 +351,28 @@ except ValueError as err:
328
351
 
329
352
  ```
330
353
  points needed: 12 (error bar 0.00999); with 11: 0.01035
354
+ 12 points, threshold fitted: beta +- 0.0197, chi_c +- 1.4e-05 (relative)
355
+ points needed with the threshold fitted: 57 (error bar 0.00995)
331
356
  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
357
  ```
333
358
 
334
359
  `beta_relative_sigma` gives the error bar of a straight-line slope on a
335
360
  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.
361
+ `beta`, not a relative one. It assumes the threshold is known, and
362
+ `points_for_beta` finds the smallest number of points that meets the
363
+ target on that assumption.
364
+
365
+ `fit_branch` fits the threshold as well, and that costs precision:
366
+ the same 12 points give an error bar about twice as large. `plan_fit`
367
+ gives the error bars `fit_branch` will report (for `beta`, and
368
+ relative ones for `chi_c` and `A`), before any data are taken, from
369
+ the planned range of `eps` and the expected `beta`. `points_for_fit`
370
+ turns that into a number of points: 57 instead of 12 here. How much
371
+ fitting the threshold costs depends on how many decades the points
372
+ span: for 12 points the error bar grows about 2.0 times over two
373
+ decades, 1.6 over three, and 1.3 over five. These are the standard
374
+ asymptotic error bars, held in the tests against 300 seeded simulated
375
+ fits (within 12 %).
340
376
 
341
377
  ### 6. A spread of coupling strengths changes the exponent
342
378
 
@@ -369,7 +405,86 @@ oscillator's own kernel has none: `s = min(s0, (gamma-2)/eta)`, with
369
405
  `s0` the tail of the single-oscillator kernel (infinite for Kuramoto).
370
406
  Here `s = 1.8`, so `beta = 1/(s-1) = 1.25`, which is `1/(gamma-3)`.
371
407
 
372
- ### 7. The quantum spin model against exact diagonalisation
408
+ ### 7. The amplitude when beta = 1/2
409
+
410
+ ```python
411
+ import mpmath as mp
412
+ from lockkernel.lineshapes import lorentzian, gaussian
413
+ from lockkernel.kernels import kuramoto, gaussian_kernel
414
+ from lockkernel.parametric import amplitude_curvature, branch_point
415
+
416
+ mp.mp.dps = 20
417
+ for line, kern in [(lorentzian(1.0), kuramoto()), (gaussian(1.0), kuramoto()),
418
+ (gaussian(1.0), gaussian_kernel())]:
419
+ A = amplitude_curvature(line, kern)
420
+ _, R, eps = branch_point(line, kern, mp.mpf("1e-6"))
421
+ print(f"{kern.name:9s} kernel, {line.name:10s} line: A = {mp.nstr(A, 12)}, "
422
+ f"R/sqrt(eps) at Omega = 1e-6: {mp.nstr(R / mp.sqrt(eps), 12)}")
423
+ print("sqrt(pi) =", mp.nstr(mp.sqrt(mp.pi), 12))
424
+ ```
425
+
426
+ ```
427
+ kuramoto kernel, lorentzian line: A = 1.0, R/sqrt(eps) at Omega = 1e-6: 0.999999999999
428
+ kuramoto kernel, gaussian line: A = 1.77245385091, R/sqrt(eps) at Omega = 1e-6: 1.7724538509
429
+ gaussian kernel, gaussian line: A = 1.41421356237, R/sqrt(eps) at Omega = 1e-6: 1.41421356237
430
+ sqrt(pi) = 1.77245385091
431
+ ```
432
+
433
+ For a kernel with no tail, or a tail `s > 3`, the onset is
434
+ `R = A eps^(1/2)`. The first correction to the self-consistency then
435
+ comes from the curvature of the line at its centre, `p''(0)`, and
436
+ `amplitude_curvature` returns
437
+ `A = G0^(3/2) sqrt(2 / (-p''(0) M2))`, where `G0 = p(0) m` and
438
+ `M2 = integral u^2 W(u) du` is the kernel's second moment
439
+ (`Kernel.second_moment()`). For the Kuramoto kernel on a Lorentzian
440
+ line this is 1, the closed form `R = sqrt(1 - chiN_c/chiN)`. On a
441
+ Gaussian line the width cancels, which leaves `sqrt(pi)` (Kuramoto
442
+ kernel) and `sqrt(2)` (Gaussian kernel) at any width. The branch
443
+ approaches these values with corrections of relative size
444
+ `Omega^2`, or `Omega^(s-3)` for a tail `3 < s < 5`.
445
+
446
+ ### 8. A first-order onset: the hysteresis loop
447
+
448
+ ```python
449
+ import mpmath as mp
450
+ from lockkernel.lineshapes import bimodal_gaussian
451
+ from lockkernel.kernels import conservative
452
+ from lockkernel.parametric import fold_interval, threshold, extract_beta
453
+
454
+ line = bimodal_gaussian(4.0) # two Gaussian peaks (width 1) at -2 and +2
455
+ f = fold_interval(line, n=30, lo=-2, hi=1)
456
+ print("threshold :", mp.nstr(threshold(line, conservative()), 10))
457
+ print("hysteresis from chiN =", mp.nstr(f["chiN_lo"], 10), "to", mp.nstr(f["chiN_hi"], 10))
458
+ print("R jumps from 0 to :", mp.nstr(f["R_jump"], 10))
459
+ print("R where the high branch ends:", mp.nstr(f["R_high_at_lo"], 10))
460
+ try:
461
+ extract_beta(line, conservative(), [-3, -4, -5])
462
+ except ValueError as err:
463
+ print("refused:", str(err)[:72], "...")
464
+ ```
465
+
466
+ ```
467
+ threshold : 5.89561378
468
+ hysteresis from chiN = 3.493404803 to 5.89561378
469
+ R jumps from 0 to : 0.8480505911
470
+ R where the high branch ends: 0.3342759883
471
+ refused: eps = -0.0016464 <= 0 at Omega = 10^-3: the branch is at or below the th ...
472
+ ```
473
+
474
+ With the peaks 4 widths apart `c < 0` (example 3). The branch leaves
475
+ the threshold backwards, then turns round at `chiN = 3.4934`. In the
476
+ usual reading of such a fold (the package computes where the
477
+ solutions are, not whether they are stable), raising the coupling
478
+ keeps the unsynchronized state up to the threshold, where `R` jumps to
479
+ 0.848; lowering it again keeps the synchronized state down to
480
+ `chiN = 3.4934` (where `R = 0.334`) before it collapses. `fold_interval` samples the branch on `n` points
481
+ between `Omega = 10^lo` and `10^hi` and then finds the turning points
482
+ and the jump exactly. The tests check every number it returns against
483
+ an independent closed form (the Voigt profile). There is no exponent
484
+ on such a branch, and `extract_beta` says so instead of returning
485
+ one.
486
+
487
+ ### 9. The quantum spin model against exact diagonalisation
373
488
 
374
489
  ```python
375
490
  import numpy as np
@@ -424,12 +539,14 @@ directly.
424
539
  Makers: `lorentzian`, `gaussian`, `student_t`, `box`,
425
540
  `bimodal_gaussian`; `LINESHAPES` maps names to them.
426
541
  - `Kernel` -- an even kernel with `W(0) = 1`, its tail exponent, its
427
- support, and `mass()`. Makers: `conservative`, `kuramoto`,
542
+ support, `mass()` and `second_moment()` (the integral of
543
+ `u^2 W(u)`, finite only for compact support, fast decay or `s > 3`).
544
+ Makers: `conservative`, `kuramoto`,
428
545
  `power_tail(s)`, `gaussian_kernel`; `KERNELS` maps names to them.
429
546
  - `heterogeneous(base, degree_exponent, eta=1, k_min=1)` -- the kernel
430
547
  averaged over a power-law spread of coupling strengths (example 6).
431
548
  - `predicted_beta(s)` -- the exponent the rule gives for tail `s`
432
- (`None` meaning no algebraic tail).
549
+ (`None` meaning no algebraic tail), for a line with a rounded top.
433
550
 
434
551
  **The exact solution** (`lockkernel.parametric`, mpmath precision)
435
552
 
@@ -439,14 +556,18 @@ directly.
439
556
  - `threshold`, `branch_point`, `sweep`, `extract_beta` -- the
440
557
  threshold, one point `(chiN, R, eps)` of the branch, the branch at
441
558
  `Omega = 10^e` for a list of `e`, and the local exponents along it.
559
+ `eps` is computed from `G(0) - G(Omega)` directly, so it keeps the
560
+ working precision however small it is.
442
561
  - `c_coefficient`, `amplitude` -- for the conservative kernel, the
443
562
  number `c` whose sign sets the order of the onset, and the amplitude
444
563
  `A = pi p(0)^2 / c`.
445
564
  - `tail_integral`, `amplitude_general` -- the amplitude for a kernel
446
565
  with tail exponent `1 < s < 3`.
566
+ - `amplitude_curvature` -- the amplitude when `beta = 1/2` (no tail,
567
+ or a tail `s > 3`, on a line with a rounded top; example 7).
447
568
  - `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.
569
+ the coupling range of the hysteresis, the turning points and the
570
+ jump in `R` (`None` if the branch does not fold; example 8).
450
571
 
451
572
  **Fitting measured data** (`lockkernel.measured`, also at the top level)
452
573
 
@@ -458,8 +579,14 @@ directly.
458
579
  exponent `s` and its error.
459
580
  - `beta_relative_sigma(n_points, decades, sigma_log)`,
460
581
  `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.
582
+ error bar of `beta` for a planned measurement with the threshold
583
+ known, and the number of points for a target error bar.
584
+ - `plan_fit(n_points, eps_min, eps_max, beta, sigma_log,
585
+ fit_threshold=True)`, `points_for_fit(target_sigma_beta, eps_min,
586
+ eps_max, beta, sigma_log)` -- the same for the fit `fit_branch`
587
+ actually does, with the threshold fitted: `plan_fit` returns a
588
+ `FitPlan` with `sigma_beta`, `sigma_chi_c_rel` and
589
+ `sigma_amplitude_rel` (example 5).
463
590
 
464
591
  **Quantum spin dynamics** (`lockkernel.cumulant`, `lockkernel.ensemble`,
465
592
  `lockkernel.exact`)
@@ -506,8 +633,20 @@ gives its inputs and conventions.
506
633
  - a fitted `beta` is consistent with 1/2 (only the class `s >= 3` is
507
634
  identified), clearly below 1/2 (no kernel gives that; the fit has
508
635
  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;
636
+ - a planned measurement has fewer than 3 points (6 with the threshold
637
+ fitted, as `fit_branch` needs), a non-positive or reversed range, a
638
+ non-positive scatter or `beta`, a non-positive target, or would need
639
+ more than 10^7 points;
640
+ - `extract_beta` meets a point with `eps <= 0`: the branch is at or
641
+ below the threshold there, because it bends back (a first-order
642
+ onset, example 8) or is flat (`R` jumps at the threshold), and there
643
+ is no exponent to measure;
644
+ - `amplitude_curvature` is asked for a line with `p''(0) >= 0` (a flat
645
+ top or a dip at the centre) or a kernel with tail `s <= 3`, and
646
+ `second_moment` for a kernel with tail `s <= 3` (it diverges);
647
+ - `fold_interval` finds a fold on a line with compact support (`box`),
648
+ which it cannot refine, or cannot bracket a turning point on its
649
+ grid;
511
650
  - `power_tail(s)` is asked for `s <= 1` (the kernel mass would be
512
651
  infinite), or `heterogeneous` for `gamma <= 2` or a resulting tail
513
652
  `s <= 1`;
@@ -520,7 +659,7 @@ gives its inputs and conventions.
520
659
 
521
660
  ## How the results are checked
522
661
 
523
- 65 automated tests run on every push and pull request, on Python 3.9
662
+ 97 automated tests run on every push and pull request, on Python 3.9
524
663
  to 3.14, and once more on Python 3.10 with the oldest NumPy (1.22.0),
525
664
  SciPy (1.8.0) and mpmath (1.2.1) the package allows. The numerical
526
665
  checks compare the package with something independent of it: a closed
@@ -549,6 +688,38 @@ the package does not import matplotlib. The main checks:
549
688
  `Omega` = 1e-6 and 2e-6 to a relative 1e-9, on four lines; `R/eps` at `Omega = 1e-6` is
550
689
  within a relative 1e-5 of the amplitude.
551
690
 
691
+ **New in 1.2.0** (15 digits unless stated; the references are closed
692
+ forms evaluated separately from the package's integrals)
693
+
694
+ - `eps`, `chiN` and `R` on the Lorentzian line match `eps = Omega/a`,
695
+ `chiN = a + Omega`, `R = Omega/(a + Omega)` to a relative 1e-13 for
696
+ `Omega` from 1e-6 to 1e-14, and to 1e-28 at 30 digits down to
697
+ `Omega` = 1e-20.
698
+ - Box line, kernels `1/(1+|u|^s)` with `s` = 1.5, 2.5, 4 and 6: `G`
699
+ and `eps` match the closed form `G = X 2F1(1, 1/s; 1+1/s; -X^s)`,
700
+ `X = 1/Omega`, to a relative 1e-13 at `Omega` = 1e-1, 1e-3 and 1e-5
701
+ (down to `eps` of about 1e-26). On this flat-topped line
702
+ `extract_beta` gives `1/(s-1)` = 1/3 and 1/5 for `s` = 4 and 6, within
703
+ 1e-6.
704
+ - Two-peaked line: `c_coefficient` matches `(1 - 2x D(x))/pi` to 1e-13
705
+ for separations 1, 2 and 4; the sign of `c` flips across the
706
+ separation 2.61386 (the maximum of Dawson's function, located by root
707
+ finding); `G` matches the Voigt closed form to a relative 1e-13.
708
+ - `fold_interval`: every number it returns (couplings, order
709
+ parameters, `Omega` of the turning points and of the jump) matches
710
+ the Voigt closed form, with turning points found by root finding on
711
+ its derivative, to a relative 1e-12, for a branch that leaves the
712
+ threshold backwards (two peaks 4 widths apart) and for an S-shaped
713
+ branch (a three-peak line); it returns `None` for a monotonic branch.
714
+ - `extract_beta` refuses the backward branch and the flat one (box
715
+ line, Kuramoto kernel, where `eps` is exactly 0).
716
+ - `second_moment` matches `pi/8` (Kuramoto), `sqrt(pi)/2` (Gaussian
717
+ kernel) and `2 (pi/s)/sin(3 pi/s)` (`s` = 4, 6) to 1e-12.
718
+ `amplitude_curvature` is 1 for the Kuramoto kernel on a Lorentzian
719
+ line and `sqrt(pi)`, `sqrt(2)` on Gaussian lines of two widths, to
720
+ 1e-12; at 20 digits it matches `R/sqrt(eps)` at `Omega = 1e-6` to a
721
+ relative 1e-10 for three kernel-line pairs, and to 2e-6 for `s = 4`.
722
+
552
723
  **The exponent rule** (25 digits, Gaussian line, `Omega` down to 1e-6)
553
724
 
554
725
  - `beta = 1/(s-1)` or 1/2 for the kernel family with `s` = 1.5, 1.8,
@@ -577,6 +748,17 @@ the package does not import matplotlib. The main checks:
577
748
  - The planning formula matches 6000 seeded simulated fits within 5 %,
578
749
  and `points_for_beta` returns the smallest `n` that meets the target
579
750
  (checked on both sides).
751
+ - (New in 1.2.0) On noiseless data reaching `eps = 1e-7`, the error
752
+ bars `fit_branch` reports, and those `plan_fit` predicts, equal the
753
+ ones built from mpmath's numerical derivatives at 30 digits to a
754
+ relative 1e-6. With the threshold known, `plan_fit` equals
755
+ `beta_relative_sigma` to 1e-12. Over 300 seeded noisy fits (1 %
756
+ scatter), the scatter of `chi_c`, `beta` and `A` matches `plan_fit`,
757
+ and the median reported error bar matches the scatter, within 12 %.
758
+ `points_for_fit` is checked on both sides, and the error bar is
759
+ checked to fall with every added point from 6 to 400. `sigma_beta`
760
+ and `sigma_A` from `plan_fit` do not depend on `beta` and
761
+ `sigma_chi_c` scales as `1/beta` (to 1e-12).
580
762
 
581
763
  **Dynamics and discretisation**
582
764
 
@@ -596,11 +778,46 @@ the package does not import matplotlib. The main checks:
596
778
  within 5e-3 for couplings `r` = 1.2, 2 and 3 times the threshold,
597
779
  and stays below 0.05 at 0.6 times the threshold.
598
780
 
599
- Not covered by tests: `fold_interval`, `bimodal_gaussian`,
600
- `class_exact`, `physicality` and `valid_window`.
781
+ Not covered by tests: `class_exact`, `physicality` and
782
+ `valid_window`.
601
783
 
602
784
  ## Corrections in earlier versions
603
785
 
786
+ **1.2.0 fixed four silent inaccuracies.** Numbers below are at
787
+ mpmath's default 15 digits unless stated.
788
+
789
+ - `branch_point` formed `eps` as `chiN/chiN_c - 1` and lost about
790
+ `log10(1/eps)` digits: on the Lorentzian line `eps` was 2e-4
791
+ (relative) off at `Omega = 1e-9` and wrong by a factor of about 220 at
792
+ `1e-12`. `G_of_Omega` itself was 4e-10 off at `Omega = 1e-12`,
793
+ because a segment spanning many decades was integrated on a linear
794
+ scale. On the box line with `s = 4`, `extract_beta` quoted 0.3194
795
+ instead of 1/3, and with `s = 6` it raised `ZeroDivisionError`. Both
796
+ quantities now keep the working precision (see the new checks above).
797
+ The earlier tests and examples ran at 20 to 30 digits with `eps` no
798
+ smaller than about 1e-12, where the loss did not show: all of them
799
+ still pass unchanged, and README examples 1, 2, 3, 6 and 9 print the
800
+ same as before.
801
+ - `extract_beta` returned slopes close to 1 (1.0016, 1.00016, 1.000016
802
+ for `Omega` = 1e-3 down to 1e-6, at 20 digits) for the backward
803
+ branch of the two-peaked line (a first-order onset, `eps < 0`), and
804
+ raised `ZeroDivisionError` on a flat branch. It now refuses both.
805
+ - `fit_branch` took its derivatives by finite differences, which near
806
+ the threshold are not small steps: with points down to `eps = 1e-7`
807
+ it reported `sigma_chi_c` about 17 % and `sigma_beta` about 2 % too
808
+ small, and on example 4's data the fit stopped marginally short of
809
+ the least-squares minimum. It now uses exact derivatives. In example 4 the threshold
810
+ error bar goes from 3.5e-9 to 3.8e-9 (beta 0.67322 -> 0.67325, its
811
+ error bar 0.00275 -> 0.00279).
812
+ - `fold_interval`, for a branch that leaves the threshold backwards,
813
+ returned the first grid point instead of the threshold as the end of
814
+ the low branch (5.89464 instead of 5.89561 in example 8, and so a
815
+ jump to 0.84800 instead of 0.84805). A turning point it could not
816
+ refine was silently replaced by a grid point; it now raises. It is
817
+ also much faster: on the line of example 8 with `n = 60` and the
818
+ default range it took 79 s in 1.1.1 and takes 3.5 s now (17 s at the
819
+ default `n = 400`).
820
+
604
821
  **1.1.1 fixed the amplitude at ordinary precision.**
605
822
  `c_coefficient` (and so `amplitude`) lost about 40 digits to a
606
823
  cancellation near the centre of the line. At mpmath's default 15
@@ -626,6 +843,16 @@ CI did not run Python 3.10. The full history is in
626
843
  right when the power law holds and the noise estimate is right.
627
844
  - `extract_beta` converges slowly near `s = 3`, where the exponent
628
845
  carries logarithmic corrections.
846
+ - The rule `beta = 1/2` for `s >= 3` (and `predicted_beta`,
847
+ `kernel_tail_from_beta`, `amplitude_curvature`) assumes a line with a
848
+ rounded top, `p''(0) < 0`. On a line that is flat at the centre, as
849
+ `box` is, a tail gives `beta = 1/(s-1)` for every `s > 1`, and a
850
+ kernel without a tail gives a jump. A measured `beta` below 1/2 is
851
+ refused by `kernel_tail_from_beta`, although a flat-topped line can
852
+ produce one.
853
+ - `plan_fit` gives the asymptotic error bars, exact to first order in
854
+ the noise. They were checked against simulated fits at 1 % scatter;
855
+ at much larger scatter the real fit can do worse.
629
856
  - In the undamped spin model the truncated cumulant equations become
630
857
  unstable at long times; check `physicality` / `valid_window` before
631
858
  trusting a long run. The time-averaged kernel is an assumption,
@@ -635,9 +862,16 @@ CI did not run Python 3.10. The full history is in
635
862
  digits at most, however high `mp.dps` is set: the integral leaves
636
863
  out the first 1e-20 line widths next to the centre, a piece of
637
864
  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.
865
+ - `fold_interval` finds a fold only if its grid shows it: structure
866
+ below `Omega = 10^lo` or narrower than the grid spacing is missed, and
867
+ with several folds only the first and last turning points are used.
868
+ It needs a differentiable line. It costs `n` plus about 40
869
+ high-precision integrals (about 17 s at the default `n = 400` on the
870
+ two-peaked line).
871
+ - `Kernel.mass()` integrates slow tails numerically: for
872
+ `power_tail(1.5)` it is about 2e-10 (relative) off at 15 digits and
873
+ 5e-18 at 30. This enters `threshold` (not `eps`, whose integral
874
+ treats the tail separately).
641
875
  - `build_system`'s docstring refers to a convergence check in
642
876
  `scripts/vlasov_check.py`; that script belongs to the research
643
877
  repository and is not part of this package.
@@ -651,8 +885,10 @@ concept DOI
651
885
  [10.5281/zenodo.22696369](https://doi.org/10.5281/zenodo.22696369)),
652
886
  whose scripts, archived run records and figures remain with the
653
887
  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).
888
+ changes only `c_coefficient`; 1.2.0 changes how `G`, `eps` and the
889
+ fold are computed, and adds `amplitude_curvature` and
890
+ `Kernel.second_moment`); the `measured` module and the packaging are
891
+ new here. Copyright as in [NOTICE](NOTICE).
656
892
 
657
893
  ## Citing, support and license
658
894