TransitionListener 2.0.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.
- transitionlistener/__init__.py +21 -0
- transitionlistener/bubbledynamics.py +2250 -0
- transitionlistener/bubbledynamics_fixedstep.py +2929 -0
- transitionlistener/check_installation.py +309 -0
- transitionlistener/ckm.py +91 -0
- transitionlistener/colors.py +288 -0
- transitionlistener/config.py +185 -0
- transitionlistener/constants.py +64 -0
- transitionlistener/counterterms/__init__.py +230 -0
- transitionlistener/counterterms/cw.py +298 -0
- transitionlistener/counterterms/tensors.py +343 -0
- transitionlistener/counterterms/twohdm.py +520 -0
- transitionlistener/counterterms/twohdm_solver.py +374 -0
- transitionlistener/errors.py +315 -0
- transitionlistener/examples/bp_et.yaml +64 -0
- transitionlistener/examples/bp_lisa.yaml +70 -0
- transitionlistener/examples/bp_muares.yaml +69 -0
- transitionlistener/examples/bp_pta.yaml +64 -0
- transitionlistener/examples/example_dark_photon.yaml +73 -0
- transitionlistener/examples/example_grid.yaml +55 -0
- transitionlistener/examples/example_line.yaml +51 -0
- transitionlistener/examples/example_point.yaml +73 -0
- transitionlistener/examples/example_random_conformal.yaml +42 -0
- transitionlistener/examples/example_ultranest.yaml +39 -0
- transitionlistener/finiteT.py +468 -0
- transitionlistener/generic_potential.py +1521 -0
- transitionlistener/gridplots.py +592 -0
- transitionlistener/gwfopt.py +604 -0
- transitionlistener/helper_functions.py +843 -0
- transitionlistener/hydrodynamics.py +647 -0
- transitionlistener/interface/__init__.py +95 -0
- transitionlistener/interface/check.py +51 -0
- transitionlistener/interface/cli.py +211 -0
- transitionlistener/interface/config_loader.py +264 -0
- transitionlistener/interface/logging_utils.py +30 -0
- transitionlistener/interface/output_schema.py +296 -0
- transitionlistener/interface/pipeline.py +300 -0
- transitionlistener/interface/samplers.py +565 -0
- transitionlistener/interface/scans.py +468 -0
- transitionlistener/interface/single_point.py +581 -0
- transitionlistener/interface/state.py +61 -0
- transitionlistener/lineplots.py +597 -0
- transitionlistener/logo/TL-logo_large.png +0 -0
- transitionlistener/logo/TL-logo_small.png +0 -0
- transitionlistener/models/TL_2HDM.py +512 -0
- transitionlistener/models/TL_conformal_dark_u1.py +202 -0
- transitionlistener/models/TL_dark_U1.py +198 -0
- transitionlistener/models/TL_dark_U1_g_parameterization.py +198 -0
- transitionlistener/models/TL_dark_flipflop.py +274 -0
- transitionlistener/models/templatePotential.py +260 -0
- transitionlistener/nucleation.py +412 -0
- transitionlistener/observability.py +790 -0
- transitionlistener/particles.py +436 -0
- transitionlistener/pathDeformation.py +1462 -0
- transitionlistener/percolation_adaptive_gridbuilders.py +1385 -0
- transitionlistener/percolation_adaptive_rate.py +1049 -0
- transitionlistener/percolation_adaptivestepsize.py +1632 -0
- transitionlistener/percolation_fixedstepsize.py +618 -0
- transitionlistener/phases.py +1499 -0
- transitionlistener/plot_settings.py +121 -0
- transitionlistener/plots.py +1781 -0
- transitionlistener/runtime_options.py +278 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/bandwidths.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/density.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/freqs.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log.txt +16 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rhogrid.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rholabels.txt +30 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/pulsar_list.txt +1 -0
- transitionlistener/tab_data/HLV-O2-PLI.csv +82 -0
- transitionlistener/tab_data/HLV-O2-noise.csv +83 -0
- transitionlistener/tab_data/HLVK-design-PLI.csv +61 -0
- transitionlistener/tab_data/HLVK-design-noise.csv +83 -0
- transitionlistener/tab_data/NG15_PLI_hc.csv +31 -0
- transitionlistener/tab_data/NG15_noise_hc.csv +56 -0
- transitionlistener/tab_data/PLI-dataset.csv +327 -0
- transitionlistener/tab_data/energydensities.dat +1001 -0
- transitionlistener/tab_data/finiteT_b.dat.txt +1999 -0
- transitionlistener/tab_data/finiteT_f.dat.txt +1000 -0
- transitionlistener/tab_data/geff_SM.dat +10009 -0
- transitionlistener/tab_data/muAres_f_Hz_PLI_h2OmegaGW.csv +131 -0
- transitionlistener/tab_data/muAres_f_Hz_h2Omega_noise.csv +99 -0
- transitionlistener/tab_data/noise-dataset.csv +327 -0
- transitionlistener/tab_data/numberdensities.dat +1001 -0
- transitionlistener/tab_data/pressures.dat +1001 -0
- transitionlistener/thermodynamics.py +686 -0
- transitionlistener/transitionObservables.py +1199 -0
- transitionlistener/transitionObservables_fixedstep.py +976 -0
- transitionlistener/transitions.py +1000 -0
- transitionlistener/tunneling1D.py +910 -0
- transitionlistener-2.0.0.dist-info/METADATA +817 -0
- transitionlistener-2.0.0.dist-info/RECORD +95 -0
- transitionlistener-2.0.0.dist-info/WHEEL +4 -0
- transitionlistener-2.0.0.dist-info/entry_points.txt +2 -0
- transitionlistener-2.0.0.dist-info/licenses/LICENSE.txt +674 -0
|
@@ -0,0 +1,1385 @@
|
|
|
1
|
+
"""Grid-building helpers for the adaptive_step_size solver.
|
|
2
|
+
|
|
3
|
+
This module owns the self-contained support-point and candidate-grid
|
|
4
|
+
construction logic used by
|
|
5
|
+
:mod:`transitionlistener.percolation_adaptivestepsize`. It takes numpy
|
|
6
|
+
arrays and settings in and produces candidate temperature grids; the
|
|
7
|
+
top-level scan controllers (and their :class:`PercolationState`
|
|
8
|
+
bookkeeping) stay in the parent module.
|
|
9
|
+
|
|
10
|
+
Part of TransitionListener v2.0
|
|
11
|
+
Documentation: https://tasillo.de/TransitionListener/
|
|
12
|
+
|
|
13
|
+
Authors:
|
|
14
|
+
Jonas Matuszak <jonas.matuszak@kit.edu>
|
|
15
|
+
Carlo Tasillo <carlo.tasillo@ific.uv.es>
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
import math
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
from transitionlistener import errors
|
|
23
|
+
from transitionlistener.bubbledynamics import (
|
|
24
|
+
PercolationGrid,
|
|
25
|
+
_temperature_grid,
|
|
26
|
+
)
|
|
27
|
+
from transitionlistener.percolation_adaptive_rate import (
|
|
28
|
+
HOT_PROBABILITY_ANCHORS,
|
|
29
|
+
HOT_RATE_ACCEPT_LOG,
|
|
30
|
+
_build_peak_centered_step1_window,
|
|
31
|
+
_build_step1_hot_probability_anchor_grid,
|
|
32
|
+
_build_step1_hot_rate_anchor_grid,
|
|
33
|
+
_find_value_crossing,
|
|
34
|
+
_hot_head_underresolved,
|
|
35
|
+
_log_gamma_h4_array,
|
|
36
|
+
_probability_anchor_presence_tolerance,
|
|
37
|
+
_probability_at,
|
|
38
|
+
_rate_interval_is_post_peak_inert,
|
|
39
|
+
_remesh_dp_limit,
|
|
40
|
+
_temperature_midpoint,
|
|
41
|
+
_temperature_present,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Fixed probability anchors used by the simplified step-2/step-3 remeshing
|
|
45
|
+
# controller. The idea is to approximate a near-uniform mesh in the physically
|
|
46
|
+
# interesting part of P(T) without requiring the caller to micromanage several
|
|
47
|
+
# different refinement heuristics. Anchors are only activated once the current
|
|
48
|
+
# profile actually reaches the corresponding probability.
|
|
49
|
+
_PROBABILITY_REMESH_ANCHORS = (
|
|
50
|
+
1.0e-3,
|
|
51
|
+
3.0e-3,
|
|
52
|
+
1.0e-2,
|
|
53
|
+
3.0e-2,
|
|
54
|
+
1.0e-1,
|
|
55
|
+
2.0e-1,
|
|
56
|
+
3.5e-1,
|
|
57
|
+
5.0e-1,
|
|
58
|
+
6.5e-1,
|
|
59
|
+
8.0e-1,
|
|
60
|
+
9.0e-1,
|
|
61
|
+
9.5e-1,
|
|
62
|
+
9.8e-1,
|
|
63
|
+
9.9e-1,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
_STARTUP_WINDOW_HIGH = 0.999
|
|
67
|
+
_COLD_TAIL_TARGET_DP = 0.08
|
|
68
|
+
_COLD_TAIL_TARGET_DI = 0.25
|
|
69
|
+
_COLD_TAIL_INTEGRAL_SWITCH_P = 0.8
|
|
70
|
+
_COLD_TAIL_MIN_LOG_STEP = 0.25
|
|
71
|
+
_COLD_TAIL_MAX_LOG_STEP = 5.0
|
|
72
|
+
_COLD_TAIL_STEP_SHRINK_LIMIT = 0.35
|
|
73
|
+
_COLD_TAIL_STEP_GROWTH_LIMIT = 3.0
|
|
74
|
+
_COLD_TAIL_ZERO_GAIN_SCALE = 2.0
|
|
75
|
+
_COLD_TAIL_POST_PEAK_MIN_STREAK = 2
|
|
76
|
+
_COLD_TAIL_POST_PEAK_BOOST_COEFF = 0.25
|
|
77
|
+
_COLD_TAIL_POST_PEAK_MAX_BOOST = 3.0
|
|
78
|
+
_STRICT_JUMP_REFINE_THRESHOLD = 0.05
|
|
79
|
+
_STRICT_JUMP_SUCCESS_THRESHOLD = 0.05
|
|
80
|
+
_STRICT_JUMP_P_MAX = 0.2
|
|
81
|
+
_STRICT_JUMP_APPLY_GLOBALLY = False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _tb_root_bracketed_for_tsym(
|
|
85
|
+
TSYM: float,
|
|
86
|
+
TBROmin: float,
|
|
87
|
+
TBROmax: float,
|
|
88
|
+
phase_symmetric,
|
|
89
|
+
phase_broken,
|
|
90
|
+
pot,
|
|
91
|
+
) -> bool:
|
|
92
|
+
"""Return whether the reheating criterion changes sign across ``[TBROmin, TBROmax]``."""
|
|
93
|
+
from transitionlistener import bubbledynamics as bd
|
|
94
|
+
|
|
95
|
+
crit_min = float(bd.Tb_criterion(TBROmin, TSYM, phase_symmetric, phase_broken, pot))
|
|
96
|
+
crit_max = float(bd.Tb_criterion(TBROmax, TSYM, phase_symmetric, phase_broken, pot))
|
|
97
|
+
if not (np.isfinite(crit_min) and np.isfinite(crit_max)):
|
|
98
|
+
return False
|
|
99
|
+
if crit_min == 0.0 or crit_max == 0.0:
|
|
100
|
+
return True
|
|
101
|
+
return bool(crit_min * crit_max < 0.0)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _highest_tsym_with_tb_root(
|
|
105
|
+
tmin: float,
|
|
106
|
+
tmax: float,
|
|
107
|
+
TBROmin: float,
|
|
108
|
+
TBROmax: float,
|
|
109
|
+
phase_symmetric,
|
|
110
|
+
phase_broken,
|
|
111
|
+
pot,
|
|
112
|
+
) -> float:
|
|
113
|
+
"""Find the hottest traced temperature that still admits a bracketed reheating root."""
|
|
114
|
+
tmin = float(tmin)
|
|
115
|
+
tmax = float(tmax)
|
|
116
|
+
if _tb_root_bracketed_for_tsym(tmax, TBROmin, TBROmax, phase_symmetric, phase_broken, pot):
|
|
117
|
+
return tmax
|
|
118
|
+
if not _tb_root_bracketed_for_tsym(tmin, TBROmin, TBROmax, phase_symmetric, phase_broken, pot):
|
|
119
|
+
raise errors.PercolationError(
|
|
120
|
+
"Tb_criterion has no bracketed reheating root anywhere inside the traced "
|
|
121
|
+
f"phase-overlap interval [{tmin:.8g}, {tmax:.8g}]."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
low = tmin
|
|
125
|
+
high = tmax
|
|
126
|
+
scale = max(abs(tmin), abs(tmax), abs(TBROmin), abs(TBROmax), 1.0)
|
|
127
|
+
tol = 1e-8 * scale
|
|
128
|
+
for _ in range(80):
|
|
129
|
+
if abs(high - low) <= tol:
|
|
130
|
+
break
|
|
131
|
+
mid = 0.5 * (low + high)
|
|
132
|
+
if _tb_root_bracketed_for_tsym(mid, TBROmin, TBROmax, phase_symmetric, phase_broken, pot):
|
|
133
|
+
low = mid
|
|
134
|
+
else:
|
|
135
|
+
high = mid
|
|
136
|
+
return float(low)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _next_untried_scout_temperature(
|
|
140
|
+
high: float,
|
|
141
|
+
low: float,
|
|
142
|
+
tried: np.ndarray,
|
|
143
|
+
*,
|
|
144
|
+
prefer: str,
|
|
145
|
+
) -> float | None:
|
|
146
|
+
"""Bisect an interval until a temperature not already present in ``tried`` is found."""
|
|
147
|
+
high = float(high)
|
|
148
|
+
low = float(low)
|
|
149
|
+
tried = np.asarray(tried, dtype=float)
|
|
150
|
+
if not (np.isfinite(high) and np.isfinite(low)) or high <= low:
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
current_high = high
|
|
154
|
+
current_low = low
|
|
155
|
+
for _ in range(32):
|
|
156
|
+
candidate = _temperature_midpoint(current_high, current_low)
|
|
157
|
+
if candidate is None or not np.isfinite(float(candidate)):
|
|
158
|
+
return None
|
|
159
|
+
if not _temperature_present(tried, float(candidate)):
|
|
160
|
+
return float(candidate)
|
|
161
|
+
if prefer == "hotter":
|
|
162
|
+
current_low = float(candidate)
|
|
163
|
+
else:
|
|
164
|
+
current_high = float(candidate)
|
|
165
|
+
if current_high <= current_low:
|
|
166
|
+
return None
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class _Step1ApproxProfile:
|
|
172
|
+
"""Approximate percolation profile reconstructed from cached step-1 actions."""
|
|
173
|
+
|
|
174
|
+
sampled_temperatures: np.ndarray
|
|
175
|
+
temperatures: np.ndarray
|
|
176
|
+
actions: np.ndarray
|
|
177
|
+
gamma: np.ndarray
|
|
178
|
+
hubble_sym: np.ndarray
|
|
179
|
+
log_gamma_h4_sym: np.ndarray
|
|
180
|
+
i_approx: np.ndarray
|
|
181
|
+
probabilities: np.ndarray
|
|
182
|
+
tperc_approx: float
|
|
183
|
+
startup_center: float
|
|
184
|
+
window_high: float | None
|
|
185
|
+
window_low: float | None
|
|
186
|
+
window_source: str
|
|
187
|
+
peak_temperature: float | None = None
|
|
188
|
+
peak_log_gamma_h4: float | None = None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _build_step1_approx_profile(
|
|
192
|
+
outdict: dict,
|
|
193
|
+
pot,
|
|
194
|
+
phase_symmetric,
|
|
195
|
+
phase_broken,
|
|
196
|
+
vw: float,
|
|
197
|
+
tmin: float,
|
|
198
|
+
tmax: float,
|
|
199
|
+
tperc_approx: float,
|
|
200
|
+
settings,
|
|
201
|
+
sampled_temperatures: np.ndarray | None = None,
|
|
202
|
+
) -> _Step1ApproxProfile | None:
|
|
203
|
+
"""Reconstruct an approximate percolation profile from cached action temperatures."""
|
|
204
|
+
from transitionlistener import bubbledynamics as bd
|
|
205
|
+
|
|
206
|
+
cached_temperatures = bd._transition_action_outdict_temperatures(
|
|
207
|
+
outdict,
|
|
208
|
+
phase_symmetric,
|
|
209
|
+
phase_broken,
|
|
210
|
+
tmin=tmin,
|
|
211
|
+
tmax=tmax,
|
|
212
|
+
)
|
|
213
|
+
all_sampled_temperatures = _temperature_grid(
|
|
214
|
+
np.concatenate(
|
|
215
|
+
(
|
|
216
|
+
np.asarray(cached_temperatures, dtype=float),
|
|
217
|
+
np.asarray([] if sampled_temperatures is None else sampled_temperatures, dtype=float),
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
if cached_temperatures.size < 2:
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
actions = np.asarray(
|
|
225
|
+
[float(bd.calcAction(pot, float(temp), phase_symmetric, phase_broken, outdict)) for temp in cached_temperatures],
|
|
226
|
+
dtype=float,
|
|
227
|
+
)
|
|
228
|
+
mask = np.isfinite(cached_temperatures) & np.isfinite(actions)
|
|
229
|
+
temperatures = np.asarray(cached_temperatures[mask], dtype=float)
|
|
230
|
+
actions = np.asarray(actions[mask], dtype=float)
|
|
231
|
+
if temperatures.size < 2:
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
hubble_sym = np.asarray(
|
|
235
|
+
[
|
|
236
|
+
float(bd.HubbleParameter(bd.energyDensity(pot, phase_symmetric, float(temp)), pot.conversionFactor))
|
|
237
|
+
for temp in temperatures
|
|
238
|
+
],
|
|
239
|
+
dtype=float,
|
|
240
|
+
)
|
|
241
|
+
gamma = np.asarray(bd.Gamma(temperatures, actions), dtype=float)
|
|
242
|
+
log_gamma_h4_sym = np.asarray(bd.logGamma(temperatures, actions), dtype=float) - 4.0 * np.log(hubble_sym)
|
|
243
|
+
if not (np.all(np.isfinite(hubble_sym)) and np.all(np.isfinite(gamma)) and np.all(np.isfinite(log_gamma_h4_sym))):
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
i_approx = np.zeros_like(temperatures, dtype=float)
|
|
247
|
+
probabilities = np.zeros_like(temperatures, dtype=float)
|
|
248
|
+
i_approx_vals, probabilities[:] = bd.percIntegralODE_full_sweep(
|
|
249
|
+
temperatures,
|
|
250
|
+
hubble_sym,
|
|
251
|
+
actions,
|
|
252
|
+
vw=vw,
|
|
253
|
+
pot=pot,
|
|
254
|
+
phase_symmetric=phase_symmetric,
|
|
255
|
+
time_temperature_mode=settings.time_temperature_mode,
|
|
256
|
+
integral_method=settings.integral_method,
|
|
257
|
+
)
|
|
258
|
+
i_approx[:] = i_approx_vals
|
|
259
|
+
|
|
260
|
+
window_high, _, _ = _find_value_crossing(
|
|
261
|
+
temperatures,
|
|
262
|
+
probabilities,
|
|
263
|
+
float(settings.f_start),
|
|
264
|
+
)
|
|
265
|
+
window_low, _, _ = _find_value_crossing(
|
|
266
|
+
temperatures,
|
|
267
|
+
probabilities,
|
|
268
|
+
_STARTUP_WINDOW_HIGH,
|
|
269
|
+
)
|
|
270
|
+
startup_center = float(tperc_approx)
|
|
271
|
+
window_source = "probability"
|
|
272
|
+
peak_temperature = None
|
|
273
|
+
peak_log_gamma_h4 = None
|
|
274
|
+
|
|
275
|
+
if window_high is None or window_low is None:
|
|
276
|
+
peak_temperature, peak_log_gamma_h4, peak_window_high, peak_window_low = _build_peak_centered_step1_window(
|
|
277
|
+
temperatures,
|
|
278
|
+
log_gamma_h4_sym,
|
|
279
|
+
settings,
|
|
280
|
+
)
|
|
281
|
+
if peak_window_high is not None and peak_window_low is not None:
|
|
282
|
+
window_high = float(peak_window_high)
|
|
283
|
+
window_low = float(peak_window_low)
|
|
284
|
+
startup_center = float(peak_temperature)
|
|
285
|
+
window_source = "peak_log_gamma_h4"
|
|
286
|
+
|
|
287
|
+
return _Step1ApproxProfile(
|
|
288
|
+
sampled_temperatures=all_sampled_temperatures,
|
|
289
|
+
temperatures=temperatures,
|
|
290
|
+
actions=actions,
|
|
291
|
+
gamma=gamma,
|
|
292
|
+
hubble_sym=hubble_sym,
|
|
293
|
+
log_gamma_h4_sym=log_gamma_h4_sym,
|
|
294
|
+
i_approx=i_approx,
|
|
295
|
+
probabilities=probabilities,
|
|
296
|
+
tperc_approx=float(tperc_approx),
|
|
297
|
+
startup_center=float(startup_center),
|
|
298
|
+
window_high=None if window_high is None else float(window_high),
|
|
299
|
+
window_low=None if window_low is None else float(window_low),
|
|
300
|
+
window_source=str(window_source),
|
|
301
|
+
peak_temperature=None if peak_temperature is None else float(peak_temperature),
|
|
302
|
+
peak_log_gamma_h4=None if peak_log_gamma_h4 is None else float(peak_log_gamma_h4),
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _scout_needs_hot_extension(profile: _Step1ApproxProfile | None, settings) -> bool:
|
|
307
|
+
"""Decide whether the approximate step-1 profile must be extended to hotter temperatures."""
|
|
308
|
+
if profile is None:
|
|
309
|
+
return True
|
|
310
|
+
if profile.probabilities.size == 0:
|
|
311
|
+
return True
|
|
312
|
+
if float(profile.probabilities[0]) > float(settings.f_start):
|
|
313
|
+
return True
|
|
314
|
+
hot_head_unresolved, _, _ = _hot_head_underresolved(
|
|
315
|
+
profile.temperatures,
|
|
316
|
+
profile.probabilities,
|
|
317
|
+
profile.actions,
|
|
318
|
+
profile.hubble_sym,
|
|
319
|
+
settings,
|
|
320
|
+
for_acceptance=False,
|
|
321
|
+
)
|
|
322
|
+
if hot_head_unresolved:
|
|
323
|
+
return True
|
|
324
|
+
if profile.window_high is not None and profile.window_low is not None:
|
|
325
|
+
return False
|
|
326
|
+
return profile.window_high is None
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def _scout_needs_cold_extension(profile: _Step1ApproxProfile | None, settings) -> bool:
|
|
330
|
+
"""Decide whether the approximate step-1 profile must be extended to colder temperatures."""
|
|
331
|
+
if profile is None:
|
|
332
|
+
return True
|
|
333
|
+
if profile.window_high is not None and profile.window_low is not None:
|
|
334
|
+
return False
|
|
335
|
+
if profile.probabilities.size == 0:
|
|
336
|
+
return True
|
|
337
|
+
if float(profile.probabilities[-1]) < float(settings.f_final):
|
|
338
|
+
return True
|
|
339
|
+
return profile.window_low is None
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _remaining_scout_action_budget(
|
|
343
|
+
outdict: dict,
|
|
344
|
+
phase_symmetric,
|
|
345
|
+
phase_broken,
|
|
346
|
+
settings,
|
|
347
|
+
*,
|
|
348
|
+
tmin: float,
|
|
349
|
+
tmax: float,
|
|
350
|
+
) -> int:
|
|
351
|
+
"""Return how many more unique transition action temperatures may be added."""
|
|
352
|
+
from transitionlistener import bubbledynamics as bd
|
|
353
|
+
|
|
354
|
+
used = int(
|
|
355
|
+
len(
|
|
356
|
+
bd._transition_action_outdict_temperatures(
|
|
357
|
+
outdict,
|
|
358
|
+
phase_symmetric,
|
|
359
|
+
phase_broken,
|
|
360
|
+
tmin=tmin,
|
|
361
|
+
tmax=tmax,
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
return max(int(settings.max_action_temperatures) - int(used), 0)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _extend_step1_scout_until_bracketed(
|
|
369
|
+
outdict: dict,
|
|
370
|
+
pot,
|
|
371
|
+
phase_symmetric,
|
|
372
|
+
phase_broken,
|
|
373
|
+
vw: float,
|
|
374
|
+
hot_grid_start: float,
|
|
375
|
+
tmin: float,
|
|
376
|
+
tmax: float,
|
|
377
|
+
tperc_approx: float,
|
|
378
|
+
settings,
|
|
379
|
+
verbose: bool,
|
|
380
|
+
) -> _Step1ApproxProfile | None:
|
|
381
|
+
"""Enlarge the step-1 scout bank until the approximate transition window is bracketed."""
|
|
382
|
+
from transitionlistener import bubbledynamics as bd
|
|
383
|
+
|
|
384
|
+
attempted_scout_temperatures = np.asarray([], dtype=float)
|
|
385
|
+
|
|
386
|
+
profile = _build_step1_approx_profile(
|
|
387
|
+
outdict,
|
|
388
|
+
pot,
|
|
389
|
+
phase_symmetric,
|
|
390
|
+
phase_broken,
|
|
391
|
+
vw,
|
|
392
|
+
tmin,
|
|
393
|
+
tmax,
|
|
394
|
+
tperc_approx,
|
|
395
|
+
settings,
|
|
396
|
+
sampled_temperatures=attempted_scout_temperatures,
|
|
397
|
+
)
|
|
398
|
+
max_extensions = max(int(settings.maxit), 1) * 4
|
|
399
|
+
for _ in range(max_extensions):
|
|
400
|
+
need_hot = _scout_needs_hot_extension(profile, settings)
|
|
401
|
+
need_cold = _scout_needs_cold_extension(profile, settings)
|
|
402
|
+
if not need_hot and not need_cold:
|
|
403
|
+
return profile
|
|
404
|
+
|
|
405
|
+
remaining_budget = _remaining_scout_action_budget(
|
|
406
|
+
outdict,
|
|
407
|
+
phase_symmetric,
|
|
408
|
+
phase_broken,
|
|
409
|
+
settings,
|
|
410
|
+
tmin=tmin,
|
|
411
|
+
tmax=tmax,
|
|
412
|
+
)
|
|
413
|
+
if remaining_budget <= 0:
|
|
414
|
+
break
|
|
415
|
+
|
|
416
|
+
all_cached = bd._transition_action_outdict_temperatures(
|
|
417
|
+
outdict,
|
|
418
|
+
phase_symmetric,
|
|
419
|
+
phase_broken,
|
|
420
|
+
tmin=tmin,
|
|
421
|
+
tmax=tmax,
|
|
422
|
+
)
|
|
423
|
+
occupied_scout_points = _temperature_grid(
|
|
424
|
+
np.concatenate((np.asarray(all_cached, dtype=float), attempted_scout_temperatures))
|
|
425
|
+
)
|
|
426
|
+
candidate_temperatures: list[float] = []
|
|
427
|
+
if need_hot:
|
|
428
|
+
hottest = float(occupied_scout_points[0]) if occupied_scout_points.size else (
|
|
429
|
+
float(profile.temperatures[0]) if profile is not None and profile.temperatures.size else float(tperc_approx)
|
|
430
|
+
)
|
|
431
|
+
hot_scout_bound = max(float(hot_grid_start), float(tmax))
|
|
432
|
+
scout_temp = _next_untried_scout_temperature(
|
|
433
|
+
hot_scout_bound,
|
|
434
|
+
hottest,
|
|
435
|
+
occupied_scout_points,
|
|
436
|
+
prefer="hotter",
|
|
437
|
+
)
|
|
438
|
+
if scout_temp is not None and scout_temp < hot_scout_bound and scout_temp > hottest:
|
|
439
|
+
candidate_temperatures.append(float(scout_temp))
|
|
440
|
+
if need_cold:
|
|
441
|
+
coldest = float(occupied_scout_points[-1]) if occupied_scout_points.size else (
|
|
442
|
+
float(profile.temperatures[-1]) if profile is not None and profile.temperatures.size else float(tperc_approx)
|
|
443
|
+
)
|
|
444
|
+
scout_temp = _next_untried_scout_temperature(
|
|
445
|
+
coldest,
|
|
446
|
+
float(tmin),
|
|
447
|
+
occupied_scout_points,
|
|
448
|
+
prefer="colder",
|
|
449
|
+
)
|
|
450
|
+
if scout_temp is not None and scout_temp < coldest and scout_temp > float(tmin):
|
|
451
|
+
candidate_temperatures.append(float(scout_temp))
|
|
452
|
+
|
|
453
|
+
if not candidate_temperatures:
|
|
454
|
+
break
|
|
455
|
+
unique_candidates = _temperature_grid(candidate_temperatures)
|
|
456
|
+
attempted_scout_temperatures = _temperature_grid(
|
|
457
|
+
np.concatenate((attempted_scout_temperatures, unique_candidates))
|
|
458
|
+
)
|
|
459
|
+
for temperature in unique_candidates[:remaining_budget]:
|
|
460
|
+
if verbose:
|
|
461
|
+
print(
|
|
462
|
+
"Step-1 scout extension:",
|
|
463
|
+
f"T = {float(temperature) * pot.conversionFactor:2.5g} GeV",
|
|
464
|
+
)
|
|
465
|
+
bd.calcAction(pot, float(temperature), phase_symmetric, phase_broken, outdict)
|
|
466
|
+
|
|
467
|
+
profile = _build_step1_approx_profile(
|
|
468
|
+
outdict,
|
|
469
|
+
pot,
|
|
470
|
+
phase_symmetric,
|
|
471
|
+
phase_broken,
|
|
472
|
+
vw,
|
|
473
|
+
tmin,
|
|
474
|
+
tmax,
|
|
475
|
+
tperc_approx,
|
|
476
|
+
settings,
|
|
477
|
+
sampled_temperatures=attempted_scout_temperatures,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
return profile
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _fill_largest_gap(points: np.ndarray, high: float, low: float) -> float | None:
|
|
484
|
+
"""Return the midpoint of the largest gap inside one ordered interval."""
|
|
485
|
+
anchors = [float(high), float(low)]
|
|
486
|
+
for value in np.asarray(points, dtype=float):
|
|
487
|
+
if float(low) <= float(value) <= float(high):
|
|
488
|
+
anchors.append(float(value))
|
|
489
|
+
anchors = np.asarray(sorted(set(anchors), reverse=True), dtype=float)
|
|
490
|
+
if anchors.size < 2:
|
|
491
|
+
return None
|
|
492
|
+
gaps = anchors[:-1] - anchors[1:]
|
|
493
|
+
if gaps.size == 0:
|
|
494
|
+
return None
|
|
495
|
+
gap_index = int(np.argmax(gaps))
|
|
496
|
+
upper = float(anchors[gap_index])
|
|
497
|
+
lower = float(anchors[gap_index + 1])
|
|
498
|
+
return _temperature_midpoint(upper, lower)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _cached_promotion_bounds(active_grid: np.ndarray) -> tuple[float, float] | None:
|
|
502
|
+
"""Return a narrow interval around the current active grid for cache promotion.
|
|
503
|
+
|
|
504
|
+
Dormant step-1 scout points remain available in the support bank, but only
|
|
505
|
+
the cached temperatures close to the currently active transition band should
|
|
506
|
+
be promoted automatically. Larger range changes are handled by the explicit
|
|
507
|
+
range-expansion logic.
|
|
508
|
+
"""
|
|
509
|
+
temps = _temperature_grid(active_grid)
|
|
510
|
+
if temps.size == 0:
|
|
511
|
+
return None
|
|
512
|
+
high = float(temps[0])
|
|
513
|
+
low = float(temps[-1])
|
|
514
|
+
span = max(high - low, 0.0)
|
|
515
|
+
mean_spacing = span / max(len(temps) - 1, 1)
|
|
516
|
+
padding = max(0.15 * span, 1.5 * mean_spacing)
|
|
517
|
+
return float(high + padding), float(low - padding)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _build_step1_active_grid(
|
|
521
|
+
profile: _Step1ApproxProfile,
|
|
522
|
+
settings,
|
|
523
|
+
) -> np.ndarray | None:
|
|
524
|
+
"""Build the compact active TSYM grid from the approximate step-1 profile."""
|
|
525
|
+
if profile.window_high is None or profile.window_low is None:
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
temperatures = _temperature_grid(profile.temperatures)
|
|
529
|
+
scale = max(
|
|
530
|
+
abs(float(profile.window_high)),
|
|
531
|
+
abs(float(profile.window_low)),
|
|
532
|
+
abs(float(profile.startup_center)),
|
|
533
|
+
1.0,
|
|
534
|
+
)
|
|
535
|
+
atol = 1e-10 * scale
|
|
536
|
+
active = np.asarray(
|
|
537
|
+
[
|
|
538
|
+
float(value)
|
|
539
|
+
for value in temperatures
|
|
540
|
+
if float(profile.window_low) - atol <= float(value) <= float(profile.window_high) + atol
|
|
541
|
+
],
|
|
542
|
+
dtype=float,
|
|
543
|
+
)
|
|
544
|
+
active = _temperature_grid(active)
|
|
545
|
+
|
|
546
|
+
boundary_candidates = [float(profile.window_high), float(profile.window_low)]
|
|
547
|
+
for candidate in boundary_candidates:
|
|
548
|
+
if not np.any(np.isclose(active, candidate, atol=atol, rtol=0.0)):
|
|
549
|
+
active = _temperature_grid(np.append(active, candidate))
|
|
550
|
+
|
|
551
|
+
hot_probability_grid = _build_step1_hot_probability_anchor_grid(profile, settings, active)
|
|
552
|
+
if hot_probability_grid is not None:
|
|
553
|
+
for value in hot_probability_grid:
|
|
554
|
+
active = _temperature_grid(np.append(active, float(value)))
|
|
555
|
+
|
|
556
|
+
hot_rate_grid = _build_step1_hot_rate_anchor_grid(profile, settings, active)
|
|
557
|
+
if hot_rate_grid is not None:
|
|
558
|
+
for value in hot_rate_grid:
|
|
559
|
+
active = _temperature_grid(np.append(active, float(value)))
|
|
560
|
+
|
|
561
|
+
requested = max(int(settings.n_action_min), 2)
|
|
562
|
+
n_hi = max(int(math.ceil(requested * float(settings.weight))), 1)
|
|
563
|
+
n_lo = max(int(requested - n_hi), 1)
|
|
564
|
+
|
|
565
|
+
def _count_high(points: np.ndarray) -> int:
|
|
566
|
+
return int(np.count_nonzero(np.asarray(points, dtype=float) > float(profile.startup_center) + atol))
|
|
567
|
+
|
|
568
|
+
def _count_low(points: np.ndarray) -> int:
|
|
569
|
+
return int(np.count_nonzero(np.asarray(points, dtype=float) < float(profile.startup_center) - atol))
|
|
570
|
+
|
|
571
|
+
while _count_high(active) < n_hi:
|
|
572
|
+
midpoint = _fill_largest_gap(active, float(profile.window_high), float(profile.startup_center))
|
|
573
|
+
if midpoint is None:
|
|
574
|
+
break
|
|
575
|
+
updated_active = _temperature_grid(np.append(active, midpoint))
|
|
576
|
+
if updated_active.size == active.size:
|
|
577
|
+
break
|
|
578
|
+
active = updated_active
|
|
579
|
+
|
|
580
|
+
while _count_low(active) < n_lo:
|
|
581
|
+
midpoint = _fill_largest_gap(active, float(profile.startup_center), float(profile.window_low))
|
|
582
|
+
if midpoint is None:
|
|
583
|
+
break
|
|
584
|
+
updated_active = _temperature_grid(np.append(active, midpoint))
|
|
585
|
+
if updated_active.size == active.size:
|
|
586
|
+
break
|
|
587
|
+
active = updated_active
|
|
588
|
+
|
|
589
|
+
return _temperature_grid(active)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _initial_temperature_grid(
|
|
593
|
+
outdict: dict,
|
|
594
|
+
pot,
|
|
595
|
+
Tstart: float,
|
|
596
|
+
phase_symmetric,
|
|
597
|
+
phase_broken,
|
|
598
|
+
vw: float,
|
|
599
|
+
nAction: int,
|
|
600
|
+
settings,
|
|
601
|
+
overlap_tmin: float,
|
|
602
|
+
overlap_tmax: float,
|
|
603
|
+
verbose: bool,
|
|
604
|
+
) -> PercolationGrid:
|
|
605
|
+
"""Step 1: build the initial TSYM grid around the approximate percolation point."""
|
|
606
|
+
from transitionlistener import bubbledynamics as bd
|
|
607
|
+
tmin = float(overlap_tmin)
|
|
608
|
+
raw_tmax = float(overlap_tmax)
|
|
609
|
+
hot_grid_start = _highest_tsym_with_tb_root(
|
|
610
|
+
tmin,
|
|
611
|
+
raw_tmax,
|
|
612
|
+
float(phase_broken.Tmin),
|
|
613
|
+
float(phase_broken.Tmax),
|
|
614
|
+
phase_symmetric,
|
|
615
|
+
phase_broken,
|
|
616
|
+
pot,
|
|
617
|
+
)
|
|
618
|
+
tmax = raw_tmax
|
|
619
|
+
if verbose and hot_grid_start < raw_tmax:
|
|
620
|
+
print(
|
|
621
|
+
"Initial active adaptive step size grid starts at the hottest TSYM that still "
|
|
622
|
+
"brackets Tb_criterion, but hotter pure-symmetric support remains available:",
|
|
623
|
+
f"{raw_tmax * pot.conversionFactor:2.5g} -> {hot_grid_start * pot.conversionFactor:2.5g} GeV",
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
if verbose:
|
|
627
|
+
print("\nPercolation step 1: calculating Tperc using the saddlepoint approximation")
|
|
628
|
+
try:
|
|
629
|
+
TpercApprox = bd.calcApproxPercolation(
|
|
630
|
+
outdict,
|
|
631
|
+
pot,
|
|
632
|
+
phase_symmetric,
|
|
633
|
+
phase_broken,
|
|
634
|
+
vw,
|
|
635
|
+
verbose,
|
|
636
|
+
tmin=overlap_tmin,
|
|
637
|
+
tmax=overlap_tmax,
|
|
638
|
+
)
|
|
639
|
+
except errors.PercolationApproximation1Error as err:
|
|
640
|
+
if verbose:
|
|
641
|
+
print(
|
|
642
|
+
"Percolation approximation 1 failed: ",
|
|
643
|
+
err,
|
|
644
|
+
". We will continue with TpercApprox = hot_grid_start.",
|
|
645
|
+
)
|
|
646
|
+
TpercApprox = hot_grid_start
|
|
647
|
+
if verbose:
|
|
648
|
+
print(f"Approximate percolation temperature: {TpercApprox * pot.conversionFactor:2.5g} GeV")
|
|
649
|
+
|
|
650
|
+
initial_n_action = int(nAction)
|
|
651
|
+
grid_n = int(settings.n_action_min)
|
|
652
|
+
|
|
653
|
+
scout_profile = _extend_step1_scout_until_bracketed(
|
|
654
|
+
outdict,
|
|
655
|
+
pot,
|
|
656
|
+
phase_symmetric,
|
|
657
|
+
phase_broken,
|
|
658
|
+
vw,
|
|
659
|
+
hot_grid_start,
|
|
660
|
+
tmin,
|
|
661
|
+
tmax,
|
|
662
|
+
TpercApprox,
|
|
663
|
+
settings,
|
|
664
|
+
verbose,
|
|
665
|
+
)
|
|
666
|
+
if scout_profile is not None:
|
|
667
|
+
active_grid = _build_step1_active_grid(scout_profile, settings)
|
|
668
|
+
if active_grid is not None and active_grid.size >= 2:
|
|
669
|
+
if verbose:
|
|
670
|
+
print(
|
|
671
|
+
"Step-1 scout resolved an approximate active window:",
|
|
672
|
+
f"[{float(active_grid[-1]) * pot.conversionFactor:2.5g}, "
|
|
673
|
+
f"{float(active_grid[0]) * pot.conversionFactor:2.5g}] GeV with "
|
|
674
|
+
f"{len(active_grid)} active TSYM support points and "
|
|
675
|
+
f"{len(scout_profile.temperatures)} cached scout temperatures.",
|
|
676
|
+
f"(source: {scout_profile.window_source})",
|
|
677
|
+
)
|
|
678
|
+
return PercolationGrid(
|
|
679
|
+
TSYM=active_grid,
|
|
680
|
+
Tstart=float(active_grid[0]),
|
|
681
|
+
TpercApprox=float(scout_profile.tperc_approx),
|
|
682
|
+
tmin=tmin,
|
|
683
|
+
tmax=tmax,
|
|
684
|
+
TBROmin=phase_broken.Tmin,
|
|
685
|
+
TBROmax=phase_broken.Tmax,
|
|
686
|
+
free_support_bank=scout_profile.sampled_temperatures.copy(),
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
if verbose:
|
|
690
|
+
print(
|
|
691
|
+
"Warning: step-1 scout profile could not define any compact startup grid. "
|
|
692
|
+
"Using the broad startup-grid heuristic."
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
dT = (hot_grid_start - TpercApprox) / int(initial_n_action * settings.weight)
|
|
696
|
+
|
|
697
|
+
if TpercApprox < hot_grid_start:
|
|
698
|
+
upper = np.maximum(TpercApprox - int(initial_n_action * (1 - settings.weight)) * dT, tmin)
|
|
699
|
+
TSYM = np.linspace(hot_grid_start, upper, grid_n)
|
|
700
|
+
else:
|
|
701
|
+
if verbose:
|
|
702
|
+
print("Warning: TpercApprox > overlap Tmax, use interval around TpercApprox")
|
|
703
|
+
_, alpha_perc_approx, _, _, _, _, _ = bd.calcAlphas(
|
|
704
|
+
TpercApprox,
|
|
705
|
+
pot,
|
|
706
|
+
phase_symmetric,
|
|
707
|
+
phase_broken,
|
|
708
|
+
verbose=verbose,
|
|
709
|
+
)
|
|
710
|
+
if alpha_perc_approx < 1:
|
|
711
|
+
Tmin = 0.999 * TpercApprox
|
|
712
|
+
Tmax = 1.001 * TpercApprox
|
|
713
|
+
elif alpha_perc_approx < 10:
|
|
714
|
+
Tmin = 0.9 * TpercApprox
|
|
715
|
+
Tmax = 1.1 * TpercApprox
|
|
716
|
+
elif alpha_perc_approx < 100:
|
|
717
|
+
Tmin = 0.8 * TpercApprox
|
|
718
|
+
Tmax = 1.2 * TpercApprox
|
|
719
|
+
else:
|
|
720
|
+
Tmin = 0.5 * TpercApprox
|
|
721
|
+
Tmax = 1.5 * TpercApprox
|
|
722
|
+
Tmax = min(Tmax, tmax)
|
|
723
|
+
Tmin = max(Tmin, tmin)
|
|
724
|
+
if Tmax <= Tmin:
|
|
725
|
+
# If clipping the local interval around TpercApprox collapses or inverts
|
|
726
|
+
# the bracket, fall back to a narrow valid window at the hot overlap edge.
|
|
727
|
+
# This keeps the startup grid descending and lets step 2 decide whether
|
|
728
|
+
# the transition history has to expand colder.
|
|
729
|
+
Tmax = float(tmax)
|
|
730
|
+
Tmin = max(float(tmin), float(tmax) * (1.0 - 1e-3))
|
|
731
|
+
TSYM = np.linspace(Tmax, Tmin, grid_n)
|
|
732
|
+
|
|
733
|
+
TSYM = _temperature_grid(TSYM)
|
|
734
|
+
|
|
735
|
+
return PercolationGrid(
|
|
736
|
+
TSYM=TSYM,
|
|
737
|
+
Tstart=hot_grid_start,
|
|
738
|
+
TpercApprox=TpercApprox,
|
|
739
|
+
tmin=tmin,
|
|
740
|
+
tmax=tmax,
|
|
741
|
+
TBROmin=phase_broken.Tmin,
|
|
742
|
+
TBROmax=phase_broken.Tmax,
|
|
743
|
+
free_support_bank=None if scout_profile is None else scout_profile.sampled_temperatures.copy(),
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def _build_dynamiczoomwindow_jump_grid(
|
|
748
|
+
jump_high: float,
|
|
749
|
+
jump_low: float,
|
|
750
|
+
*,
|
|
751
|
+
max_new_points: int,
|
|
752
|
+
) -> np.ndarray | None:
|
|
753
|
+
"""Resolve a sharp P(T) jump by adding a few intermediate temperatures.
|
|
754
|
+
|
|
755
|
+
Large late-time jumps often span several decades in temperature. In that
|
|
756
|
+
regime a geometric ladder is more informative than a linear spacing, while
|
|
757
|
+
modest intervals are still sampled linearly.
|
|
758
|
+
"""
|
|
759
|
+
if max_new_points <= 0:
|
|
760
|
+
return None
|
|
761
|
+
jump_high = float(jump_high)
|
|
762
|
+
jump_low = float(jump_low)
|
|
763
|
+
if not (np.isfinite(jump_high) and np.isfinite(jump_low)) or jump_high <= jump_low:
|
|
764
|
+
return None
|
|
765
|
+
if jump_high / max(jump_low, 1e-30) - 1 <= 1e-10:
|
|
766
|
+
return None
|
|
767
|
+
if jump_low > 0.0 and jump_high / jump_low >= 2.0:
|
|
768
|
+
refined = np.geomspace(jump_high, jump_low, int(max_new_points) + 2, dtype=float)[1:-1]
|
|
769
|
+
else:
|
|
770
|
+
refined = np.linspace(jump_high, jump_low, int(max_new_points) + 2, dtype=float)[1:-1]
|
|
771
|
+
refined = _temperature_grid(refined)
|
|
772
|
+
return refined if refined.size > 0 else None
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _largest_probability_jump_interval(
|
|
776
|
+
temperatures: np.ndarray,
|
|
777
|
+
probabilities: np.ndarray,
|
|
778
|
+
*,
|
|
779
|
+
min_delta_p: float,
|
|
780
|
+
p_max: float | None = None,
|
|
781
|
+
apply_globally: bool = True,
|
|
782
|
+
prefer_hottest: bool = False,
|
|
783
|
+
) -> tuple[float, float, float, float, float] | None:
|
|
784
|
+
"""Return the largest neighbouring interval with Delta P above the threshold.
|
|
785
|
+
|
|
786
|
+
The return values are ``(jump_high, jump_low, probability_high,
|
|
787
|
+
probability_low, delta_p)`` in the descending-temperature convention used by
|
|
788
|
+
the support bank. Intervals that contain NaNs or non-descending
|
|
789
|
+
temperatures are ignored. When ``apply_globally`` is false, only intervals
|
|
790
|
+
touching ``P < p_max`` are considered.
|
|
791
|
+
"""
|
|
792
|
+
temps = np.asarray(temperatures, dtype=float)
|
|
793
|
+
probs = np.asarray(probabilities, dtype=float)
|
|
794
|
+
if temps.size < 2 or probs.size < 2:
|
|
795
|
+
return None
|
|
796
|
+
|
|
797
|
+
best_interval: tuple[float, float, float, float, float] | None = None
|
|
798
|
+
best_delta_p = float(min_delta_p)
|
|
799
|
+
for i in range(min(len(temps), len(probs)) - 1):
|
|
800
|
+
jump_high = float(temps[i])
|
|
801
|
+
jump_low = float(temps[i + 1])
|
|
802
|
+
probability_high = float(probs[i])
|
|
803
|
+
probability_low = float(probs[i + 1])
|
|
804
|
+
if not (
|
|
805
|
+
np.isfinite(jump_high)
|
|
806
|
+
and np.isfinite(jump_low)
|
|
807
|
+
and np.isfinite(probability_high)
|
|
808
|
+
and np.isfinite(probability_low)
|
|
809
|
+
and jump_high > jump_low
|
|
810
|
+
):
|
|
811
|
+
continue
|
|
812
|
+
if not apply_globally:
|
|
813
|
+
if p_max is None or not np.isfinite(float(p_max)) or float(p_max) <= 0.0:
|
|
814
|
+
continue
|
|
815
|
+
if min(probability_high, probability_low) >= float(p_max):
|
|
816
|
+
continue
|
|
817
|
+
delta_p = abs(probability_low - probability_high)
|
|
818
|
+
if delta_p <= float(min_delta_p):
|
|
819
|
+
continue
|
|
820
|
+
if prefer_hottest:
|
|
821
|
+
return (
|
|
822
|
+
jump_high,
|
|
823
|
+
jump_low,
|
|
824
|
+
probability_high,
|
|
825
|
+
probability_low,
|
|
826
|
+
float(delta_p),
|
|
827
|
+
)
|
|
828
|
+
if delta_p > best_delta_p:
|
|
829
|
+
best_delta_p = float(delta_p)
|
|
830
|
+
best_interval = (
|
|
831
|
+
jump_high,
|
|
832
|
+
jump_low,
|
|
833
|
+
probability_high,
|
|
834
|
+
probability_low,
|
|
835
|
+
float(delta_p),
|
|
836
|
+
)
|
|
837
|
+
return best_interval
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def _preferred_probability_jump_interval(
|
|
841
|
+
temperatures: np.ndarray,
|
|
842
|
+
probabilities: np.ndarray,
|
|
843
|
+
settings,
|
|
844
|
+
*,
|
|
845
|
+
for_acceptance: bool,
|
|
846
|
+
) -> tuple[tuple[float, float, float, float, float] | None, float, str]:
|
|
847
|
+
"""Return the jump interval the controller should refine or enforce first.
|
|
848
|
+
|
|
849
|
+
The stricter optional threshold is tried before the historical global one.
|
|
850
|
+
This allows threshold studies to demand tighter neighbouring-``Delta P``
|
|
851
|
+
control either globally or only in the early-rise part of the curve.
|
|
852
|
+
"""
|
|
853
|
+
|
|
854
|
+
strict_threshold = _STRICT_JUMP_SUCCESS_THRESHOLD if for_acceptance else _STRICT_JUMP_REFINE_THRESHOLD
|
|
855
|
+
generic_threshold = float(
|
|
856
|
+
settings.large_delta_p_success_threshold
|
|
857
|
+
if for_acceptance
|
|
858
|
+
else settings.large_delta_p_refine_threshold
|
|
859
|
+
)
|
|
860
|
+
generic_interval = _largest_probability_jump_interval(
|
|
861
|
+
temperatures,
|
|
862
|
+
probabilities,
|
|
863
|
+
min_delta_p=generic_threshold,
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
if strict_threshold > 0.0:
|
|
867
|
+
strict_interval = _largest_probability_jump_interval(
|
|
868
|
+
temperatures,
|
|
869
|
+
probabilities,
|
|
870
|
+
min_delta_p=strict_threshold,
|
|
871
|
+
p_max=_STRICT_JUMP_P_MAX,
|
|
872
|
+
apply_globally=_STRICT_JUMP_APPLY_GLOBALLY,
|
|
873
|
+
prefer_hottest=(not _STRICT_JUMP_APPLY_GLOBALLY and not bool(for_acceptance)),
|
|
874
|
+
)
|
|
875
|
+
if strict_interval is not None:
|
|
876
|
+
if (
|
|
877
|
+
not bool(for_acceptance)
|
|
878
|
+
and not _STRICT_JUMP_APPLY_GLOBALLY
|
|
879
|
+
and generic_interval is not None
|
|
880
|
+
and generic_threshold > 0.0
|
|
881
|
+
):
|
|
882
|
+
strict_severity = float(strict_interval[-1]) / max(float(strict_threshold), 1.0e-30)
|
|
883
|
+
generic_severity = float(generic_interval[-1]) / max(float(generic_threshold), 1.0e-30)
|
|
884
|
+
if generic_severity > max(1.5 * strict_severity, strict_severity + 0.5):
|
|
885
|
+
return generic_interval, generic_threshold, "generic"
|
|
886
|
+
strict_mode = "strict global" if _STRICT_JUMP_APPLY_GLOBALLY else f"strict P<{_STRICT_JUMP_P_MAX:2.3g}"
|
|
887
|
+
return strict_interval, strict_threshold, strict_mode
|
|
888
|
+
return generic_interval, generic_threshold, "generic"
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def _raise_if_large_delta_p_underresolved(
|
|
892
|
+
temperatures: np.ndarray,
|
|
893
|
+
probabilities: np.ndarray,
|
|
894
|
+
settings,
|
|
895
|
+
*,
|
|
896
|
+
stage: str,
|
|
897
|
+
outcome: str,
|
|
898
|
+
) -> None:
|
|
899
|
+
"""Reject a final profile when the remaining neighbouring jump is too large.
|
|
900
|
+
|
|
901
|
+
The controller already treats large neighbouring ``Delta P`` intervals as a
|
|
902
|
+
refinement target. This helper enforces a stricter final acceptance rule:
|
|
903
|
+
once the solver is about to return a physical verdict (success or eternal
|
|
904
|
+
inflation), the remaining largest neighbouring jump must be small enough to
|
|
905
|
+
trust that verdict numerically.
|
|
906
|
+
"""
|
|
907
|
+
|
|
908
|
+
jump_interval, threshold, threshold_mode = _preferred_probability_jump_interval(
|
|
909
|
+
temperatures,
|
|
910
|
+
probabilities,
|
|
911
|
+
settings,
|
|
912
|
+
for_acceptance=True,
|
|
913
|
+
)
|
|
914
|
+
if threshold <= 0.0:
|
|
915
|
+
return
|
|
916
|
+
if jump_interval is None:
|
|
917
|
+
return
|
|
918
|
+
jump_high, jump_low, probability_high, probability_low, delta_p = jump_interval
|
|
919
|
+
raise errors.PercolationError(
|
|
920
|
+
"Numerical accuracy error: detected a sharp unresolved jump in P(T) at "
|
|
921
|
+
f"T = [{jump_low:2.5g}, {jump_high:2.5g}] where "
|
|
922
|
+
f"P = {probability_high:2.5g} -> {probability_low:2.5g} "
|
|
923
|
+
f"(Delta P = {delta_p:2.5g} > {threshold:2.5g}). "
|
|
924
|
+
f"{stage} cannot accept the {outcome} profile because this jump remained "
|
|
925
|
+
"after the controller already spent its remaining support budget on local "
|
|
926
|
+
f"jump refinement ({threshold_mode} criterion). This indicates a "
|
|
927
|
+
"non-smooth or unstable bounce/action input in that interval, so the "
|
|
928
|
+
"transition profile is not trustworthy."
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _raise_if_hot_head_underresolved(
|
|
933
|
+
temperatures: np.ndarray,
|
|
934
|
+
probabilities: np.ndarray,
|
|
935
|
+
actions: np.ndarray,
|
|
936
|
+
hubble: np.ndarray,
|
|
937
|
+
settings,
|
|
938
|
+
*,
|
|
939
|
+
stage: str,
|
|
940
|
+
outcome: str,
|
|
941
|
+
) -> None:
|
|
942
|
+
"""Reject a final profile when the hottest retained support point starts too late."""
|
|
943
|
+
|
|
944
|
+
hot_head_unresolved, probability_hot, log_gamma_h4_hot = _hot_head_underresolved(
|
|
945
|
+
temperatures,
|
|
946
|
+
probabilities,
|
|
947
|
+
actions,
|
|
948
|
+
hubble,
|
|
949
|
+
settings,
|
|
950
|
+
for_acceptance=True,
|
|
951
|
+
)
|
|
952
|
+
if not hot_head_unresolved:
|
|
953
|
+
return
|
|
954
|
+
|
|
955
|
+
probability_gate = float(settings.f_start)
|
|
956
|
+
threshold = HOT_RATE_ACCEPT_LOG
|
|
957
|
+
raise errors.PercolationError(
|
|
958
|
+
"Numerical accuracy error: the hot onset of P(T) is still underresolved. "
|
|
959
|
+
f"The hottest retained support point has P = {probability_hot:2.5g} <= {probability_gate:2.5g}, "
|
|
960
|
+
f"but log10(Gamma/H^4) = {log_gamma_h4_hot:2.5g} > {threshold:2.5g}. "
|
|
961
|
+
f"{stage} cannot accept the {outcome} profile because the support still starts "
|
|
962
|
+
"too late on the hot side. This misses part of the early nucleation tail and "
|
|
963
|
+
"can underestimate RH / betaH_from_RH."
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
def _build_dynamiczoomwindow_unresolved_jump_grid(
|
|
968
|
+
temperatures: np.ndarray,
|
|
969
|
+
probabilities: np.ndarray,
|
|
970
|
+
settings,
|
|
971
|
+
*,
|
|
972
|
+
max_new_points: int,
|
|
973
|
+
actions: np.ndarray | None = None,
|
|
974
|
+
hubble: np.ndarray | None = None,
|
|
975
|
+
vw: float = 1.0,
|
|
976
|
+
) -> np.ndarray | None:
|
|
977
|
+
"""Return mandatory jump-refinement points for unresolved-tail profiles.
|
|
978
|
+
|
|
979
|
+
Even before the full transition is bracketed, a broad neighbouring jump in
|
|
980
|
+
``P(T)`` can already invalidate any later eternal-inflation / completion
|
|
981
|
+
verdict. In that situation the controller should spend support directly
|
|
982
|
+
inside the sharp interval before extending the cold tail further.
|
|
983
|
+
"""
|
|
984
|
+
|
|
985
|
+
jump_interval, _, threshold_mode = _preferred_probability_jump_interval(
|
|
986
|
+
temperatures,
|
|
987
|
+
probabilities,
|
|
988
|
+
settings,
|
|
989
|
+
for_acceptance=True,
|
|
990
|
+
)
|
|
991
|
+
if jump_interval is None:
|
|
992
|
+
return None
|
|
993
|
+
jump_high, jump_low, _, _, delta_p = jump_interval
|
|
994
|
+
if _rate_interval_is_post_peak_inert(
|
|
995
|
+
temperatures,
|
|
996
|
+
actions,
|
|
997
|
+
hubble,
|
|
998
|
+
jump_high,
|
|
999
|
+
jump_low,
|
|
1000
|
+
settings,
|
|
1001
|
+
vw=vw,
|
|
1002
|
+
):
|
|
1003
|
+
return None
|
|
1004
|
+
_, threshold, _ = _preferred_probability_jump_interval(
|
|
1005
|
+
temperatures,
|
|
1006
|
+
probabilities,
|
|
1007
|
+
settings,
|
|
1008
|
+
for_acceptance=True,
|
|
1009
|
+
)
|
|
1010
|
+
requested_points = int(max(1, max_new_points))
|
|
1011
|
+
if threshold > 0.0 and np.isfinite(delta_p):
|
|
1012
|
+
base_points = max(int(settings.n_action_increment), 1)
|
|
1013
|
+
severity = max(float(delta_p) / float(threshold), 1.0)
|
|
1014
|
+
boosted_points = int(math.ceil(base_points * severity))
|
|
1015
|
+
requested_points = min(
|
|
1016
|
+
int(max_new_points),
|
|
1017
|
+
max(base_points, min(boosted_points, 3 * base_points)),
|
|
1018
|
+
)
|
|
1019
|
+
return _build_dynamiczoomwindow_jump_grid(
|
|
1020
|
+
jump_high,
|
|
1021
|
+
jump_low,
|
|
1022
|
+
max_new_points=requested_points,
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def _range_expand_batch_size(settings, remaining_budget: int) -> int:
|
|
1027
|
+
"""Return the number of points one unresolved-edge range expansion may add.
|
|
1028
|
+
|
|
1029
|
+
Range growth is only meant to probe the next unresolved edge, not to flood
|
|
1030
|
+
the whole tail with support. Large batches were one of the main causes of
|
|
1031
|
+
the 150-250 point active grids in the first v2 tests, especially for low-g
|
|
1032
|
+
points that still needed many cold expansions.
|
|
1033
|
+
"""
|
|
1034
|
+
if remaining_budget <= 0:
|
|
1035
|
+
return 0
|
|
1036
|
+
return max(1, min(int(settings.n_action_increment), int(remaining_budget), 5))
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _build_dynamiczoomwindow_anchor_remesh_grid(
|
|
1040
|
+
temperatures: np.ndarray,
|
|
1041
|
+
probabilities: np.ndarray,
|
|
1042
|
+
settings,
|
|
1043
|
+
*,
|
|
1044
|
+
max_new_points: int | None = None,
|
|
1045
|
+
actions: np.ndarray | None = None,
|
|
1046
|
+
hubble: np.ndarray | None = None,
|
|
1047
|
+
vw: float = 1.0,
|
|
1048
|
+
) -> tuple[np.ndarray | None, str | None]:
|
|
1049
|
+
"""Return one simplified remeshing batch from fixed ``P(T)`` anchors.
|
|
1050
|
+
|
|
1051
|
+
The controller is intentionally simple:
|
|
1052
|
+
|
|
1053
|
+
1. add temperatures that correspond to a fixed set of probability anchors
|
|
1054
|
+
already reached by the current profile,
|
|
1055
|
+
2. then repeatedly split the largest neighbouring interval whose
|
|
1056
|
+
``Delta P`` still exceeds the allowed band-dependent threshold.
|
|
1057
|
+
|
|
1058
|
+
This gives a near-uniform mesh in probability space, which is close to the
|
|
1059
|
+
original goal of distributing support roughly evenly over the whole rise of
|
|
1060
|
+
``P(T)`` without stacking many separate refinement heuristics.
|
|
1061
|
+
"""
|
|
1062
|
+
|
|
1063
|
+
temps = np.asarray(temperatures, dtype=float)
|
|
1064
|
+
probs = np.asarray(probabilities, dtype=float)
|
|
1065
|
+
finite_mask = np.isfinite(temps) & np.isfinite(probs)
|
|
1066
|
+
temps = temps[finite_mask]
|
|
1067
|
+
probs = probs[finite_mask]
|
|
1068
|
+
if temps.size < 2:
|
|
1069
|
+
return None, None
|
|
1070
|
+
temps = _temperature_grid(temps)
|
|
1071
|
+
if temps.size != probs.size:
|
|
1072
|
+
# Preserve the original descending ordering when finite masks shortened
|
|
1073
|
+
# the arrays asymmetrically.
|
|
1074
|
+
probs = np.asarray(
|
|
1075
|
+
[float(value) for _, value in sorted(zip(np.asarray(temperatures, dtype=float)[finite_mask], probs), reverse=True)],
|
|
1076
|
+
dtype=float,
|
|
1077
|
+
)
|
|
1078
|
+
probs = probs[: temps.size]
|
|
1079
|
+
if temps.size < 2 or probs.size < 2:
|
|
1080
|
+
return None, None
|
|
1081
|
+
|
|
1082
|
+
requested_new_points = max(int(settings.n_action_increment if max_new_points is None else max_new_points), 1)
|
|
1083
|
+
candidate = np.asarray([], dtype=float)
|
|
1084
|
+
occupied = temps.copy()
|
|
1085
|
+
added_labels: list[str] = []
|
|
1086
|
+
occupied_probs_cache = np.asarray(probs, dtype=float).copy()
|
|
1087
|
+
|
|
1088
|
+
def update_occupied_probabilities() -> np.ndarray:
|
|
1089
|
+
values: list[float] = []
|
|
1090
|
+
for value in occupied:
|
|
1091
|
+
probability = _probability_at(value, temps, probs)
|
|
1092
|
+
values.append(float("nan") if probability is None else float(probability))
|
|
1093
|
+
return np.asarray(values, dtype=float)
|
|
1094
|
+
|
|
1095
|
+
def add_candidate(temperature: float | None, label: str) -> bool:
|
|
1096
|
+
nonlocal candidate, occupied, occupied_probs_cache
|
|
1097
|
+
if temperature is None or not np.isfinite(float(temperature)):
|
|
1098
|
+
return False
|
|
1099
|
+
value = float(temperature)
|
|
1100
|
+
if _temperature_present(occupied, value) or _temperature_present(candidate, value):
|
|
1101
|
+
return False
|
|
1102
|
+
updated_occupied = _temperature_grid(np.append(occupied, value))
|
|
1103
|
+
candidate = _temperature_grid(np.append(candidate, value))
|
|
1104
|
+
occupied = updated_occupied
|
|
1105
|
+
occupied_probs_cache = update_occupied_probabilities()
|
|
1106
|
+
added_labels.append(label)
|
|
1107
|
+
return True
|
|
1108
|
+
|
|
1109
|
+
p_min = float(np.min(probs))
|
|
1110
|
+
p_max = float(np.max(probs))
|
|
1111
|
+
anchor_targets = sorted(
|
|
1112
|
+
{
|
|
1113
|
+
float(value)
|
|
1114
|
+
for value in _PROBABILITY_REMESH_ANCHORS
|
|
1115
|
+
if np.isfinite(float(value))
|
|
1116
|
+
}
|
|
1117
|
+
| {
|
|
1118
|
+
float(value)
|
|
1119
|
+
for value in HOT_PROBABILITY_ANCHORS
|
|
1120
|
+
if np.isfinite(float(value))
|
|
1121
|
+
}
|
|
1122
|
+
| {float(settings.f_perc), float(settings.f_final), 1.0e-2}
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
for target in anchor_targets:
|
|
1126
|
+
if candidate.size >= requested_new_points:
|
|
1127
|
+
break
|
|
1128
|
+
if not (p_min + 1e-12 < target < p_max - 1e-12):
|
|
1129
|
+
continue
|
|
1130
|
+
target_tolerance = _probability_anchor_presence_tolerance(target, settings)
|
|
1131
|
+
if np.isfinite(target_tolerance) and np.any(
|
|
1132
|
+
np.isfinite(occupied_probs_cache)
|
|
1133
|
+
& (np.abs(occupied_probs_cache - float(target)) <= float(target_tolerance))
|
|
1134
|
+
):
|
|
1135
|
+
continue
|
|
1136
|
+
crossing, _, crossing_index = _find_value_crossing(temps, probs, target)
|
|
1137
|
+
if crossing_index is None:
|
|
1138
|
+
continue
|
|
1139
|
+
add_candidate(crossing, f"P={target:2.3g}")
|
|
1140
|
+
|
|
1141
|
+
while candidate.size < requested_new_points:
|
|
1142
|
+
occupied_probs = occupied_probs_cache
|
|
1143
|
+
|
|
1144
|
+
best_interval: tuple[float, float, float, float, float, float] | None = None
|
|
1145
|
+
best_score = 1.0
|
|
1146
|
+
for i in range(min(len(occupied), len(occupied_probs)) - 1):
|
|
1147
|
+
upper = float(occupied[i])
|
|
1148
|
+
lower = float(occupied[i + 1])
|
|
1149
|
+
probability_high = float(occupied_probs[i])
|
|
1150
|
+
probability_low = float(occupied_probs[i + 1])
|
|
1151
|
+
if not (
|
|
1152
|
+
np.isfinite(upper)
|
|
1153
|
+
and np.isfinite(lower)
|
|
1154
|
+
and np.isfinite(probability_high)
|
|
1155
|
+
and np.isfinite(probability_low)
|
|
1156
|
+
and upper > lower
|
|
1157
|
+
):
|
|
1158
|
+
continue
|
|
1159
|
+
delta_p = abs(probability_low - probability_high)
|
|
1160
|
+
threshold = _remesh_dp_limit(probability_high, probability_low, settings)
|
|
1161
|
+
if threshold <= 0.0:
|
|
1162
|
+
continue
|
|
1163
|
+
score = delta_p / threshold
|
|
1164
|
+
if _rate_interval_is_post_peak_inert(
|
|
1165
|
+
temps,
|
|
1166
|
+
actions,
|
|
1167
|
+
hubble,
|
|
1168
|
+
upper,
|
|
1169
|
+
lower,
|
|
1170
|
+
settings,
|
|
1171
|
+
vw=vw,
|
|
1172
|
+
):
|
|
1173
|
+
continue
|
|
1174
|
+
if score > best_score:
|
|
1175
|
+
best_score = score
|
|
1176
|
+
best_interval = (
|
|
1177
|
+
upper,
|
|
1178
|
+
lower,
|
|
1179
|
+
probability_high,
|
|
1180
|
+
probability_low,
|
|
1181
|
+
delta_p,
|
|
1182
|
+
threshold,
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
if best_interval is None:
|
|
1186
|
+
break
|
|
1187
|
+
|
|
1188
|
+
upper, lower, probability_high, probability_low, delta_p, threshold = best_interval
|
|
1189
|
+
midpoint = _temperature_midpoint(upper, lower)
|
|
1190
|
+
if not add_candidate(
|
|
1191
|
+
midpoint,
|
|
1192
|
+
f"dP={delta_p:2.3g}>{threshold:2.3g} at {probability_high:2.3g}->{probability_low:2.3g}",
|
|
1193
|
+
):
|
|
1194
|
+
break
|
|
1195
|
+
|
|
1196
|
+
if candidate.size == 0:
|
|
1197
|
+
return None, None
|
|
1198
|
+
|
|
1199
|
+
reason = (
|
|
1200
|
+
"anchor_remesh from fixed P(T) anchors and largest remaining Delta P gaps; added "
|
|
1201
|
+
+ ", ".join(added_labels)
|
|
1202
|
+
)
|
|
1203
|
+
return candidate, reason
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def _build_dynamiczoomwindow_range_grid(
|
|
1207
|
+
temperatures: np.ndarray,
|
|
1208
|
+
*,
|
|
1209
|
+
new_high: float,
|
|
1210
|
+
new_low: float,
|
|
1211
|
+
max_new_points: int,
|
|
1212
|
+
) -> np.ndarray | None:
|
|
1213
|
+
"""Extend the sampled temperature range with only a few new support points."""
|
|
1214
|
+
if max_new_points <= 0:
|
|
1215
|
+
return None
|
|
1216
|
+
|
|
1217
|
+
temps = _temperature_grid(temperatures)
|
|
1218
|
+
if temps.size == 0:
|
|
1219
|
+
return None
|
|
1220
|
+
|
|
1221
|
+
current_high = float(temps[0])
|
|
1222
|
+
current_low = float(temps[-1])
|
|
1223
|
+
span_high = max(float(new_high) - current_high, 0.0)
|
|
1224
|
+
span_low = max(current_low - float(new_low), 0.0)
|
|
1225
|
+
if span_high <= 0.0 and span_low <= 0.0:
|
|
1226
|
+
return None
|
|
1227
|
+
|
|
1228
|
+
remaining = int(max_new_points)
|
|
1229
|
+
n_high = 0
|
|
1230
|
+
n_low = 0
|
|
1231
|
+
if span_high > 0.0:
|
|
1232
|
+
n_high = 1
|
|
1233
|
+
remaining -= 1
|
|
1234
|
+
if span_low > 0.0 and remaining > 0:
|
|
1235
|
+
n_low = 1
|
|
1236
|
+
remaining -= 1
|
|
1237
|
+
|
|
1238
|
+
total_span = span_high + span_low
|
|
1239
|
+
while remaining > 0 and total_span > 0.0:
|
|
1240
|
+
if span_high > 0.0 and (
|
|
1241
|
+
span_low <= 0.0
|
|
1242
|
+
or (n_high - 1 + 1e-12) / max(span_high, 1e-30)
|
|
1243
|
+
<= (n_low - 1 + 1e-12) / max(span_low, 1e-30)
|
|
1244
|
+
):
|
|
1245
|
+
n_high += 1
|
|
1246
|
+
elif span_low > 0.0:
|
|
1247
|
+
n_low += 1
|
|
1248
|
+
remaining -= 1
|
|
1249
|
+
|
|
1250
|
+
candidate = np.asarray([], dtype=float)
|
|
1251
|
+
if n_high > 0:
|
|
1252
|
+
for value in np.linspace(float(new_high), current_high, n_high + 1, dtype=float)[:-1]:
|
|
1253
|
+
candidate = _temperature_grid(np.append(candidate, float(value)))
|
|
1254
|
+
if n_low > 0:
|
|
1255
|
+
low_endpoint = float(new_low)
|
|
1256
|
+
low_ratio = (
|
|
1257
|
+
current_low / low_endpoint
|
|
1258
|
+
if current_low > 0.0 and low_endpoint > 0.0
|
|
1259
|
+
else 1.0
|
|
1260
|
+
)
|
|
1261
|
+
if low_ratio > 10.0:
|
|
1262
|
+
low_values = np.geomspace(current_low, low_endpoint, n_low + 1, dtype=float)[1:]
|
|
1263
|
+
else:
|
|
1264
|
+
low_values = np.linspace(current_low, low_endpoint, n_low + 1, dtype=float)[1:]
|
|
1265
|
+
for value in low_values:
|
|
1266
|
+
candidate = _temperature_grid(np.append(candidate, float(value)))
|
|
1267
|
+
|
|
1268
|
+
candidate = np.asarray(
|
|
1269
|
+
[
|
|
1270
|
+
float(value)
|
|
1271
|
+
for value in _temperature_grid(candidate)
|
|
1272
|
+
if not _temperature_present(temps, float(value))
|
|
1273
|
+
],
|
|
1274
|
+
dtype=float,
|
|
1275
|
+
)
|
|
1276
|
+
return candidate if candidate.size > 0 else None
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def _adaptive_cold_boundary(
|
|
1280
|
+
*,
|
|
1281
|
+
current_low: float,
|
|
1282
|
+
target_low: float,
|
|
1283
|
+
current_probability: float,
|
|
1284
|
+
current_action: float,
|
|
1285
|
+
current_hubble: float,
|
|
1286
|
+
settings,
|
|
1287
|
+
last_temperature: float | None,
|
|
1288
|
+
last_probability: float | None,
|
|
1289
|
+
last_integral: float | None,
|
|
1290
|
+
last_log_gamma_h4: float | None,
|
|
1291
|
+
post_peak_streak: int,
|
|
1292
|
+
) -> tuple[float, float, float | None, int]:
|
|
1293
|
+
"""Choose the next cold boundary from the observed gain of the last batch.
|
|
1294
|
+
|
|
1295
|
+
The controller measures how much the previous cold-side extension increased
|
|
1296
|
+
``P(Tmin)`` (or, close to completion, ``I(Tmin) = -ln(1-P)``). Large gains
|
|
1297
|
+
imply the next step should shrink; tiny gains imply that the remaining cold
|
|
1298
|
+
tail must be explored with a larger logarithmic jump. Once
|
|
1299
|
+
``log(Gamma/H^4)`` is already falling on the cold side, the controller
|
|
1300
|
+
boosts the step size further because the rate has passed its peak.
|
|
1301
|
+
"""
|
|
1302
|
+
current = float(current_low)
|
|
1303
|
+
target = float(target_low)
|
|
1304
|
+
if np.isfinite(current_probability):
|
|
1305
|
+
p_clipped = min(max(float(current_probability), 0.0), 1.0 - 1e-15)
|
|
1306
|
+
current_integral = float(-math.log1p(-p_clipped)) if p_clipped > 0.0 else 0.0
|
|
1307
|
+
else:
|
|
1308
|
+
current_integral = 0.0
|
|
1309
|
+
current_log_gamma_h4_raw = _log_gamma_h4_array(
|
|
1310
|
+
np.asarray([current], dtype=float),
|
|
1311
|
+
np.asarray([current_action], dtype=float),
|
|
1312
|
+
np.asarray([current_hubble], dtype=float),
|
|
1313
|
+
)[0]
|
|
1314
|
+
current_log_gamma_h4 = float(current_log_gamma_h4_raw) if np.isfinite(current_log_gamma_h4_raw) else None
|
|
1315
|
+
|
|
1316
|
+
if not (np.isfinite(current) and np.isfinite(target) and current > 0.0 and target > 0.0 and target < current):
|
|
1317
|
+
return target, current_integral, current_log_gamma_h4, 0
|
|
1318
|
+
|
|
1319
|
+
remaining_log_span = math.log(current / target)
|
|
1320
|
+
dlnT_next = settings.rel_increment * remaining_log_span
|
|
1321
|
+
dlnT_next = min(max(dlnT_next, _COLD_TAIL_MIN_LOG_STEP), _COLD_TAIL_MAX_LOG_STEP)
|
|
1322
|
+
|
|
1323
|
+
updated_streak = 0
|
|
1324
|
+
last_temp_finite = bool(last_temperature is not None and np.isfinite(last_temperature) and float(last_temperature) > current)
|
|
1325
|
+
if last_temp_finite:
|
|
1326
|
+
previous_log_step = math.log(float(last_temperature) / current)
|
|
1327
|
+
if previous_log_step > 0.0:
|
|
1328
|
+
if (
|
|
1329
|
+
current_log_gamma_h4 is not None
|
|
1330
|
+
and last_log_gamma_h4 is not None
|
|
1331
|
+
and np.isfinite(last_log_gamma_h4)
|
|
1332
|
+
):
|
|
1333
|
+
if current_log_gamma_h4 < float(last_log_gamma_h4):
|
|
1334
|
+
updated_streak = int(post_peak_streak) + 1
|
|
1335
|
+
else:
|
|
1336
|
+
updated_streak = 0
|
|
1337
|
+
else:
|
|
1338
|
+
updated_streak = 0
|
|
1339
|
+
|
|
1340
|
+
if current_probability >= _COLD_TAIL_INTEGRAL_SWITCH_P:
|
|
1341
|
+
previous_metric = 0.0 if last_integral is None or not np.isfinite(last_integral) else float(last_integral)
|
|
1342
|
+
metric_gain = current_integral - previous_metric
|
|
1343
|
+
target_gain = _COLD_TAIL_TARGET_DI
|
|
1344
|
+
else:
|
|
1345
|
+
previous_metric = 0.0 if last_probability is None or not np.isfinite(last_probability) else float(last_probability)
|
|
1346
|
+
metric_gain = float(current_probability) - previous_metric
|
|
1347
|
+
target_gain = _COLD_TAIL_TARGET_DP
|
|
1348
|
+
|
|
1349
|
+
if not np.isfinite(metric_gain) or metric_gain <= 1e-8:
|
|
1350
|
+
# If the previous cold-side extension produced essentially no
|
|
1351
|
+
# gain in P(Tmin), do not keep exploring decade-by-decade. Jump
|
|
1352
|
+
# by at least two decades when possible; later remeshing is much
|
|
1353
|
+
# cheaper than repeatedly adding almost equivalent cold points.
|
|
1354
|
+
dlnT_next = max(
|
|
1355
|
+
previous_log_step * _COLD_TAIL_ZERO_GAIN_SCALE,
|
|
1356
|
+
math.log(100.0),
|
|
1357
|
+
)
|
|
1358
|
+
else:
|
|
1359
|
+
dlnT_next = previous_log_step * target_gain / metric_gain
|
|
1360
|
+
|
|
1361
|
+
dlnT_next = min(
|
|
1362
|
+
max(dlnT_next, previous_log_step * _COLD_TAIL_STEP_SHRINK_LIMIT),
|
|
1363
|
+
previous_log_step * _COLD_TAIL_STEP_GROWTH_LIMIT,
|
|
1364
|
+
)
|
|
1365
|
+
dlnT_next = min(max(dlnT_next, _COLD_TAIL_MIN_LOG_STEP), _COLD_TAIL_MAX_LOG_STEP)
|
|
1366
|
+
|
|
1367
|
+
if (
|
|
1368
|
+
updated_streak >= _COLD_TAIL_POST_PEAK_MIN_STREAK
|
|
1369
|
+
and current_log_gamma_h4 is not None
|
|
1370
|
+
and last_log_gamma_h4 is not None
|
|
1371
|
+
and np.isfinite(last_log_gamma_h4)
|
|
1372
|
+
):
|
|
1373
|
+
slope = abs((current_log_gamma_h4 - float(last_log_gamma_h4)) / max(previous_log_step, 1e-12))
|
|
1374
|
+
boost = 1.0 + _COLD_TAIL_POST_PEAK_BOOST_COEFF * slope
|
|
1375
|
+
boost = min(boost, _COLD_TAIL_POST_PEAK_MAX_BOOST)
|
|
1376
|
+
dlnT_next *= boost
|
|
1377
|
+
dlnT_next = min(max(dlnT_next, _COLD_TAIL_MIN_LOG_STEP), _COLD_TAIL_MAX_LOG_STEP)
|
|
1378
|
+
|
|
1379
|
+
dlnT_next = min(dlnT_next, remaining_log_span)
|
|
1380
|
+
if dlnT_next <= 0.0:
|
|
1381
|
+
return target, current_integral, current_log_gamma_h4, updated_streak
|
|
1382
|
+
|
|
1383
|
+
if dlnT_next >= remaining_log_span * (1.0 - 1e-12):
|
|
1384
|
+
return target, current_integral, current_log_gamma_h4, updated_streak
|
|
1385
|
+
return float(current * math.exp(-dlnT_next)), current_integral, current_log_gamma_h4, updated_streak
|