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/measured.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Fit a MEASURED synchronization branch: threshold, exponent, kernel.
|
|
2
|
+
|
|
3
|
+
The theory half of this package computes the branch exactly from a
|
|
4
|
+
known line shape and a known locking kernel. This module is the lab
|
|
5
|
+
half: given measured (coupling, order parameter) points near an onset
|
|
6
|
+
-- from an experiment or from a simulation whose kernel is NOT known
|
|
7
|
+
-- it fits the near-threshold law
|
|
8
|
+
|
|
9
|
+
R = A (chiN / chiN_c - 1)^beta, chiN > chiN_c,
|
|
10
|
+
|
|
11
|
+
for the threshold chiN_c, the exponent beta and the amplitude A
|
|
12
|
+
together, with error bars, and translates beta back into the locking
|
|
13
|
+
kernel's tail exponent through the exact dictionary of
|
|
14
|
+
`lockkernel.parametric`:
|
|
15
|
+
|
|
16
|
+
beta = 1/(s-1) for 1 < s < 3, beta = 1/2 for s >= 3,
|
|
17
|
+
|
|
18
|
+
so s = 1 + 1/beta wherever beta > 1/2, while beta consistent with 1/2
|
|
19
|
+
identifies only the CLASS s >= 3 (compact support or fast decay) --
|
|
20
|
+
`kernel_tail_from_beta` refuses to name a single s there, because the
|
|
21
|
+
data cannot single one out.
|
|
22
|
+
|
|
23
|
+
House rules, as everywhere in this organization: measured data carry
|
|
24
|
+
a mandatory `reference` (instrument or simulation provenance), the
|
|
25
|
+
fit REFUSES -- with the reason -- rather than returning numbers it
|
|
26
|
+
cannot defend (too few points, couplings not increasing, less than a
|
|
27
|
+
decade of reduced coupling, a threshold estimate that collides with
|
|
28
|
+
the data, a fit that does not converge), and every closed form in
|
|
29
|
+
this module is held in the tests against the exact parametric branch
|
|
30
|
+
of `lockkernel.parametric` and against seeded Monte Carlo -- the
|
|
31
|
+
theory half checks the lab half.
|
|
32
|
+
|
|
33
|
+
`points_for_beta` plans the measurement before it is taken: the
|
|
34
|
+
closed-form error bar of a log-log slope over a given range of
|
|
35
|
+
reduced coupling, inverted exactly for the number of points a target
|
|
36
|
+
error bar costs.
|
|
37
|
+
|
|
38
|
+
Honest limits: the power law is the LEADING near-threshold behavior;
|
|
39
|
+
data taken far above threshold bend away from it and bias beta -- fit
|
|
40
|
+
what is near the onset (the eps_max refusal guards the worst of it,
|
|
41
|
+
and the residual diagnostics are returned so the bend is visible).
|
|
42
|
+
Error bars are the standard asymptotic (J^T J)^-1 covariance of the
|
|
43
|
+
weighted fit: honest when the model holds and the noise estimate is
|
|
44
|
+
right, and stated as such.
|
|
45
|
+
"""
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
from dataclasses import dataclass
|
|
49
|
+
|
|
50
|
+
import numpy as np
|
|
51
|
+
from scipy.optimize import least_squares
|
|
52
|
+
|
|
53
|
+
__all__ = ["BranchFit", "fit_branch", "kernel_tail_from_beta",
|
|
54
|
+
"beta_relative_sigma", "points_for_beta"]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class BranchFit:
|
|
59
|
+
"""The fitted near-threshold law, with provenance."""
|
|
60
|
+
chi_c: float
|
|
61
|
+
beta: float
|
|
62
|
+
amplitude: float
|
|
63
|
+
sigma_chi_c: float
|
|
64
|
+
sigma_beta: float
|
|
65
|
+
sigma_amplitude: float
|
|
66
|
+
n_points: int
|
|
67
|
+
eps_range: tuple
|
|
68
|
+
residual_rms_log: float
|
|
69
|
+
reference: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _check_reference(reference):
|
|
73
|
+
if not isinstance(reference, str) or len(reference.strip()) < 8:
|
|
74
|
+
raise ValueError("measured branch data require a real "
|
|
75
|
+
"reference (instrument or simulation "
|
|
76
|
+
"provenance, >= 8 characters); this package "
|
|
77
|
+
"does not fit uncited data")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def fit_branch(chi, r, reference, sigma_r=None, min_decade=1.0):
|
|
81
|
+
"""Fit R = A (chi/chi_c - 1)^beta to measured points.
|
|
82
|
+
|
|
83
|
+
chi : couplings chiN, strictly increasing, all above threshold.
|
|
84
|
+
r : measured order parameters at those couplings, all > 0.
|
|
85
|
+
reference : where the data come from (mandatory, >= 8 chars).
|
|
86
|
+
sigma_r : absolute 1-sigma errors of r (same shape); when given,
|
|
87
|
+
the covariance is exact for that noise; when omitted, the
|
|
88
|
+
residual scatter sets the scale and the error bars say so by
|
|
89
|
+
construction (residual-scaled).
|
|
90
|
+
min_decade : the minimum decades of reduced coupling
|
|
91
|
+
eps = chi/chi_c - 1 the fitted points must span (default one
|
|
92
|
+
decade); a narrower fit cannot separate the exponent from the
|
|
93
|
+
amplitude and is refused.
|
|
94
|
+
|
|
95
|
+
Returns a `BranchFit`. Refusals: fewer than 6 points, couplings
|
|
96
|
+
not strictly increasing, non-positive R, a converged threshold
|
|
97
|
+
indistinguishable from the smallest coupling (the data do not
|
|
98
|
+
reach below the bend), a span of eps under `min_decade` decades,
|
|
99
|
+
or a fit that does not converge.
|
|
100
|
+
"""
|
|
101
|
+
_check_reference(reference)
|
|
102
|
+
x = np.asarray(chi, dtype=float)
|
|
103
|
+
y = np.asarray(r, dtype=float)
|
|
104
|
+
if x.size < 6 or x.size != y.size:
|
|
105
|
+
raise ValueError("need >= 6 (chi, R) points of equal length: "
|
|
106
|
+
"three parameters and error bars cannot come "
|
|
107
|
+
"from fewer")
|
|
108
|
+
if np.any(~np.isfinite(x)) or np.any(np.diff(x) <= 0.0):
|
|
109
|
+
raise ValueError("couplings must be finite and strictly "
|
|
110
|
+
"increasing")
|
|
111
|
+
if np.any(~np.isfinite(y)) or np.any(y <= 0.0):
|
|
112
|
+
raise ValueError("order parameters must be finite and > 0: "
|
|
113
|
+
"points below threshold (R = 0) carry no "
|
|
114
|
+
"exponent information in this law; drop them")
|
|
115
|
+
if sigma_r is not None:
|
|
116
|
+
sy = np.asarray(sigma_r, dtype=float)
|
|
117
|
+
if sy.shape != y.shape or np.any(sy <= 0.0):
|
|
118
|
+
raise ValueError("sigma_r must be positive on the same "
|
|
119
|
+
"grid as r")
|
|
120
|
+
w_log = sy / y # d(log R) = dR / R
|
|
121
|
+
else:
|
|
122
|
+
w_log = np.ones_like(y)
|
|
123
|
+
|
|
124
|
+
ly = np.log(y)
|
|
125
|
+
chi_min = x[0]
|
|
126
|
+
|
|
127
|
+
def resid(p):
|
|
128
|
+
chi_c, beta, la = p
|
|
129
|
+
eps = x / chi_c - 1.0
|
|
130
|
+
return (la + beta * np.log(eps) - ly) / w_log
|
|
131
|
+
|
|
132
|
+
# start: threshold a little under the first point, slope from the
|
|
133
|
+
# outermost pair
|
|
134
|
+
eps0 = x / (0.95 * chi_min) - 1.0
|
|
135
|
+
b0 = (ly[-1] - ly[0]) / np.log(eps0[-1] / eps0[0])
|
|
136
|
+
b0 = float(np.clip(b0, 0.1, 10.0))
|
|
137
|
+
p0 = np.array([0.95 * chi_min, b0,
|
|
138
|
+
ly[-1] - b0 * np.log(eps0[-1])])
|
|
139
|
+
sol = least_squares(resid, p0,
|
|
140
|
+
bounds=([1e-300, 0.05, -np.inf],
|
|
141
|
+
[chi_min * (1 - 1e-9), 20.0, np.inf]))
|
|
142
|
+
if not sol.success:
|
|
143
|
+
raise ValueError(f"the branch fit did not converge: "
|
|
144
|
+
f"{sol.message}")
|
|
145
|
+
chi_c, beta, la = sol.x
|
|
146
|
+
if chi_c >= chi_min * (1.0 - 1e-8):
|
|
147
|
+
raise ValueError(
|
|
148
|
+
"the fitted threshold is indistinguishable from the "
|
|
149
|
+
"smallest measured coupling: the data do not resolve the "
|
|
150
|
+
"onset. Measure closer to threshold (smaller R), or "
|
|
151
|
+
"supply points at lower coupling")
|
|
152
|
+
eps = x / chi_c - 1.0
|
|
153
|
+
decades = np.log10(eps[-1] / eps[0])
|
|
154
|
+
if decades < float(min_decade):
|
|
155
|
+
raise ValueError(
|
|
156
|
+
f"the fitted points span only {decades:.2f} decades of "
|
|
157
|
+
f"reduced coupling eps (minimum {float(min_decade):.2f}): "
|
|
158
|
+
"exponent and amplitude are not separable on so short a "
|
|
159
|
+
"lever arm. Extend the coupling range")
|
|
160
|
+
|
|
161
|
+
# asymptotic covariance of the weighted fit
|
|
162
|
+
jac = sol.jac
|
|
163
|
+
jtj = jac.T @ jac
|
|
164
|
+
try:
|
|
165
|
+
cov = np.linalg.inv(jtj)
|
|
166
|
+
except np.linalg.LinAlgError:
|
|
167
|
+
raise ValueError("the fit covariance is singular: the data "
|
|
168
|
+
"do not constrain all three parameters "
|
|
169
|
+
"independently")
|
|
170
|
+
dof = x.size - 3
|
|
171
|
+
res = sol.fun
|
|
172
|
+
if sigma_r is None and dof > 0:
|
|
173
|
+
cov = cov * float(res @ res) / dof # residual-scaled
|
|
174
|
+
sig = np.sqrt(np.diag(cov))
|
|
175
|
+
return BranchFit(
|
|
176
|
+
chi_c=float(chi_c), beta=float(beta),
|
|
177
|
+
amplitude=float(np.exp(la)),
|
|
178
|
+
sigma_chi_c=float(sig[0]), sigma_beta=float(sig[1]),
|
|
179
|
+
sigma_amplitude=float(np.exp(la) * sig[2]),
|
|
180
|
+
n_points=int(x.size),
|
|
181
|
+
eps_range=(float(eps[0]), float(eps[-1])),
|
|
182
|
+
residual_rms_log=float(np.sqrt(np.mean((res * w_log) ** 2))),
|
|
183
|
+
reference=reference)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def kernel_tail_from_beta(beta, sigma_beta=0.0, n_sigma=2.0):
|
|
187
|
+
"""The kernel tail exponent s from a measured beta: s = 1 + 1/beta.
|
|
188
|
+
|
|
189
|
+
Error propagation: sigma_s = sigma_beta / beta^2.
|
|
190
|
+
|
|
191
|
+
Refuses to name a single s when beta is consistent with 1/2
|
|
192
|
+
within `n_sigma` error bars: every kernel with s >= 3 (compact
|
|
193
|
+
support, Gaussian, any decay faster than |u|^-3) produces
|
|
194
|
+
beta = 1/2, so the measurement identifies only the class. Also
|
|
195
|
+
refuses beta significantly BELOW 1/2, which no locking kernel in
|
|
196
|
+
this dictionary produces -- that is a sign the fitted range left
|
|
197
|
+
the near-threshold regime.
|
|
198
|
+
"""
|
|
199
|
+
b = float(beta)
|
|
200
|
+
sb = abs(float(sigma_beta))
|
|
201
|
+
if not (np.isfinite(b) and b > 0.0):
|
|
202
|
+
raise ValueError("beta must be finite and positive")
|
|
203
|
+
if b + n_sigma * sb < 0.5:
|
|
204
|
+
raise ValueError(
|
|
205
|
+
f"beta = {b:.4g} +- {sb:.4g} lies significantly below "
|
|
206
|
+
"1/2, which no locking kernel produces in the dictionary "
|
|
207
|
+
"beta = 1/(s-1) (1 < s < 3), 1/2 (s >= 3). The fitted "
|
|
208
|
+
"range has likely left the near-threshold regime; refit "
|
|
209
|
+
"closer to the onset")
|
|
210
|
+
if b - n_sigma * sb <= 0.5:
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"beta = {b:.4g} +- {sb:.4g} is consistent with 1/2, "
|
|
213
|
+
"which identifies only the CLASS s >= 3 (compact support "
|
|
214
|
+
"or decay faster than |u|^-3); no single tail exponent "
|
|
215
|
+
"can be named from it")
|
|
216
|
+
s = 1.0 + 1.0 / b
|
|
217
|
+
if s <= 1.0 or s >= 3.0:
|
|
218
|
+
raise ValueError(f"implied s = {s:.4g} outside the algebraic "
|
|
219
|
+
"window 1 < s < 3")
|
|
220
|
+
return s, sb / b ** 2
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def beta_relative_sigma(n_points, decades, sigma_log):
|
|
224
|
+
"""Closed-form 1-sigma error of a log-log slope.
|
|
225
|
+
|
|
226
|
+
n_points : measured couplings, log-uniform over the eps range.
|
|
227
|
+
decades : decades of reduced coupling spanned.
|
|
228
|
+
sigma_log : per-point scatter of log R (for small relative noise,
|
|
229
|
+
sigma_log ~ sigma_R / R).
|
|
230
|
+
|
|
231
|
+
For n equally spaced abscissas spanning L = decades * ln 10,
|
|
232
|
+
|
|
233
|
+
sigma_beta = sigma_log / sqrt( L^2 n (n+1) / (12 (n-1)) ),
|
|
234
|
+
|
|
235
|
+
the exact variance of the least-squares slope; the tests hold it
|
|
236
|
+
against seeded simulation. This is the KNOWN-threshold error --
|
|
237
|
+
fitting chi_c together with beta (as `fit_branch` does) is
|
|
238
|
+
strictly harder, so plan with margin.
|
|
239
|
+
"""
|
|
240
|
+
n = int(n_points)
|
|
241
|
+
if n < 3:
|
|
242
|
+
raise ValueError("need >= 3 points for a slope with an error "
|
|
243
|
+
"bar")
|
|
244
|
+
d = float(decades)
|
|
245
|
+
s = float(sigma_log)
|
|
246
|
+
if not (d > 0.0 and s > 0.0):
|
|
247
|
+
raise ValueError("decades and sigma_log must be positive")
|
|
248
|
+
ell = d * np.log(10.0)
|
|
249
|
+
sxx = ell ** 2 * n * (n + 1) / (12.0 * (n - 1))
|
|
250
|
+
return s / np.sqrt(sxx)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def points_for_beta(target_sigma_beta, decades, sigma_log):
|
|
254
|
+
"""Points needed for a target error bar on beta.
|
|
255
|
+
|
|
256
|
+
Exact inversion of `beta_relative_sigma`: returns (n, achieved)
|
|
257
|
+
with achieved <= target and n-1 failing it (asserted two-sided in
|
|
258
|
+
the tests).
|
|
259
|
+
"""
|
|
260
|
+
tgt = float(target_sigma_beta)
|
|
261
|
+
if not (np.isfinite(tgt) and tgt > 0.0):
|
|
262
|
+
raise ValueError("target_sigma_beta must be positive")
|
|
263
|
+
n = 3
|
|
264
|
+
while beta_relative_sigma(n, decades, sigma_log) > tgt:
|
|
265
|
+
n += 1
|
|
266
|
+
if n > 10 ** 7:
|
|
267
|
+
raise ValueError(
|
|
268
|
+
"over 10^7 points would be needed: the target is out "
|
|
269
|
+
"of reach at this noise and range; widen the range "
|
|
270
|
+
"or reduce the noise")
|
|
271
|
+
return n, beta_relative_sigma(n, decades, sigma_log)
|
lockkernel/parametric.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""The exact parametric solution of the locking self consistency.
|
|
2
|
+
|
|
3
|
+
THE SELF CONSISTENCY. Write R for the order parameter, chiN for the coupling
|
|
4
|
+
per spin times the spin number, and Omega = chiN R for the locking bandwidth,
|
|
5
|
+
the width in detuning over which an oscillator follows the mean field. With a
|
|
6
|
+
locking kernel W the stationary condition is
|
|
7
|
+
|
|
8
|
+
R = int p(delta) W(delta / Omega) d delta.
|
|
9
|
+
|
|
10
|
+
THE RECASTING. Substituting delta = Omega u turns the right hand side into a
|
|
11
|
+
function of Omega alone,
|
|
12
|
+
|
|
13
|
+
H(Omega) = Omega int p(Omega u) W(u) du,
|
|
14
|
+
|
|
15
|
+
so that R = H(Omega), and since R = Omega / chiN,
|
|
16
|
+
|
|
17
|
+
chiN(Omega) = Omega / H(Omega), R(Omega) = H(Omega).
|
|
18
|
+
|
|
19
|
+
This is an EXACT PARAMETRIC SOLUTION of the transition for any line shape and
|
|
20
|
+
any kernel. Sweeping Omega upward from zero traces the branch, with no root
|
|
21
|
+
finding and no cancellation near threshold, which is what makes high precision
|
|
22
|
+
exponent extraction possible. Letting Omega go to zero gives the threshold
|
|
23
|
+
|
|
24
|
+
chiN_c = 1 / (p(0) m), m = int W(u) du,
|
|
25
|
+
|
|
26
|
+
for every kernel: the kernel enters the threshold only through its mass.
|
|
27
|
+
|
|
28
|
+
THE MECHANISM. Everything about the onset is contained in how H leaves its
|
|
29
|
+
linear behaviour. Define
|
|
30
|
+
|
|
31
|
+
G(Omega) = H(Omega) / Omega = int p(Omega u) W(u) du,
|
|
32
|
+
|
|
33
|
+
so G(0) = p(0) m. If the kernel has an algebraic tail W ~ |u|^-s with
|
|
34
|
+
1 < s < 3, the integral samples the whole line and
|
|
35
|
+
|
|
36
|
+
G(0) - G(Omega) ~ const * Omega^(s-1),
|
|
37
|
+
|
|
38
|
+
because the tail of the kernel meets the tail of the line. If instead the
|
|
39
|
+
kernel has compact support, or decays faster than any power, the leading
|
|
40
|
+
correction comes from the local curvature of the line and is quadratic,
|
|
41
|
+
|
|
42
|
+
G(0) - G(Omega) = -(1/2) p''(0) M_2 Omega^2 + O(Omega^4),
|
|
43
|
+
M_2 = int u^2 W(u) du.
|
|
44
|
+
|
|
45
|
+
Since eps = chiN/chiN_c - 1 = G(0)/G(Omega) - 1 and R = Omega G(Omega), the
|
|
46
|
+
order parameter follows eps to the power beta with
|
|
47
|
+
|
|
48
|
+
beta = 1/(s-1) for 1 < s < 3, beta = 1/2 for s >= 3.
|
|
49
|
+
|
|
50
|
+
The crossover at s = 3 is exactly where the second moment M_2 of the kernel
|
|
51
|
+
stops converging, so that the tail contribution and the curvature contribution
|
|
52
|
+
exchange dominance.
|
|
53
|
+
|
|
54
|
+
THE CONSERVATIVE CASE. For conservative spins W(u) = 1/(1+u^2), whence
|
|
55
|
+
s = 2, m = pi and beta = 1. Then H is a Lorentzian smoothing of the line,
|
|
56
|
+
|
|
57
|
+
H(Omega) = pi Omega (p * L_Omega)(0),
|
|
58
|
+
|
|
59
|
+
with L_Omega the normalised Lorentzian of half width Omega, and the linear
|
|
60
|
+
coefficient can be written in closed form,
|
|
61
|
+
|
|
62
|
+
G(Omega) = pi [p(0) - c Omega + o(Omega)],
|
|
63
|
+
c = -(1/pi) int [p(delta) - p(0)] / delta^2 d delta,
|
|
64
|
+
|
|
65
|
+
giving R = A eps with A = pi p(0)^2 / c. The SIGN of c decides the order of
|
|
66
|
+
the transition: c > 0 gives a monotonic branch and a continuous onset, c < 0
|
|
67
|
+
gives a fold in the branch and therefore a hysteretic, first order onset.
|
|
68
|
+
"""
|
|
69
|
+
from __future__ import annotations
|
|
70
|
+
|
|
71
|
+
import mpmath as mp
|
|
72
|
+
|
|
73
|
+
from .kernels import Kernel, conservative
|
|
74
|
+
from .lineshapes import LineShape
|
|
75
|
+
|
|
76
|
+
__all__ = ["G_of_Omega", "H_of_Omega", "branch_point", "threshold",
|
|
77
|
+
"c_coefficient", "amplitude", "sweep", "extract_beta",
|
|
78
|
+
"fold_interval"]
|
|
79
|
+
|
|
80
|
+
INF = mp.inf
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _breakpoints(line: LineShape, kernel: Kernel, Omega):
|
|
84
|
+
"""Quadrature break points in u for int p(Omega u) W(u) du.
|
|
85
|
+
|
|
86
|
+
The integrand has structure on two scales: the kernel varies on u ~ 1 and
|
|
87
|
+
the line varies on u ~ scale/Omega. Both are given to the quadrature.
|
|
88
|
+
"""
|
|
89
|
+
s = mp.mpf(line.scale) / Omega
|
|
90
|
+
if kernel.support is not None:
|
|
91
|
+
cut = mp.mpf(kernel.support)
|
|
92
|
+
pts = sorted({mp.mpf(0), min(s, cut), cut})
|
|
93
|
+
return [-p for p in reversed(pts)] + list(pts)[1:]
|
|
94
|
+
pts = sorted({mp.mpf(0), mp.mpf(1), s, 10 * s, mp.mpf(10)})
|
|
95
|
+
return [-INF] + [-p for p in reversed(pts) if p > 0] + list(pts) + [INF]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def G_of_Omega(line: LineShape, kernel: Kernel, Omega) -> mp.mpf:
|
|
99
|
+
"""G(Omega) = int p(Omega u) W(u) du. Finite and positive at Omega = 0."""
|
|
100
|
+
Omega = mp.mpf(Omega)
|
|
101
|
+
if Omega == 0:
|
|
102
|
+
return line.p0() * kernel.mass()
|
|
103
|
+
f = lambda u: line.pdf(Omega * u) * kernel.W(u)
|
|
104
|
+
return mp.quad(f, _breakpoints(line, kernel, Omega))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def H_of_Omega(line: LineShape, kernel: Kernel, Omega) -> mp.mpf:
|
|
108
|
+
"""H(Omega) = Omega G(Omega). Equals the order parameter on the branch."""
|
|
109
|
+
return mp.mpf(Omega) * G_of_Omega(line, kernel, Omega)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def threshold(line: LineShape, kernel: Kernel) -> mp.mpf:
|
|
113
|
+
"""chiN_c = 1 / (p(0) m). The kernel enters only through its mass."""
|
|
114
|
+
return 1 / (line.p0() * kernel.mass())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def branch_point(line: LineShape, kernel: Kernel, Omega):
|
|
118
|
+
"""One point of the branch: (chiN, R, eps) at locking bandwidth Omega."""
|
|
119
|
+
Omega = mp.mpf(Omega)
|
|
120
|
+
G = G_of_Omega(line, kernel, Omega)
|
|
121
|
+
chiN = 1 / G
|
|
122
|
+
R = Omega * G
|
|
123
|
+
eps = chiN / threshold(line, kernel) - 1
|
|
124
|
+
return chiN, R, eps
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def sweep(line: LineShape, kernel: Kernel, exponents):
|
|
128
|
+
"""The branch at Omega = 10^e for each e in `exponents`."""
|
|
129
|
+
return [branch_point(line, kernel, mp.mpf(10) ** mp.mpf(e)) for e in exponents]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def extract_beta(line: LineShape, kernel: Kernel, exponents):
|
|
133
|
+
"""Successive local slopes d log R / d log eps along the branch.
|
|
134
|
+
|
|
135
|
+
Returned as a list with one fewer entry than `exponents`. Convergence of
|
|
136
|
+
the list is the evidence that the exponent has been reached; the last
|
|
137
|
+
entry is quoted as the measured exponent.
|
|
138
|
+
"""
|
|
139
|
+
pts = sweep(line, kernel, exponents)
|
|
140
|
+
out = []
|
|
141
|
+
for i in range(len(pts) - 1):
|
|
142
|
+
e0, r0 = pts[i][2], pts[i][1]
|
|
143
|
+
e1, r1 = pts[i + 1][2], pts[i + 1][1]
|
|
144
|
+
out.append(mp.log(r1 / r0) / mp.log(e1 / e0))
|
|
145
|
+
return out
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def c_coefficient(line: LineShape) -> mp.mpf:
|
|
149
|
+
"""c = -(1/pi) int [p(delta) - p(0)] / delta^2 d delta.
|
|
150
|
+
|
|
151
|
+
Defined for the conservative kernel only, where it is both the linear
|
|
152
|
+
coefficient of the smoothed peak and the criterion for the order of the
|
|
153
|
+
transition. The integrand has a REMOVABLE singularity at the origin, its
|
|
154
|
+
limit being p''(0)/2, so the quadrature must not evaluate it there; the
|
|
155
|
+
range is split away from zero and the integrand is even.
|
|
156
|
+
"""
|
|
157
|
+
p0 = line.p0()
|
|
158
|
+
sc = mp.mpf(line.scale)
|
|
159
|
+
|
|
160
|
+
def f(d):
|
|
161
|
+
if d == 0:
|
|
162
|
+
return mp.diff(line.pdf, mp.mpf(0), 2) / 2
|
|
163
|
+
return (line.pdf(d) - p0) / d ** 2
|
|
164
|
+
|
|
165
|
+
val = 2 * mp.quad(f, [mp.mpf(10) ** -20, sc * mp.mpf("1e-3"), sc,
|
|
166
|
+
10 * sc, 100 * sc, INF])
|
|
167
|
+
return -val / mp.pi
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def amplitude(line: LineShape) -> mp.mpf:
|
|
171
|
+
"""A = pi p(0)^2 / c, the slope of R against eps for conservative spins."""
|
|
172
|
+
return mp.pi * line.p0() ** 2 / c_coefficient(line)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def tail_integral(line: LineShape, s) -> mp.mpf:
|
|
176
|
+
"""I_s = int [p(0) - p(delta)] |delta|^-s d delta.
|
|
177
|
+
|
|
178
|
+
This is the object that carries the whole onset for a kernel of tail
|
|
179
|
+
exponent s, and the regime 1 < s < 3 of the exponent formula is exactly
|
|
180
|
+
the regime in which it converges: at s = 3 it diverges at the origin, so
|
|
181
|
+
the local curvature takes over and the exponent returns to 1/2, and at
|
|
182
|
+
s = 1 it diverges at infinity, together with the mass of the kernel, so
|
|
183
|
+
the threshold collapses. At s = 2 it equals pi times the coefficient c.
|
|
184
|
+
|
|
185
|
+
Integrating by parts,
|
|
186
|
+
|
|
187
|
+
I_s = (2 / (s-1)) int_0^inf delta^(1-s) [-p'(delta)] d delta,
|
|
188
|
+
|
|
189
|
+
which is the form used here: it removes the subtraction of two nearly
|
|
190
|
+
equal numbers near the origin, where p(0) - p(delta) is a cancellation of
|
|
191
|
+
order delta^2 that costs as many digits as the quadrature comes close.
|
|
192
|
+
"""
|
|
193
|
+
s = mp.mpf(s)
|
|
194
|
+
if s <= 1 or s >= 3:
|
|
195
|
+
raise ValueError("I_s converges only for 1 < s < 3")
|
|
196
|
+
sc = mp.mpf(line.scale)
|
|
197
|
+
f = lambda d: d ** (1 - s) * (-mp.diff(line.pdf, d))
|
|
198
|
+
return (2 / (s - 1)) * (mp.quad(f, [0, sc])
|
|
199
|
+
+ mp.quad(f, [sc, 10 * sc, 100 * sc, INF]))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def amplitude_general(line: LineShape, kernel: Kernel, C=None) -> mp.mpf:
|
|
203
|
+
"""A_s in R = A_s eps^(1/(s-1)), for a kernel of tail W ~ C |u|^-s.
|
|
204
|
+
|
|
205
|
+
A_s = p(0) m [ p(0) m / (C I_s) ]^(1/(s-1)),
|
|
206
|
+
|
|
207
|
+
with m the kernel mass and I_s the integral above. It reduces to
|
|
208
|
+
pi p(0)^2 / c at s = 2, where the exponent is one and the branch is
|
|
209
|
+
linear. `C` defaults to the tail amplitude measured from the kernel
|
|
210
|
+
itself, which is one for the family 1/(1+|u|^s).
|
|
211
|
+
"""
|
|
212
|
+
s = mp.mpf(kernel.tail)
|
|
213
|
+
m = kernel.mass()
|
|
214
|
+
if C is None:
|
|
215
|
+
v = mp.mpf(10) ** 6
|
|
216
|
+
C = kernel.W(v) * v ** s
|
|
217
|
+
p0m = line.p0() * m
|
|
218
|
+
return p0m * (p0m / (mp.mpf(C) * tail_integral(line, s))) ** (1 / (s - 1))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def fold_interval(line: LineShape, kernel: Kernel = None, n: int = 400,
|
|
222
|
+
lo: float = -4.0, hi: float = 1.5):
|
|
223
|
+
"""Locate a fold in the branch, if there is one.
|
|
224
|
+
|
|
225
|
+
The branch chiN(Omega) is sampled on a logarithmic grid. Where it is not
|
|
226
|
+
monotonic there are two turning points, and between the couplings they
|
|
227
|
+
define the stationary condition has three solutions, so the onset is
|
|
228
|
+
hysteretic. Each turning point is refined by solving d chiN / d Omega = 0
|
|
229
|
+
rather than being read off the grid, and the size of the jump is measured
|
|
230
|
+
at the upper turning point, where the low branch ceases to exist and the
|
|
231
|
+
order parameter must move to the highest solution at the same coupling.
|
|
232
|
+
|
|
233
|
+
Returns None when the branch is monotonic.
|
|
234
|
+
"""
|
|
235
|
+
kernel = kernel or conservative()
|
|
236
|
+
Om = [mp.mpf(10) ** (lo + (hi - lo) * i / (n - 1)) for i in range(n)]
|
|
237
|
+
chi = [branch_point(line, kernel, o)[0] for o in Om]
|
|
238
|
+
down = [i for i in range(len(chi) - 1) if chi[i + 1] < chi[i]]
|
|
239
|
+
if not down:
|
|
240
|
+
return None
|
|
241
|
+
i0, i1 = down[0], down[-1] + 1
|
|
242
|
+
|
|
243
|
+
dchi = lambda o: mp.diff(lambda x: branch_point(line, kernel, x)[0], o)
|
|
244
|
+
def refine(ia, ib):
|
|
245
|
+
try:
|
|
246
|
+
return mp.findroot(dchi, (Om[ia], Om[ib]), solver="bisect",
|
|
247
|
+
tol=mp.mpf("1e-16"))
|
|
248
|
+
except Exception:
|
|
249
|
+
return Om[ia]
|
|
250
|
+
|
|
251
|
+
O_hi = refine(max(i0 - 1, 0), min(i0 + 1, n - 1)) # end of the low branch
|
|
252
|
+
O_lo = refine(max(i1 - 1, 0), min(i1 + 1, n - 1)) # end of the high branch
|
|
253
|
+
chiN_hi, R_low, _ = branch_point(line, kernel, O_hi)
|
|
254
|
+
chiN_lo, R_high_at_lo, _ = branch_point(line, kernel, O_lo)
|
|
255
|
+
|
|
256
|
+
# the state the system jumps to: the largest Omega with chiN(Omega) = chiN_hi
|
|
257
|
+
f = lambda o: branch_point(line, kernel, o)[0] - chiN_hi
|
|
258
|
+
O_jump = None
|
|
259
|
+
for k in range(i1, n - 1):
|
|
260
|
+
if f(Om[k]) * f(Om[k + 1]) <= 0:
|
|
261
|
+
O_jump = mp.findroot(f, (Om[k], Om[k + 1]), solver="bisect",
|
|
262
|
+
tol=mp.mpf("1e-14"))
|
|
263
|
+
R_jump = branch_point(line, kernel, O_jump)[1] if O_jump else None
|
|
264
|
+
return dict(chiN_lo=chiN_lo, chiN_hi=chiN_hi,
|
|
265
|
+
R_low=R_low, R_jump=R_jump, R_high_at_lo=R_high_at_lo,
|
|
266
|
+
Omega_lo=O_lo, Omega_hi=O_hi)
|