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,1049 @@
|
|
|
1
|
+
"""Rate-peak and hot-onset helpers for adaptive_step_size percolation.
|
|
2
|
+
|
|
3
|
+
Part of TransitionListener v2.0
|
|
4
|
+
Documentation: https://tasillo.de/TransitionListener/
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Jonas Matuszak <jonas.matuszak@kit.edu>
|
|
8
|
+
Carlo Tasillo <carlo.tasillo@ific.uv.es>
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from scipy import interpolate
|
|
15
|
+
from scipy import optimize
|
|
16
|
+
|
|
17
|
+
from transitionlistener.bubbledynamics import _temperature_grid
|
|
18
|
+
|
|
19
|
+
HOT_RATE_REFINE_LOG = 1.0
|
|
20
|
+
HOT_RATE_ACCEPT_LOG = 0.0
|
|
21
|
+
HOT_RATE_ANCHORS = (3.0, 2.0, 1.0, 0.0)
|
|
22
|
+
HOT_PROBABILITY_ANCHORS = (1.0e-3, 3.0e-3, 1.0e-2)
|
|
23
|
+
HOT_SHOULDER_LOG10_RATE_THRESHOLD = 1.0
|
|
24
|
+
HOT_SHOULDER_MAX_LOG_STEP = 0.3
|
|
25
|
+
HOT_SHOULDER_ONSET_P_ZERO = 1.0e-8
|
|
26
|
+
HOT_SHOULDER_ONSET_MAX_LOG_STEP = 0.05
|
|
27
|
+
|
|
28
|
+
RATE_PEAK_REFINE_FLOOR_LOG10 = -10.0
|
|
29
|
+
RATE_PEAK_ONRAMP_FLOOR_LOG10 = -18.0
|
|
30
|
+
RATE_PEAK_ONRAMP_MIN_GAIN_LOG10 = 8.0
|
|
31
|
+
RATE_PEAK_ONRAMP_MIN_LOG_WIDTH = 0.12
|
|
32
|
+
SUBCRITICAL_RATE_PEAK_MIN_PROMINENCE = 1.0
|
|
33
|
+
RATE_PEAK_INERT_MIN_PEAK_LOG10 = -3.0
|
|
34
|
+
RATE_PEAK_INERT_LOG10 = -10.0
|
|
35
|
+
RATE_PEAK_INERT_MAX_INTERVAL_DI = 1.0e-4
|
|
36
|
+
RATE_PEAK_INERT_MIN_MONOTONE_POINTS = 2
|
|
37
|
+
RATE_PEAK_INERT_MONOTONE_TOLERANCE_LOG10 = 0.25
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _temperature_midpoint(high: float, low: float) -> float | None:
|
|
41
|
+
"""Return a geometric or arithmetic midpoint suited to logarithmic temperature scans."""
|
|
42
|
+
high = float(high)
|
|
43
|
+
low = float(low)
|
|
44
|
+
if not (np.isfinite(high) and np.isfinite(low)) or high <= low:
|
|
45
|
+
return None
|
|
46
|
+
if high > 0.0 and low > 0.0 and high / low >= 2.0:
|
|
47
|
+
return float(np.sqrt(high * low))
|
|
48
|
+
return 0.5 * (high + low)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _temperature_tolerance(
|
|
52
|
+
values: np.ndarray | list[float] | tuple[float, ...] | None,
|
|
53
|
+
candidate: float,
|
|
54
|
+
) -> float:
|
|
55
|
+
"""Return the absolute tolerance used when deduplicating temperature samples."""
|
|
56
|
+
candidate = float(candidate)
|
|
57
|
+
values_arr = np.asarray([] if values is None else values, dtype=float)
|
|
58
|
+
scale = max(
|
|
59
|
+
abs(candidate),
|
|
60
|
+
np.max(np.abs(values_arr)) if values_arr.size else 0.0,
|
|
61
|
+
1.0,
|
|
62
|
+
)
|
|
63
|
+
return 1e-6 * scale
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _temperature_present(
|
|
67
|
+
values: np.ndarray | list[float] | tuple[float, ...] | None,
|
|
68
|
+
candidate: float,
|
|
69
|
+
) -> bool:
|
|
70
|
+
"""Return whether ``candidate`` is already represented inside ``values``."""
|
|
71
|
+
values_arr = np.asarray([] if values is None else values, dtype=float)
|
|
72
|
+
if values_arr.size == 0:
|
|
73
|
+
return False
|
|
74
|
+
return bool(np.any(np.isclose(values_arr, float(candidate), atol=_temperature_tolerance(values_arr, candidate), rtol=0.0)))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _find_value_crossing(
|
|
78
|
+
temperatures: np.ndarray,
|
|
79
|
+
values: np.ndarray,
|
|
80
|
+
target: float,
|
|
81
|
+
) -> tuple[float | None, float | None, int | None]:
|
|
82
|
+
"""Locate the first linearised crossing of ``values`` through ``target``."""
|
|
83
|
+
temps = np.asarray(temperatures, dtype=float)
|
|
84
|
+
sampled = np.asarray(values, dtype=float)
|
|
85
|
+
if temps.size != sampled.size or temps.size < 2:
|
|
86
|
+
return None, None, None
|
|
87
|
+
|
|
88
|
+
bracket_index = None
|
|
89
|
+
for index in range(sampled.size - 1):
|
|
90
|
+
value_high = float(sampled[index])
|
|
91
|
+
value_low = float(sampled[index + 1])
|
|
92
|
+
if not np.isfinite(value_high) or not np.isfinite(value_low):
|
|
93
|
+
continue
|
|
94
|
+
if value_high == target or value_low == target or (
|
|
95
|
+
value_high < target < value_low or value_low < target < value_high
|
|
96
|
+
):
|
|
97
|
+
bracket_index = index
|
|
98
|
+
break
|
|
99
|
+
if bracket_index is None:
|
|
100
|
+
return None, None, None
|
|
101
|
+
|
|
102
|
+
i_hi = int(bracket_index)
|
|
103
|
+
i_lo = min(i_hi + 1, len(temps) - 1)
|
|
104
|
+
T_hi = float(temps[i_hi])
|
|
105
|
+
T_lo = float(temps[i_lo])
|
|
106
|
+
V_hi = float(sampled[i_hi])
|
|
107
|
+
V_lo = float(sampled[i_lo])
|
|
108
|
+
if not (np.isfinite(T_hi) and np.isfinite(T_lo) and np.isfinite(V_hi) and np.isfinite(V_lo)):
|
|
109
|
+
return None, None, None
|
|
110
|
+
if abs(V_lo - V_hi) < 1e-15:
|
|
111
|
+
return 0.5 * (T_hi + T_lo), abs(T_hi - T_lo), i_hi
|
|
112
|
+
crossing = T_hi + (float(target) - V_hi) / (V_lo - V_hi) * (T_lo - T_hi)
|
|
113
|
+
return float(crossing), abs(T_hi - T_lo), i_hi
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _probability_at(temperature: float, temperatures: np.ndarray, probabilities: np.ndarray) -> float | None:
|
|
117
|
+
"""Interpolate the false-vacuum probability profile at one temperature."""
|
|
118
|
+
temps = np.asarray(temperatures, dtype=float)
|
|
119
|
+
probs = np.asarray(probabilities, dtype=float)
|
|
120
|
+
finite = np.isfinite(temps) & np.isfinite(probs)
|
|
121
|
+
temps = temps[finite]
|
|
122
|
+
probs = probs[finite]
|
|
123
|
+
if temps.size < 2:
|
|
124
|
+
return None
|
|
125
|
+
temperature = float(temperature)
|
|
126
|
+
scale = max(np.max(np.abs(temps)), abs(temperature), 1.0)
|
|
127
|
+
matches = np.flatnonzero(np.isclose(temps, temperature, atol=1e-10 * scale, rtol=0.0))
|
|
128
|
+
if matches.size:
|
|
129
|
+
value = float(probs[int(matches[0])])
|
|
130
|
+
return value if np.isfinite(value) else None
|
|
131
|
+
order = np.argsort(temps)
|
|
132
|
+
temps = temps[order]
|
|
133
|
+
probs = probs[order]
|
|
134
|
+
if np.any(np.diff(temps) <= 0.0):
|
|
135
|
+
return None
|
|
136
|
+
value = float(np.interp(temperature, temps, probs, left=np.nan, right=np.nan))
|
|
137
|
+
if not np.isfinite(value):
|
|
138
|
+
return None
|
|
139
|
+
return float(min(max(value, 0.0), 1.0))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _remesh_dp_limit(probability_high: float, probability_low: float, settings) -> float:
|
|
143
|
+
"""Choose the allowed probability jump for a remeshed interval based on its regime."""
|
|
144
|
+
generic = max(float(settings.large_delta_p_refine_threshold), 0.0)
|
|
145
|
+
strict = 0.05
|
|
146
|
+
p_min = min(float(probability_high), float(probability_low))
|
|
147
|
+
p_max = max(float(probability_high), float(probability_low))
|
|
148
|
+
if p_max <= 0.2 or p_min < 0.2:
|
|
149
|
+
return strict
|
|
150
|
+
if p_min >= 0.8 or p_max > 0.8:
|
|
151
|
+
return min(generic, strict) if generic > 0.0 else strict
|
|
152
|
+
return generic
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _log_gamma_h4_array(
|
|
156
|
+
temperatures: np.ndarray,
|
|
157
|
+
actions: np.ndarray,
|
|
158
|
+
hubble: np.ndarray,
|
|
159
|
+
) -> np.ndarray:
|
|
160
|
+
"""Vectorized ``log(Gamma/H^4)`` for support arrays."""
|
|
161
|
+
|
|
162
|
+
from transitionlistener import bubbledynamics as bd
|
|
163
|
+
|
|
164
|
+
temps = np.asarray(temperatures, dtype=float)
|
|
165
|
+
acts = np.asarray(actions, dtype=float)
|
|
166
|
+
hub = np.asarray(hubble, dtype=float)
|
|
167
|
+
result = np.full(temps.shape, np.nan, dtype=float)
|
|
168
|
+
valid = np.isfinite(temps) & np.isfinite(acts) & np.isfinite(hub) & (temps > 0.0) & (hub > 0.0)
|
|
169
|
+
if np.any(valid):
|
|
170
|
+
result[valid] = np.asarray(bd.logGamma(temps[valid], acts[valid]), dtype=float) - 4.0 * np.log(hub[valid])
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _log10_gamma_h4_array(
|
|
175
|
+
temperatures: np.ndarray,
|
|
176
|
+
actions: np.ndarray,
|
|
177
|
+
hubble: np.ndarray,
|
|
178
|
+
) -> np.ndarray:
|
|
179
|
+
"""Vectorized ``log10(Gamma/H^4)`` for support arrays."""
|
|
180
|
+
|
|
181
|
+
return _log_gamma_h4_array(temperatures, actions, hubble) / math.log(10.0)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _refine_step1_rate_peak(
|
|
185
|
+
temperatures: np.ndarray,
|
|
186
|
+
log_gamma_h4_sym: np.ndarray,
|
|
187
|
+
) -> tuple[float | None, float | None, int | None]:
|
|
188
|
+
"""Return one interior peak of log(Gamma/H^4_sym), refined from cached scout points."""
|
|
189
|
+
|
|
190
|
+
temps = np.asarray(temperatures, dtype=float)
|
|
191
|
+
log_rate = np.asarray(log_gamma_h4_sym, dtype=float)
|
|
192
|
+
if temps.size != log_rate.size or temps.size < 3:
|
|
193
|
+
return None, None, None
|
|
194
|
+
|
|
195
|
+
peak_index = int(np.nanargmax(log_rate))
|
|
196
|
+
if peak_index <= 0 or peak_index >= temps.size - 1:
|
|
197
|
+
return None, None, None
|
|
198
|
+
|
|
199
|
+
peak_value = float(log_rate[peak_index])
|
|
200
|
+
left_neighbor = float(log_rate[peak_index - 1])
|
|
201
|
+
right_neighbor = float(log_rate[peak_index + 1])
|
|
202
|
+
boundary_prominence = peak_value - max(float(log_rate[0]), float(log_rate[-1]))
|
|
203
|
+
if not (peak_value > left_neighbor and peak_value > right_neighbor and boundary_prominence > 1.0):
|
|
204
|
+
return None, None, None
|
|
205
|
+
|
|
206
|
+
x = np.asarray(temps[::-1], dtype=float)
|
|
207
|
+
y = np.asarray(log_rate[::-1], dtype=float)
|
|
208
|
+
interpolant = interpolate.PchipInterpolator(x, y, extrapolate=False)
|
|
209
|
+
lower = float(temps[peak_index + 1])
|
|
210
|
+
upper = float(temps[peak_index - 1])
|
|
211
|
+
if not (np.isfinite(lower) and np.isfinite(upper) and lower < upper):
|
|
212
|
+
return float(temps[peak_index]), peak_value, peak_index
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
optimum = optimize.minimize_scalar(
|
|
216
|
+
lambda value: -float(interpolant(float(value))),
|
|
217
|
+
bounds=(lower, upper),
|
|
218
|
+
method="bounded",
|
|
219
|
+
options={"xatol": max(1e-10 * max(abs(lower), abs(upper), 1.0), 1e-14)},
|
|
220
|
+
)
|
|
221
|
+
except Exception:
|
|
222
|
+
optimum = None
|
|
223
|
+
|
|
224
|
+
if optimum is None or not bool(getattr(optimum, "success", False)):
|
|
225
|
+
return float(temps[peak_index]), peak_value, peak_index
|
|
226
|
+
|
|
227
|
+
peak_temperature = float(optimum.x)
|
|
228
|
+
peak_log_rate = float(interpolant(peak_temperature))
|
|
229
|
+
if not (np.isfinite(peak_temperature) and np.isfinite(peak_log_rate)):
|
|
230
|
+
return float(temps[peak_index]), peak_value, peak_index
|
|
231
|
+
return peak_temperature, peak_log_rate, peak_index
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _build_peak_centered_step1_window(
|
|
235
|
+
temperatures: np.ndarray,
|
|
236
|
+
log_gamma_h4_sym: np.ndarray,
|
|
237
|
+
settings,
|
|
238
|
+
) -> tuple[float | None, float | None, float | None, float | None]:
|
|
239
|
+
"""Build a startup window around the interior rate peak of a U-shaped action."""
|
|
240
|
+
|
|
241
|
+
temps = np.asarray(temperatures, dtype=float)
|
|
242
|
+
log_rate = np.asarray(log_gamma_h4_sym, dtype=float)
|
|
243
|
+
peak_temperature, peak_log_rate, peak_index = _refine_step1_rate_peak(temps, log_rate)
|
|
244
|
+
if peak_temperature is None or peak_log_rate is None or peak_index is None:
|
|
245
|
+
return None, None, None, None
|
|
246
|
+
|
|
247
|
+
drop = max(4.0, -math.log(max(float(settings.f_start), 1e-12)))
|
|
248
|
+
threshold = float(peak_log_rate) - float(drop)
|
|
249
|
+
|
|
250
|
+
start_index = int(peak_index)
|
|
251
|
+
while start_index > 0 and float(log_rate[start_index - 1]) >= threshold:
|
|
252
|
+
start_index -= 1
|
|
253
|
+
end_index = int(peak_index)
|
|
254
|
+
while end_index < len(log_rate) - 1 and float(log_rate[end_index + 1]) >= threshold:
|
|
255
|
+
end_index += 1
|
|
256
|
+
|
|
257
|
+
window_high = float(temps[start_index])
|
|
258
|
+
window_low = float(temps[end_index])
|
|
259
|
+
|
|
260
|
+
if start_index > 0:
|
|
261
|
+
T_hi = float(temps[start_index - 1])
|
|
262
|
+
T_lo = float(temps[start_index])
|
|
263
|
+
y_hi = float(log_rate[start_index - 1])
|
|
264
|
+
y_lo = float(log_rate[start_index])
|
|
265
|
+
if np.isfinite(y_hi) and np.isfinite(y_lo) and abs(y_lo - y_hi) > 1e-15:
|
|
266
|
+
frac = (threshold - y_hi) / (y_lo - y_hi)
|
|
267
|
+
if 0.0 <= frac <= 1.0:
|
|
268
|
+
window_high = float(T_hi + frac * (T_lo - T_hi))
|
|
269
|
+
|
|
270
|
+
if end_index < len(log_rate) - 1:
|
|
271
|
+
T_hi = float(temps[end_index])
|
|
272
|
+
T_lo = float(temps[end_index + 1])
|
|
273
|
+
y_hi = float(log_rate[end_index])
|
|
274
|
+
y_lo = float(log_rate[end_index + 1])
|
|
275
|
+
if np.isfinite(y_hi) and np.isfinite(y_lo) and abs(y_lo - y_hi) > 1e-15:
|
|
276
|
+
frac = (threshold - y_hi) / (y_lo - y_hi)
|
|
277
|
+
if 0.0 <= frac <= 1.0:
|
|
278
|
+
window_low = float(T_hi + frac * (T_lo - T_hi))
|
|
279
|
+
|
|
280
|
+
if not (np.isfinite(window_high) and np.isfinite(window_low) and window_high > window_low):
|
|
281
|
+
return None, None, None, None
|
|
282
|
+
return float(peak_temperature), float(peak_log_rate), float(window_high), float(window_low)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _build_log_temperature_bridge_grid(
|
|
286
|
+
hot_temperature: float,
|
|
287
|
+
cold_temperature: float,
|
|
288
|
+
*,
|
|
289
|
+
max_log_step: float,
|
|
290
|
+
max_new_points: int,
|
|
291
|
+
) -> np.ndarray | None:
|
|
292
|
+
"""Return geometric bridge points so the interval obeys one ``Delta ln T`` limit."""
|
|
293
|
+
|
|
294
|
+
hot_temperature = float(hot_temperature)
|
|
295
|
+
cold_temperature = float(cold_temperature)
|
|
296
|
+
if (
|
|
297
|
+
max_new_points <= 0
|
|
298
|
+
or not np.isfinite(hot_temperature)
|
|
299
|
+
or not np.isfinite(cold_temperature)
|
|
300
|
+
or hot_temperature <= cold_temperature
|
|
301
|
+
or cold_temperature <= 0.0
|
|
302
|
+
or hot_temperature <= 0.0
|
|
303
|
+
):
|
|
304
|
+
return None
|
|
305
|
+
|
|
306
|
+
dln_t = float(math.log(hot_temperature / cold_temperature))
|
|
307
|
+
if not np.isfinite(dln_t) or dln_t <= float(max_log_step):
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
n_segments = max(int(math.ceil(dln_t / max(float(max_log_step), 1.0e-6))), 2)
|
|
311
|
+
n_points = min(max(int(n_segments - 1), 1), int(max_new_points))
|
|
312
|
+
bridge = np.geomspace(hot_temperature, cold_temperature, n_points + 2, dtype=float)[1:-1]
|
|
313
|
+
bridge = _temperature_grid(bridge)
|
|
314
|
+
return bridge if bridge.size > 0 else None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _build_dynamiczoomwindow_hot_shoulder_grid(
|
|
318
|
+
temperatures: np.ndarray,
|
|
319
|
+
probabilities: np.ndarray,
|
|
320
|
+
actions: np.ndarray,
|
|
321
|
+
hubble: np.ndarray,
|
|
322
|
+
settings,
|
|
323
|
+
*,
|
|
324
|
+
max_new_points: int,
|
|
325
|
+
) -> tuple[np.ndarray | None, str | None]:
|
|
326
|
+
"""Resolve low-P hot-shoulder gaps where the local rate is already active."""
|
|
327
|
+
|
|
328
|
+
if max_new_points <= 0:
|
|
329
|
+
return None, None
|
|
330
|
+
|
|
331
|
+
temps = np.asarray(temperatures, dtype=float)
|
|
332
|
+
probs = np.asarray(probabilities, dtype=float)
|
|
333
|
+
acts = np.asarray(actions, dtype=float)
|
|
334
|
+
hub = np.asarray(hubble, dtype=float)
|
|
335
|
+
if not (temps.size >= 2 and temps.size == probs.size == acts.size == hub.size):
|
|
336
|
+
return None, None
|
|
337
|
+
order = np.argsort(-temps)
|
|
338
|
+
temps = temps[order]
|
|
339
|
+
probs = probs[order]
|
|
340
|
+
acts = acts[order]
|
|
341
|
+
hub = hub[order]
|
|
342
|
+
|
|
343
|
+
p_upper = max(max(HOT_PROBABILITY_ANCHORS), float(settings.f_start))
|
|
344
|
+
rate_threshold = HOT_SHOULDER_LOG10_RATE_THRESHOLD
|
|
345
|
+
max_log_step = HOT_SHOULDER_MAX_LOG_STEP
|
|
346
|
+
onset_max_log_step = HOT_SHOULDER_ONSET_MAX_LOG_STEP
|
|
347
|
+
onset_p_zero = HOT_SHOULDER_ONSET_P_ZERO
|
|
348
|
+
best_interval: tuple[float, float, float, float, bool] | None = None
|
|
349
|
+
log10_rate = _log10_gamma_h4_array(temps, acts, hub)
|
|
350
|
+
|
|
351
|
+
for i in range(temps.size - 1):
|
|
352
|
+
hot_temperature = float(temps[i])
|
|
353
|
+
cold_temperature = float(temps[i + 1])
|
|
354
|
+
hot_probability = float(probs[i])
|
|
355
|
+
cold_probability = float(probs[i + 1])
|
|
356
|
+
if max(hot_probability, cold_probability) > p_upper + 1.0e-12:
|
|
357
|
+
continue
|
|
358
|
+
if hot_temperature <= 0.0 or cold_temperature <= 0.0:
|
|
359
|
+
continue
|
|
360
|
+
dln_t = float(math.log(hot_temperature / cold_temperature))
|
|
361
|
+
if not np.isfinite(dln_t):
|
|
362
|
+
continue
|
|
363
|
+
is_onset_crossing = hot_probability < onset_p_zero and cold_probability > onset_p_zero
|
|
364
|
+
effective_threshold = onset_max_log_step if is_onset_crossing else max_log_step
|
|
365
|
+
if dln_t <= effective_threshold:
|
|
366
|
+
continue
|
|
367
|
+
endpoint_rates = log10_rate[[i, i + 1]]
|
|
368
|
+
finite_endpoint_rates = endpoint_rates[np.isfinite(endpoint_rates)]
|
|
369
|
+
rate_level = float(np.max(finite_endpoint_rates)) if finite_endpoint_rates.size else -np.inf
|
|
370
|
+
rate_active = np.isfinite(rate_level) and rate_level > rate_threshold
|
|
371
|
+
onset_buried = is_onset_crossing or (
|
|
372
|
+
hot_probability < onset_p_zero
|
|
373
|
+
and cold_probability < onset_p_zero
|
|
374
|
+
and (i + 2) < temps.size
|
|
375
|
+
and float(probs[i + 2]) > onset_p_zero
|
|
376
|
+
)
|
|
377
|
+
if not (rate_active or onset_buried):
|
|
378
|
+
continue
|
|
379
|
+
if best_interval is None or dln_t > best_interval[3]:
|
|
380
|
+
best_interval = (hot_temperature, cold_temperature, rate_level, dln_t, is_onset_crossing)
|
|
381
|
+
|
|
382
|
+
if best_interval is None:
|
|
383
|
+
return None, None
|
|
384
|
+
|
|
385
|
+
hot_temperature, cold_temperature, rate_level, dln_t, is_onset_crossing = best_interval
|
|
386
|
+
bridge_max_log_step = onset_max_log_step if is_onset_crossing else max_log_step
|
|
387
|
+
candidate = _build_log_temperature_bridge_grid(
|
|
388
|
+
hot_temperature,
|
|
389
|
+
cold_temperature,
|
|
390
|
+
max_log_step=bridge_max_log_step,
|
|
391
|
+
max_new_points=int(max_new_points),
|
|
392
|
+
)
|
|
393
|
+
if candidate is None or candidate.size == 0:
|
|
394
|
+
return None, None
|
|
395
|
+
mode_tag = "onset_crossing" if is_onset_crossing else "low-P"
|
|
396
|
+
reason = (
|
|
397
|
+
f"hot_shoulder_refine from {mode_tag} interval "
|
|
398
|
+
f"T={hot_temperature:2.5g}->{cold_temperature:2.5g}, "
|
|
399
|
+
f"dlnT={dln_t:2.3g}, max log10(Gamma/H^4)={rate_level:2.3g}"
|
|
400
|
+
)
|
|
401
|
+
return candidate, reason
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _build_dynamiczoomwindow_rate_peak_grid(
|
|
405
|
+
temperatures: np.ndarray,
|
|
406
|
+
actions: np.ndarray,
|
|
407
|
+
hubble: np.ndarray,
|
|
408
|
+
settings,
|
|
409
|
+
*,
|
|
410
|
+
max_new_points: int,
|
|
411
|
+
) -> tuple[np.ndarray | None, str | None]:
|
|
412
|
+
"""Refine around a sampled local maximum or unresolved on-ramp of ``Gamma/H^4``."""
|
|
413
|
+
|
|
414
|
+
if max_new_points <= 0:
|
|
415
|
+
return None, None
|
|
416
|
+
|
|
417
|
+
temps = np.asarray(temperatures, dtype=float)
|
|
418
|
+
acts = np.asarray(actions, dtype=float)
|
|
419
|
+
hub = np.asarray(hubble, dtype=float)
|
|
420
|
+
if not (temps.size >= 3 and temps.size == acts.size == hub.size):
|
|
421
|
+
return None, None
|
|
422
|
+
|
|
423
|
+
order = np.argsort(-temps)
|
|
424
|
+
temps = temps[order]
|
|
425
|
+
acts = acts[order]
|
|
426
|
+
hub = hub[order]
|
|
427
|
+
log10_rate = _log10_gamma_h4_array(temps, acts, hub)
|
|
428
|
+
finite = np.isfinite(temps) & np.isfinite(log10_rate) & (temps > 0.0)
|
|
429
|
+
if np.count_nonzero(finite) < 2:
|
|
430
|
+
return None, None
|
|
431
|
+
|
|
432
|
+
candidate = np.asarray([], dtype=float)
|
|
433
|
+
occupied = _temperature_grid(temps)
|
|
434
|
+
|
|
435
|
+
def add_candidate(value: float | None) -> bool:
|
|
436
|
+
nonlocal candidate
|
|
437
|
+
if value is None:
|
|
438
|
+
return False
|
|
439
|
+
try:
|
|
440
|
+
numeric = float(value)
|
|
441
|
+
except Exception:
|
|
442
|
+
return False
|
|
443
|
+
if not np.isfinite(numeric):
|
|
444
|
+
return False
|
|
445
|
+
if _temperature_present(occupied, numeric) or _temperature_present(candidate, numeric):
|
|
446
|
+
return False
|
|
447
|
+
candidate = _temperature_grid(np.append(candidate, numeric))
|
|
448
|
+
return True
|
|
449
|
+
|
|
450
|
+
def add_interval_points(high: float, low: float, budget: int) -> None:
|
|
451
|
+
if budget <= 0:
|
|
452
|
+
return
|
|
453
|
+
high = float(high)
|
|
454
|
+
low = float(low)
|
|
455
|
+
if not (np.isfinite(high) and np.isfinite(low) and high > low and low > 0.0):
|
|
456
|
+
return
|
|
457
|
+
if high / low >= 1.25:
|
|
458
|
+
points = np.geomspace(high, low, int(budget) + 2, dtype=float)[1:-1]
|
|
459
|
+
else:
|
|
460
|
+
points = np.linspace(high, low, int(budget) + 2, dtype=float)[1:-1]
|
|
461
|
+
for point in points:
|
|
462
|
+
add_candidate(float(point))
|
|
463
|
+
|
|
464
|
+
def build_rate_onramp_candidate() -> tuple[np.ndarray | None, str | None]:
|
|
465
|
+
nonlocal candidate
|
|
466
|
+
candidate = np.asarray([], dtype=float)
|
|
467
|
+
rate_floor = RATE_PEAK_REFINE_FLOOR_LOG10
|
|
468
|
+
onramp_floor = RATE_PEAK_ONRAMP_FLOOR_LOG10
|
|
469
|
+
min_rate_gain = RATE_PEAK_ONRAMP_MIN_GAIN_LOG10
|
|
470
|
+
min_log_width = RATE_PEAK_ONRAMP_MIN_LOG_WIDTH
|
|
471
|
+
|
|
472
|
+
best_interval: tuple[float, int, float, float, float, float] | None = None
|
|
473
|
+
for index in range(temps.size - 1):
|
|
474
|
+
if not (finite[index] and finite[index + 1]):
|
|
475
|
+
continue
|
|
476
|
+
hot_temperature = float(temps[index])
|
|
477
|
+
cold_temperature = float(temps[index + 1])
|
|
478
|
+
if not (hot_temperature > cold_temperature > 0.0):
|
|
479
|
+
continue
|
|
480
|
+
hot_log_rate = float(log10_rate[index])
|
|
481
|
+
cold_log_rate = float(log10_rate[index + 1])
|
|
482
|
+
rate_gain = cold_log_rate - hot_log_rate
|
|
483
|
+
log_width = float(math.log(hot_temperature / cold_temperature))
|
|
484
|
+
if not (np.isfinite(rate_gain) and np.isfinite(log_width)):
|
|
485
|
+
continue
|
|
486
|
+
if rate_gain < min_rate_gain or log_width < min_log_width:
|
|
487
|
+
continue
|
|
488
|
+
if cold_log_rate < onramp_floor and max(hot_log_rate, cold_log_rate) < rate_floor:
|
|
489
|
+
continue
|
|
490
|
+
|
|
491
|
+
score = log_width * max(rate_gain, 0.0) * (1.0 + max(cold_log_rate - onramp_floor, 0.0))
|
|
492
|
+
if best_interval is None or score > best_interval[0]:
|
|
493
|
+
best_interval = (score, index, hot_temperature, cold_temperature, hot_log_rate, cold_log_rate)
|
|
494
|
+
|
|
495
|
+
if best_interval is None:
|
|
496
|
+
return None, None
|
|
497
|
+
|
|
498
|
+
_, _, hot_temperature, cold_temperature, hot_log_rate, cold_log_rate = best_interval
|
|
499
|
+
for target in (rate_floor, -5.0, 0.0):
|
|
500
|
+
if candidate.size >= int(max_new_points):
|
|
501
|
+
break
|
|
502
|
+
if min(hot_log_rate, cold_log_rate) <= float(target) <= max(hot_log_rate, cold_log_rate):
|
|
503
|
+
if abs(cold_log_rate - hot_log_rate) > 1.0e-15:
|
|
504
|
+
frac = (float(target) - hot_log_rate) / (cold_log_rate - hot_log_rate)
|
|
505
|
+
log_hot = math.log(hot_temperature)
|
|
506
|
+
log_cold = math.log(cold_temperature)
|
|
507
|
+
add_candidate(float(math.exp(log_hot + frac * (log_cold - log_hot))))
|
|
508
|
+
|
|
509
|
+
if candidate.size < int(max_new_points):
|
|
510
|
+
add_interval_points(
|
|
511
|
+
hot_temperature,
|
|
512
|
+
cold_temperature,
|
|
513
|
+
int(max_new_points) - int(candidate.size),
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
if candidate.size == 0:
|
|
517
|
+
return None, None
|
|
518
|
+
reason = (
|
|
519
|
+
"rate_peak_refine on unresolved on-ramp "
|
|
520
|
+
f"T={hot_temperature:2.5g}->{cold_temperature:2.5g}, "
|
|
521
|
+
f"log10(Gamma/H^4)={hot_log_rate:2.3g}->{cold_log_rate:2.3g}"
|
|
522
|
+
)
|
|
523
|
+
return candidate[: int(max_new_points)], reason
|
|
524
|
+
|
|
525
|
+
def add_rate_crossings(target: float) -> None:
|
|
526
|
+
for index in range(temps.size - 1):
|
|
527
|
+
left = float(log10_rate[index])
|
|
528
|
+
right = float(log10_rate[index + 1])
|
|
529
|
+
if not (np.isfinite(left) and np.isfinite(right)):
|
|
530
|
+
continue
|
|
531
|
+
if (left - target) * (right - target) > 0.0:
|
|
532
|
+
continue
|
|
533
|
+
if abs(right - left) < 1.0e-15:
|
|
534
|
+
add_candidate(0.5 * (float(temps[index]) + float(temps[index + 1])))
|
|
535
|
+
continue
|
|
536
|
+
frac = (float(target) - left) / (right - left)
|
|
537
|
+
if 0.0 <= frac <= 1.0:
|
|
538
|
+
add_candidate(float(temps[index]) + frac * (float(temps[index + 1]) - float(temps[index])))
|
|
539
|
+
|
|
540
|
+
def add_largest_rate_band_gaps(max_points: int) -> None:
|
|
541
|
+
if max_points <= 0:
|
|
542
|
+
return
|
|
543
|
+
rate_floor = RATE_PEAK_REFINE_FLOOR_LOG10
|
|
544
|
+
band_points: list[float] = []
|
|
545
|
+
for temp, rate in zip(temps, log10_rate):
|
|
546
|
+
if np.isfinite(rate) and float(rate) >= rate_floor:
|
|
547
|
+
band_points.append(float(temp))
|
|
548
|
+
band_points.extend(float(value) for value in candidate.tolist())
|
|
549
|
+
band_points = [
|
|
550
|
+
float(value)
|
|
551
|
+
for value in _temperature_grid(band_points)
|
|
552
|
+
if np.isfinite(float(value)) and float(value) > 0.0
|
|
553
|
+
]
|
|
554
|
+
if len(band_points) < 2:
|
|
555
|
+
return
|
|
556
|
+
for _ in range(int(max_points)):
|
|
557
|
+
points = _temperature_grid(band_points)
|
|
558
|
+
gaps: list[tuple[float, float, float]] = []
|
|
559
|
+
for high, low in zip(points[:-1], points[1:]):
|
|
560
|
+
if float(high) <= float(low) or float(low) <= 0.0:
|
|
561
|
+
continue
|
|
562
|
+
gaps.append((float(math.log(float(high) / float(low))), float(high), float(low)))
|
|
563
|
+
if not gaps:
|
|
564
|
+
return
|
|
565
|
+
_, high, low = max(gaps, key=lambda item: item[0])
|
|
566
|
+
new_point = _temperature_midpoint(high, low)
|
|
567
|
+
if not add_candidate(new_point):
|
|
568
|
+
return
|
|
569
|
+
band_points.append(float(new_point))
|
|
570
|
+
|
|
571
|
+
budget = max(int(max_new_points), 1)
|
|
572
|
+
rate_floor = RATE_PEAK_REFINE_FLOOR_LOG10
|
|
573
|
+
onramp_grid, onramp_reason = build_rate_onramp_candidate()
|
|
574
|
+
if onramp_grid is not None and onramp_grid.size > 0:
|
|
575
|
+
return onramp_grid, onramp_reason
|
|
576
|
+
|
|
577
|
+
if np.count_nonzero(finite) < 3:
|
|
578
|
+
return None, None
|
|
579
|
+
finite_indices = np.flatnonzero(finite)
|
|
580
|
+
peak_index = int(finite_indices[int(np.nanargmax(log10_rate[finite]))])
|
|
581
|
+
if peak_index <= 0 or peak_index >= temps.size - 1:
|
|
582
|
+
return None, None
|
|
583
|
+
if not (finite[peak_index - 1] and finite[peak_index + 1]):
|
|
584
|
+
return None, None
|
|
585
|
+
|
|
586
|
+
peak_rate = float(log10_rate[peak_index])
|
|
587
|
+
hot_rate = float(log10_rate[peak_index - 1])
|
|
588
|
+
cold_rate = float(log10_rate[peak_index + 1])
|
|
589
|
+
if not (peak_rate > hot_rate and peak_rate > cold_rate):
|
|
590
|
+
return None, None
|
|
591
|
+
prominence = peak_rate - max(hot_rate, cold_rate)
|
|
592
|
+
min_prominence = SUBCRITICAL_RATE_PEAK_MIN_PROMINENCE
|
|
593
|
+
if prominence < min_prominence:
|
|
594
|
+
return None, None
|
|
595
|
+
|
|
596
|
+
candidate = np.asarray([], dtype=float)
|
|
597
|
+
if peak_rate >= rate_floor:
|
|
598
|
+
peak_temperature, _, _ = _refine_step1_rate_peak(temps, log10_rate)
|
|
599
|
+
add_candidate(peak_temperature)
|
|
600
|
+
for target in (rate_floor, -5.0, 0.0):
|
|
601
|
+
if candidate.size >= budget:
|
|
602
|
+
break
|
|
603
|
+
if float(target) <= peak_rate + 1.0e-12:
|
|
604
|
+
add_rate_crossings(float(target))
|
|
605
|
+
if candidate.size < budget:
|
|
606
|
+
add_largest_rate_band_gaps(budget - int(candidate.size))
|
|
607
|
+
else:
|
|
608
|
+
hot_budget = max(1, budget // 2)
|
|
609
|
+
cold_budget = max(1, budget - hot_budget)
|
|
610
|
+
if budget == 1:
|
|
611
|
+
hot_gap = math.log(float(temps[peak_index - 1]) / float(temps[peak_index]))
|
|
612
|
+
cold_gap = math.log(float(temps[peak_index]) / float(temps[peak_index + 1]))
|
|
613
|
+
if hot_gap >= cold_gap:
|
|
614
|
+
add_interval_points(float(temps[peak_index - 1]), float(temps[peak_index]), 1)
|
|
615
|
+
else:
|
|
616
|
+
add_interval_points(float(temps[peak_index]), float(temps[peak_index + 1]), 1)
|
|
617
|
+
else:
|
|
618
|
+
add_interval_points(float(temps[peak_index - 1]), float(temps[peak_index]), hot_budget)
|
|
619
|
+
add_interval_points(float(temps[peak_index]), float(temps[peak_index + 1]), cold_budget)
|
|
620
|
+
|
|
621
|
+
if candidate.size == 0:
|
|
622
|
+
return None, None
|
|
623
|
+
candidate = candidate[: int(max_new_points)]
|
|
624
|
+
reason = (
|
|
625
|
+
"rate_peak_refine around "
|
|
626
|
+
f"T={float(temps[peak_index]):2.5g}, "
|
|
627
|
+
f"max log10(Gamma/H^4)={peak_rate:2.3g}, "
|
|
628
|
+
f"prominence={prominence:2.3g}"
|
|
629
|
+
)
|
|
630
|
+
return candidate, reason
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _hot_head_underresolved(
|
|
634
|
+
temperatures: np.ndarray,
|
|
635
|
+
probabilities: np.ndarray,
|
|
636
|
+
actions: np.ndarray,
|
|
637
|
+
hubble: np.ndarray,
|
|
638
|
+
settings,
|
|
639
|
+
*,
|
|
640
|
+
for_acceptance: bool,
|
|
641
|
+
) -> tuple[bool, float | None, float | None]:
|
|
642
|
+
"""Return whether the hottest retained support point still starts too late."""
|
|
643
|
+
|
|
644
|
+
temps = np.asarray(temperatures, dtype=float)
|
|
645
|
+
probs = np.asarray(probabilities, dtype=float)
|
|
646
|
+
acts = np.asarray(actions, dtype=float)
|
|
647
|
+
hub = np.asarray(hubble, dtype=float)
|
|
648
|
+
if temps.size == 0 or probs.size == 0 or acts.size == 0 or hub.size == 0:
|
|
649
|
+
return False, None, None
|
|
650
|
+
if not (temps.size == probs.size == acts.size == hub.size):
|
|
651
|
+
return False, None, None
|
|
652
|
+
|
|
653
|
+
probability_hot = float(probs[0]) if np.isfinite(probs[0]) else None
|
|
654
|
+
log_rate = _log_gamma_h4_array(temps, acts, hub)
|
|
655
|
+
log_gamma_h4_hot = float(log_rate[0]) if log_rate.size and np.isfinite(log_rate[0]) else None
|
|
656
|
+
if probability_hot is None or log_gamma_h4_hot is None:
|
|
657
|
+
return False, probability_hot, log_gamma_h4_hot
|
|
658
|
+
|
|
659
|
+
if probability_hot > float(settings.f_start):
|
|
660
|
+
return False, probability_hot, log_gamma_h4_hot
|
|
661
|
+
|
|
662
|
+
threshold = HOT_RATE_ACCEPT_LOG if for_acceptance else HOT_RATE_REFINE_LOG
|
|
663
|
+
if log_gamma_h4_hot <= threshold:
|
|
664
|
+
return False, probability_hot, log_gamma_h4_hot
|
|
665
|
+
|
|
666
|
+
hot_band_upper = float(min(HOT_PROBABILITY_ANCHORS))
|
|
667
|
+
hot_band_upper = max(float(hot_band_upper), 1.0e-12)
|
|
668
|
+
hot_band_mask = np.isfinite(probs) & (probs <= hot_band_upper + 1.0e-12)
|
|
669
|
+
if np.count_nonzero(hot_band_mask) >= 3:
|
|
670
|
+
hot_band_indices = np.flatnonzero(hot_band_mask)
|
|
671
|
+
hot_band_probs = probs[hot_band_mask]
|
|
672
|
+
band_reaches_anchor = bool(np.nanmax(hot_band_probs) >= 0.5 * hot_band_upper)
|
|
673
|
+
if band_reaches_anchor:
|
|
674
|
+
resolved_index = int(hot_band_indices[int(np.nanargmax(hot_band_probs))])
|
|
675
|
+
resolved_log_rate = float(log_rate[resolved_index])
|
|
676
|
+
if np.isfinite(resolved_log_rate) and resolved_log_rate <= threshold:
|
|
677
|
+
return False, probability_hot, log_gamma_h4_hot
|
|
678
|
+
|
|
679
|
+
return True, probability_hot, log_gamma_h4_hot
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _probability_anchor_presence_tolerance(
|
|
683
|
+
target: float,
|
|
684
|
+
settings,
|
|
685
|
+
) -> float:
|
|
686
|
+
"""Return the probability-space tolerance used to mark one anchor as covered."""
|
|
687
|
+
|
|
688
|
+
target = float(target)
|
|
689
|
+
threshold = _remesh_dp_limit(target, target, settings)
|
|
690
|
+
tolerance = 0.35 * float(threshold) if threshold > 0.0 else float("inf")
|
|
691
|
+
|
|
692
|
+
if any(
|
|
693
|
+
math.isclose(
|
|
694
|
+
target,
|
|
695
|
+
level,
|
|
696
|
+
rel_tol=0.0,
|
|
697
|
+
abs_tol=max(1.0e-12, 1.0e-9 * max(abs(level), abs(target), 1.0)),
|
|
698
|
+
)
|
|
699
|
+
for level in HOT_PROBABILITY_ANCHORS
|
|
700
|
+
):
|
|
701
|
+
tolerance = min(tolerance, max(1.0e-4, 0.25 * abs(target)))
|
|
702
|
+
return float(tolerance)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _build_step1_hot_rate_anchor_grid(
|
|
706
|
+
profile,
|
|
707
|
+
settings,
|
|
708
|
+
current_grid: np.ndarray,
|
|
709
|
+
) -> np.ndarray | None:
|
|
710
|
+
"""Carry a few hotter ``log(Gamma/H^4)`` anchors from the scout into the active grid."""
|
|
711
|
+
|
|
712
|
+
temps = np.asarray(profile.temperatures, dtype=float)
|
|
713
|
+
probs = np.asarray(profile.probabilities, dtype=float)
|
|
714
|
+
log_rate = np.asarray(profile.log_gamma_h4_sym, dtype=float)
|
|
715
|
+
active = _temperature_grid(current_grid)
|
|
716
|
+
if temps.size < 2 or temps.size != probs.size or temps.size != log_rate.size or active.size == 0:
|
|
717
|
+
return None
|
|
718
|
+
|
|
719
|
+
candidate = np.asarray([], dtype=float)
|
|
720
|
+
current_hot = float(active[0])
|
|
721
|
+
scale = max(np.max(np.abs(active)), np.max(np.abs(temps)), 1.0)
|
|
722
|
+
atol = 1e-10 * scale
|
|
723
|
+
hot_probability = float(probs[0]) if np.isfinite(probs[0]) else None
|
|
724
|
+
hot_log_rate = float(log_rate[0]) if np.isfinite(log_rate[0]) else None
|
|
725
|
+
if hot_probability is None or hot_log_rate is None:
|
|
726
|
+
return None
|
|
727
|
+
if hot_probability > float(settings.f_start):
|
|
728
|
+
return None
|
|
729
|
+
|
|
730
|
+
levels = sorted(
|
|
731
|
+
{
|
|
732
|
+
float(value)
|
|
733
|
+
for value in (
|
|
734
|
+
*HOT_RATE_ANCHORS,
|
|
735
|
+
HOT_RATE_REFINE_LOG,
|
|
736
|
+
HOT_RATE_ACCEPT_LOG,
|
|
737
|
+
)
|
|
738
|
+
if np.isfinite(float(value))
|
|
739
|
+
},
|
|
740
|
+
reverse=True,
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
def add_candidate(value: float | None) -> None:
|
|
744
|
+
nonlocal candidate
|
|
745
|
+
if value is None or not np.isfinite(float(value)):
|
|
746
|
+
return
|
|
747
|
+
if float(value) <= current_hot + atol:
|
|
748
|
+
return
|
|
749
|
+
if _temperature_present(active, float(value)) or _temperature_present(candidate, float(value)):
|
|
750
|
+
return
|
|
751
|
+
candidate = _temperature_grid(np.append(candidate, float(value)))
|
|
752
|
+
|
|
753
|
+
if hot_log_rate > HOT_RATE_ACCEPT_LOG:
|
|
754
|
+
add_candidate(float(temps[0]))
|
|
755
|
+
|
|
756
|
+
for level in levels:
|
|
757
|
+
if hot_log_rate < float(level):
|
|
758
|
+
continue
|
|
759
|
+
crossing, _, _ = _find_value_crossing(temps, log_rate, float(level))
|
|
760
|
+
add_candidate(crossing)
|
|
761
|
+
|
|
762
|
+
return candidate if candidate.size > 0 else None
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _build_step1_hot_probability_anchor_grid(
|
|
766
|
+
profile,
|
|
767
|
+
settings,
|
|
768
|
+
current_grid: np.ndarray,
|
|
769
|
+
) -> np.ndarray | None:
|
|
770
|
+
"""Carry explicit hot-side probability anchors from the scout into the active grid."""
|
|
771
|
+
|
|
772
|
+
temps = np.asarray(profile.temperatures, dtype=float)
|
|
773
|
+
probs = np.asarray(profile.probabilities, dtype=float)
|
|
774
|
+
active = _temperature_grid(current_grid)
|
|
775
|
+
if temps.size < 2 or temps.size != probs.size or active.size == 0:
|
|
776
|
+
return None
|
|
777
|
+
|
|
778
|
+
candidate = np.asarray([], dtype=float)
|
|
779
|
+
|
|
780
|
+
def occupied_probabilities(temperatures_to_sample: np.ndarray) -> np.ndarray:
|
|
781
|
+
values: list[float] = []
|
|
782
|
+
for value in np.asarray(temperatures_to_sample, dtype=float):
|
|
783
|
+
probability = _probability_at(value, temps, probs)
|
|
784
|
+
values.append(float("nan") if probability is None else float(probability))
|
|
785
|
+
return np.asarray(values, dtype=float)
|
|
786
|
+
|
|
787
|
+
occupied_probs = occupied_probabilities(active)
|
|
788
|
+
|
|
789
|
+
def add_candidate(value: float | None) -> None:
|
|
790
|
+
nonlocal candidate, active, occupied_probs
|
|
791
|
+
if value is None or not np.isfinite(float(value)):
|
|
792
|
+
return
|
|
793
|
+
numeric = float(value)
|
|
794
|
+
if _temperature_present(active, numeric) or _temperature_present(candidate, numeric):
|
|
795
|
+
return
|
|
796
|
+
candidate = _temperature_grid(np.append(candidate, numeric))
|
|
797
|
+
active = _temperature_grid(np.append(active, numeric))
|
|
798
|
+
occupied_probs = occupied_probabilities(active)
|
|
799
|
+
|
|
800
|
+
p_min = float(np.nanmin(probs))
|
|
801
|
+
p_max = float(np.nanmax(probs))
|
|
802
|
+
for target in HOT_PROBABILITY_ANCHORS:
|
|
803
|
+
if not (p_min + 1.0e-12 < target < p_max - 1.0e-12):
|
|
804
|
+
continue
|
|
805
|
+
tolerance = _probability_anchor_presence_tolerance(target, settings)
|
|
806
|
+
if np.any(np.isfinite(occupied_probs) & (np.abs(occupied_probs - target) <= tolerance)):
|
|
807
|
+
continue
|
|
808
|
+
crossing, _, crossing_index = _find_value_crossing(temps, probs, target)
|
|
809
|
+
if crossing_index is None:
|
|
810
|
+
continue
|
|
811
|
+
add_candidate(crossing)
|
|
812
|
+
|
|
813
|
+
return candidate if candidate.size > 0 else None
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _build_dynamiczoomwindow_hot_head_grid(
|
|
817
|
+
temperatures: np.ndarray,
|
|
818
|
+
probabilities: np.ndarray,
|
|
819
|
+
actions: np.ndarray,
|
|
820
|
+
hubble: np.ndarray,
|
|
821
|
+
settings,
|
|
822
|
+
*,
|
|
823
|
+
new_high: float,
|
|
824
|
+
max_new_points: int,
|
|
825
|
+
) -> tuple[np.ndarray | None, str | None]:
|
|
826
|
+
"""Return a focused hot-side refinement batch for unresolved onset support."""
|
|
827
|
+
|
|
828
|
+
if max_new_points <= 0:
|
|
829
|
+
return None, None
|
|
830
|
+
|
|
831
|
+
temps = np.asarray(temperatures, dtype=float)
|
|
832
|
+
probs = np.asarray(probabilities, dtype=float)
|
|
833
|
+
acts = np.asarray(actions, dtype=float)
|
|
834
|
+
hub = np.asarray(hubble, dtype=float)
|
|
835
|
+
if not (temps.size == probs.size == acts.size == hub.size) or temps.size < 2:
|
|
836
|
+
return None, None
|
|
837
|
+
|
|
838
|
+
finite_mask = np.isfinite(temps) & np.isfinite(probs)
|
|
839
|
+
temps = temps[finite_mask]
|
|
840
|
+
probs = probs[finite_mask]
|
|
841
|
+
acts = acts[finite_mask]
|
|
842
|
+
hub = hub[finite_mask]
|
|
843
|
+
if temps.size < 2:
|
|
844
|
+
return None, None
|
|
845
|
+
|
|
846
|
+
current = _temperature_grid(temps)
|
|
847
|
+
if current.size < 2:
|
|
848
|
+
return None, None
|
|
849
|
+
|
|
850
|
+
candidate = np.asarray([], dtype=float)
|
|
851
|
+
occupied = current.copy()
|
|
852
|
+
added_labels: list[str] = []
|
|
853
|
+
|
|
854
|
+
def occupied_probabilities(temperatures_to_sample: np.ndarray) -> np.ndarray:
|
|
855
|
+
values: list[float] = []
|
|
856
|
+
for value in np.asarray(temperatures_to_sample, dtype=float):
|
|
857
|
+
probability = _probability_at(value, temps, probs)
|
|
858
|
+
values.append(float("nan") if probability is None else float(probability))
|
|
859
|
+
return np.asarray(values, dtype=float)
|
|
860
|
+
|
|
861
|
+
occupied_probs = occupied_probabilities(occupied)
|
|
862
|
+
|
|
863
|
+
def add_candidate(value: float | None, label: str) -> bool:
|
|
864
|
+
nonlocal candidate, occupied, occupied_probs
|
|
865
|
+
if value is None or not np.isfinite(float(value)):
|
|
866
|
+
return False
|
|
867
|
+
numeric = float(value)
|
|
868
|
+
if _temperature_present(occupied, numeric) or _temperature_present(candidate, numeric):
|
|
869
|
+
return False
|
|
870
|
+
candidate = _temperature_grid(np.append(candidate, numeric))
|
|
871
|
+
occupied = _temperature_grid(np.append(occupied, numeric))
|
|
872
|
+
occupied_probs = occupied_probabilities(occupied)
|
|
873
|
+
added_labels.append(label)
|
|
874
|
+
return True
|
|
875
|
+
|
|
876
|
+
p_min = float(np.nanmin(probs))
|
|
877
|
+
p_max = float(np.nanmax(probs))
|
|
878
|
+
for target in HOT_PROBABILITY_ANCHORS:
|
|
879
|
+
if candidate.size >= int(max_new_points):
|
|
880
|
+
break
|
|
881
|
+
if not (p_min + 1.0e-12 < target < p_max - 1.0e-12):
|
|
882
|
+
continue
|
|
883
|
+
tolerance = _probability_anchor_presence_tolerance(target, settings)
|
|
884
|
+
if np.any(np.isfinite(occupied_probs) & (np.abs(occupied_probs - target) <= tolerance)):
|
|
885
|
+
continue
|
|
886
|
+
crossing, _, crossing_index = _find_value_crossing(temps, probs, target)
|
|
887
|
+
if crossing_index is None:
|
|
888
|
+
continue
|
|
889
|
+
add_candidate(crossing, f"P={target:2.3g}")
|
|
890
|
+
|
|
891
|
+
log_rate = _log_gamma_h4_array(temps, acts, hub)
|
|
892
|
+
hot_log_rate = float(log_rate[0]) if log_rate.size and np.isfinite(log_rate[0]) else None
|
|
893
|
+
if hot_log_rate is not None:
|
|
894
|
+
levels = sorted(
|
|
895
|
+
{
|
|
896
|
+
float(value)
|
|
897
|
+
for value in (
|
|
898
|
+
*HOT_RATE_ANCHORS,
|
|
899
|
+
HOT_RATE_REFINE_LOG,
|
|
900
|
+
HOT_RATE_ACCEPT_LOG,
|
|
901
|
+
)
|
|
902
|
+
if np.isfinite(float(value))
|
|
903
|
+
},
|
|
904
|
+
reverse=True,
|
|
905
|
+
)
|
|
906
|
+
for level in levels:
|
|
907
|
+
if candidate.size >= int(max_new_points):
|
|
908
|
+
break
|
|
909
|
+
if hot_log_rate < float(level):
|
|
910
|
+
continue
|
|
911
|
+
crossing, _, crossing_index = _find_value_crossing(temps, log_rate, float(level))
|
|
912
|
+
if crossing_index is None:
|
|
913
|
+
continue
|
|
914
|
+
add_candidate(crossing, f"logR={level:2.3g}")
|
|
915
|
+
|
|
916
|
+
remaining = int(max_new_points) - int(candidate.size)
|
|
917
|
+
current_high = float(current[0])
|
|
918
|
+
target_high = float(max(new_high, current_high))
|
|
919
|
+
if remaining > 0 and target_high > current_high:
|
|
920
|
+
n_high = min(remaining, 2)
|
|
921
|
+
if current_high > 0.0 and target_high / current_high >= 2.0:
|
|
922
|
+
extra_hot = np.geomspace(target_high, current_high, n_high + 1, dtype=float)[:-1]
|
|
923
|
+
else:
|
|
924
|
+
extra_hot = np.linspace(target_high, current_high, n_high + 1, dtype=float)[:-1]
|
|
925
|
+
for value in extra_hot:
|
|
926
|
+
if candidate.size >= int(max_new_points):
|
|
927
|
+
break
|
|
928
|
+
add_candidate(value, f"hot@{float(value):2.5g}")
|
|
929
|
+
|
|
930
|
+
if candidate.size == 0:
|
|
931
|
+
return None, None
|
|
932
|
+
return candidate, "hot_head_refine added " + ", ".join(added_labels)
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _rate_interval_is_post_peak_inert(
|
|
936
|
+
temperatures: np.ndarray,
|
|
937
|
+
actions: np.ndarray | None,
|
|
938
|
+
hubble: np.ndarray | None,
|
|
939
|
+
interval_high: float,
|
|
940
|
+
interval_low: float,
|
|
941
|
+
settings,
|
|
942
|
+
*,
|
|
943
|
+
vw: float = 1.0,
|
|
944
|
+
) -> bool:
|
|
945
|
+
"""Return whether one interval is safely beyond a resolved rate peak."""
|
|
946
|
+
|
|
947
|
+
if actions is None or hubble is None:
|
|
948
|
+
return False
|
|
949
|
+
temps = np.asarray(temperatures, dtype=float)
|
|
950
|
+
acts = np.asarray(actions, dtype=float)
|
|
951
|
+
hub = np.asarray(hubble, dtype=float)
|
|
952
|
+
if temps.size < 3 or not (temps.size == acts.size == hub.size):
|
|
953
|
+
return False
|
|
954
|
+
|
|
955
|
+
order = np.argsort(-temps)
|
|
956
|
+
temps = temps[order]
|
|
957
|
+
acts = acts[order]
|
|
958
|
+
hub = hub[order]
|
|
959
|
+
log10_rate = _log10_gamma_h4_array(temps, acts, hub)
|
|
960
|
+
finite = np.isfinite(temps) & np.isfinite(log10_rate) & (temps > 0.0)
|
|
961
|
+
if np.count_nonzero(finite) < 3:
|
|
962
|
+
return False
|
|
963
|
+
|
|
964
|
+
finite_indices = np.flatnonzero(finite)
|
|
965
|
+
peak_index = int(finite_indices[int(np.nanargmax(log10_rate[finite]))])
|
|
966
|
+
if peak_index <= 0 or peak_index >= temps.size - 1:
|
|
967
|
+
return False
|
|
968
|
+
|
|
969
|
+
peak_rate = float(log10_rate[peak_index])
|
|
970
|
+
if peak_rate < RATE_PEAK_INERT_MIN_PEAK_LOG10:
|
|
971
|
+
return False
|
|
972
|
+
|
|
973
|
+
high = float(interval_high)
|
|
974
|
+
low = float(interval_low)
|
|
975
|
+
if not (np.isfinite(high) and np.isfinite(low) and high > low):
|
|
976
|
+
return False
|
|
977
|
+
|
|
978
|
+
scale = max(np.max(np.abs(temps)), abs(high), abs(low), 1.0)
|
|
979
|
+
atol = _temperature_tolerance(temps, high) + _temperature_tolerance(temps, low) + 1.0e-12 * scale
|
|
980
|
+
high_matches = np.flatnonzero(np.isclose(temps, high, rtol=0.0, atol=atol))
|
|
981
|
+
low_matches = np.flatnonzero(np.isclose(temps, low, rtol=0.0, atol=atol))
|
|
982
|
+
if high_matches.size == 0 or low_matches.size == 0:
|
|
983
|
+
return False
|
|
984
|
+
high_index = int(high_matches[0])
|
|
985
|
+
low_index = int(low_matches[0])
|
|
986
|
+
if high_index >= low_index:
|
|
987
|
+
return False
|
|
988
|
+
|
|
989
|
+
endpoint_rates = log10_rate[[high_index, low_index]]
|
|
990
|
+
if not np.all(np.isfinite(endpoint_rates)):
|
|
991
|
+
return False
|
|
992
|
+
if float(np.max(endpoint_rates)) > RATE_PEAK_INERT_LOG10:
|
|
993
|
+
return False
|
|
994
|
+
|
|
995
|
+
max_interval_dI = RATE_PEAK_INERT_MAX_INTERVAL_DI
|
|
996
|
+
if max_interval_dI > 0.0:
|
|
997
|
+
log_a = np.full_like(log10_rate, -np.inf, dtype=float)
|
|
998
|
+
valid_a = np.isfinite(log10_rate) & np.isfinite(hub) & (hub > 0.0) & np.isfinite(temps) & (temps > 0.0)
|
|
999
|
+
log_a[valid_a] = log10_rate[valid_a] * math.log(10.0) + 3.0 * np.log(hub[valid_a]) - 4.0 * np.log(temps[valid_a])
|
|
1000
|
+
inv_h = np.where(np.isfinite(hub) & (hub > 0.0), 1.0 / hub, 0.0)
|
|
1001
|
+
volume_to_cold = np.zeros_like(temps, dtype=float)
|
|
1002
|
+
for index in range(max(temps.size - 2, -1), -1, -1):
|
|
1003
|
+
dt = abs(float(temps[index]) - float(temps[index + 1]))
|
|
1004
|
+
volume_to_cold[index] = volume_to_cold[index + 1] + 0.5 * dt * (
|
|
1005
|
+
abs(float(inv_h[index])) + abs(float(inv_h[index + 1]))
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
interval_log_a = log_a[[high_index, low_index]]
|
|
1009
|
+
if not np.all(np.isfinite(interval_log_a)):
|
|
1010
|
+
return False
|
|
1011
|
+
max_log_a = float(np.max(interval_log_a))
|
|
1012
|
+
max_volume = float(np.max(volume_to_cold[[high_index, low_index]]))
|
|
1013
|
+
interval_width = abs(float(high) - float(low))
|
|
1014
|
+
wall_velocity = max(float(vw), 0.0)
|
|
1015
|
+
if max_volume <= 0.0 or interval_width <= 0.0 or wall_velocity <= 0.0:
|
|
1016
|
+
interval_dI_bound = 0.0
|
|
1017
|
+
else:
|
|
1018
|
+
log_bound = (
|
|
1019
|
+
math.log(4.0 * math.pi / 3.0)
|
|
1020
|
+
+ 3.0 * math.log(wall_velocity)
|
|
1021
|
+
+ max_log_a
|
|
1022
|
+
+ 3.0 * math.log(max_volume)
|
|
1023
|
+
+ math.log(interval_width)
|
|
1024
|
+
)
|
|
1025
|
+
if log_bound > 700.0:
|
|
1026
|
+
interval_dI_bound = float("inf")
|
|
1027
|
+
elif log_bound < -745.0:
|
|
1028
|
+
interval_dI_bound = 0.0
|
|
1029
|
+
else:
|
|
1030
|
+
interval_dI_bound = float(math.exp(log_bound))
|
|
1031
|
+
if not np.isfinite(interval_dI_bound) or interval_dI_bound > max_interval_dI:
|
|
1032
|
+
return False
|
|
1033
|
+
|
|
1034
|
+
min_monotone_points = RATE_PEAK_INERT_MIN_MONOTONE_POINTS
|
|
1035
|
+
monotone_tolerance = RATE_PEAK_INERT_MONOTONE_TOLERANCE_LOG10
|
|
1036
|
+
|
|
1037
|
+
if high_index > peak_index and low_index > peak_index:
|
|
1038
|
+
shoulder_rates = log10_rate[peak_index : low_index + 1]
|
|
1039
|
+
if np.count_nonzero(np.isfinite(shoulder_rates)) < min_monotone_points + 1:
|
|
1040
|
+
return False
|
|
1041
|
+
diffs = np.diff(shoulder_rates[np.isfinite(shoulder_rates)])
|
|
1042
|
+
return bool(np.all(diffs <= monotone_tolerance))
|
|
1043
|
+
if high_index < peak_index and low_index < peak_index:
|
|
1044
|
+
shoulder_rates = log10_rate[high_index : peak_index + 1]
|
|
1045
|
+
if np.count_nonzero(np.isfinite(shoulder_rates)) < min_monotone_points + 1:
|
|
1046
|
+
return False
|
|
1047
|
+
diffs = np.diff(shoulder_rates[np.isfinite(shoulder_rates)])
|
|
1048
|
+
return bool(np.all(diffs >= -monotone_tolerance))
|
|
1049
|
+
return False
|