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.
Files changed (95) hide show
  1. transitionlistener/__init__.py +21 -0
  2. transitionlistener/bubbledynamics.py +2250 -0
  3. transitionlistener/bubbledynamics_fixedstep.py +2929 -0
  4. transitionlistener/check_installation.py +309 -0
  5. transitionlistener/ckm.py +91 -0
  6. transitionlistener/colors.py +288 -0
  7. transitionlistener/config.py +185 -0
  8. transitionlistener/constants.py +64 -0
  9. transitionlistener/counterterms/__init__.py +230 -0
  10. transitionlistener/counterterms/cw.py +298 -0
  11. transitionlistener/counterterms/tensors.py +343 -0
  12. transitionlistener/counterterms/twohdm.py +520 -0
  13. transitionlistener/counterterms/twohdm_solver.py +374 -0
  14. transitionlistener/errors.py +315 -0
  15. transitionlistener/examples/bp_et.yaml +64 -0
  16. transitionlistener/examples/bp_lisa.yaml +70 -0
  17. transitionlistener/examples/bp_muares.yaml +69 -0
  18. transitionlistener/examples/bp_pta.yaml +64 -0
  19. transitionlistener/examples/example_dark_photon.yaml +73 -0
  20. transitionlistener/examples/example_grid.yaml +55 -0
  21. transitionlistener/examples/example_line.yaml +51 -0
  22. transitionlistener/examples/example_point.yaml +73 -0
  23. transitionlistener/examples/example_random_conformal.yaml +42 -0
  24. transitionlistener/examples/example_ultranest.yaml +39 -0
  25. transitionlistener/finiteT.py +468 -0
  26. transitionlistener/generic_potential.py +1521 -0
  27. transitionlistener/gridplots.py +592 -0
  28. transitionlistener/gwfopt.py +604 -0
  29. transitionlistener/helper_functions.py +843 -0
  30. transitionlistener/hydrodynamics.py +647 -0
  31. transitionlistener/interface/__init__.py +95 -0
  32. transitionlistener/interface/check.py +51 -0
  33. transitionlistener/interface/cli.py +211 -0
  34. transitionlistener/interface/config_loader.py +264 -0
  35. transitionlistener/interface/logging_utils.py +30 -0
  36. transitionlistener/interface/output_schema.py +296 -0
  37. transitionlistener/interface/pipeline.py +300 -0
  38. transitionlistener/interface/samplers.py +565 -0
  39. transitionlistener/interface/scans.py +468 -0
  40. transitionlistener/interface/single_point.py +581 -0
  41. transitionlistener/interface/state.py +61 -0
  42. transitionlistener/lineplots.py +597 -0
  43. transitionlistener/logo/TL-logo_large.png +0 -0
  44. transitionlistener/logo/TL-logo_small.png +0 -0
  45. transitionlistener/models/TL_2HDM.py +512 -0
  46. transitionlistener/models/TL_conformal_dark_u1.py +202 -0
  47. transitionlistener/models/TL_dark_U1.py +198 -0
  48. transitionlistener/models/TL_dark_U1_g_parameterization.py +198 -0
  49. transitionlistener/models/TL_dark_flipflop.py +274 -0
  50. transitionlistener/models/templatePotential.py +260 -0
  51. transitionlistener/nucleation.py +412 -0
  52. transitionlistener/observability.py +790 -0
  53. transitionlistener/particles.py +436 -0
  54. transitionlistener/pathDeformation.py +1462 -0
  55. transitionlistener/percolation_adaptive_gridbuilders.py +1385 -0
  56. transitionlistener/percolation_adaptive_rate.py +1049 -0
  57. transitionlistener/percolation_adaptivestepsize.py +1632 -0
  58. transitionlistener/percolation_fixedstepsize.py +618 -0
  59. transitionlistener/phases.py +1499 -0
  60. transitionlistener/plot_settings.py +121 -0
  61. transitionlistener/plots.py +1781 -0
  62. transitionlistener/runtime_options.py +278 -0
  63. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/bandwidths.npy +0 -0
  64. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/density.npy +0 -0
  65. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/freqs.npy +0 -0
  66. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log.txt +16 -0
  67. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rhogrid.npy +0 -0
  68. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rholabels.txt +30 -0
  69. transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/pulsar_list.txt +1 -0
  70. transitionlistener/tab_data/HLV-O2-PLI.csv +82 -0
  71. transitionlistener/tab_data/HLV-O2-noise.csv +83 -0
  72. transitionlistener/tab_data/HLVK-design-PLI.csv +61 -0
  73. transitionlistener/tab_data/HLVK-design-noise.csv +83 -0
  74. transitionlistener/tab_data/NG15_PLI_hc.csv +31 -0
  75. transitionlistener/tab_data/NG15_noise_hc.csv +56 -0
  76. transitionlistener/tab_data/PLI-dataset.csv +327 -0
  77. transitionlistener/tab_data/energydensities.dat +1001 -0
  78. transitionlistener/tab_data/finiteT_b.dat.txt +1999 -0
  79. transitionlistener/tab_data/finiteT_f.dat.txt +1000 -0
  80. transitionlistener/tab_data/geff_SM.dat +10009 -0
  81. transitionlistener/tab_data/muAres_f_Hz_PLI_h2OmegaGW.csv +131 -0
  82. transitionlistener/tab_data/muAres_f_Hz_h2Omega_noise.csv +99 -0
  83. transitionlistener/tab_data/noise-dataset.csv +327 -0
  84. transitionlistener/tab_data/numberdensities.dat +1001 -0
  85. transitionlistener/tab_data/pressures.dat +1001 -0
  86. transitionlistener/thermodynamics.py +686 -0
  87. transitionlistener/transitionObservables.py +1199 -0
  88. transitionlistener/transitionObservables_fixedstep.py +976 -0
  89. transitionlistener/transitions.py +1000 -0
  90. transitionlistener/tunneling1D.py +910 -0
  91. transitionlistener-2.0.0.dist-info/METADATA +817 -0
  92. transitionlistener-2.0.0.dist-info/RECORD +95 -0
  93. transitionlistener-2.0.0.dist-info/WHEEL +4 -0
  94. transitionlistener-2.0.0.dist-info/entry_points.txt +2 -0
  95. transitionlistener-2.0.0.dist-info/licenses/LICENSE.txt +674 -0
@@ -0,0 +1,1632 @@
1
+ """Seedless dynamic zoom-window percolation (adaptive step size) algorithm.
2
+
3
+ Grid-construction helpers (step-1 scouting, jump/range/anchor-remesh builders,
4
+ adaptive cold-boundary picker, jump-underresolution guards) live in
5
+ :mod:`transitionlistener.percolation_adaptive_gridbuilders`. This module keeps the
6
+ top-level scan controllers and their :class:`PercolationState` bookkeeping.
7
+
8
+ Part of TransitionListener v2.0
9
+ Documentation: https://tasillo.de/TransitionListener/
10
+
11
+ Authors:
12
+ Jonas Matuszak <jonas.matuszak@kit.edu>
13
+ Carlo Tasillo <carlo.tasillo@ific.uv.es>
14
+ """
15
+
16
+ import math
17
+ import numpy as np
18
+ from scipy import interpolate
19
+ from scipy import optimize
20
+
21
+ from transitionlistener import constants as cn
22
+ from transitionlistener import errors
23
+ from transitionlistener.bubbledynamics import (
24
+ PercolationGrid,
25
+ PercolationState,
26
+ _temperature_grid,
27
+ )
28
+ from transitionlistener.percolation_adaptive_rate import (
29
+ _build_dynamiczoomwindow_hot_head_grid,
30
+ _build_dynamiczoomwindow_hot_shoulder_grid,
31
+ _build_dynamiczoomwindow_rate_peak_grid,
32
+ _find_value_crossing,
33
+ _hot_head_underresolved,
34
+ _temperature_present,
35
+ )
36
+ from transitionlistener.percolation_adaptive_gridbuilders import (
37
+ _adaptive_cold_boundary,
38
+ _build_dynamiczoomwindow_anchor_remesh_grid,
39
+ _build_dynamiczoomwindow_jump_grid,
40
+ _build_dynamiczoomwindow_range_grid,
41
+ _build_dynamiczoomwindow_unresolved_jump_grid,
42
+ _cached_promotion_bounds,
43
+ _initial_temperature_grid,
44
+ _raise_if_hot_head_underresolved,
45
+ _raise_if_large_delta_p_underresolved,
46
+ _range_expand_batch_size,
47
+ )
48
+
49
+
50
+ def _controller_added_support_points(
51
+ support_bank: np.ndarray | None,
52
+ free_support_bank: np.ndarray | None,
53
+ ) -> int:
54
+ """Count support points that require new controller-requested actions."""
55
+
56
+ bank = _temperature_grid(support_bank)
57
+ free_bank = _temperature_grid(free_support_bank)
58
+ if bank.size == 0:
59
+ return 0
60
+ if free_bank.size == 0:
61
+ return int(bank.size)
62
+ count = 0
63
+ for value in bank:
64
+ scale = max(np.max(np.abs(free_bank)), abs(float(value)), 1.0)
65
+ if not np.any(np.isclose(free_bank, float(value), atol=1e-10 * scale, rtol=0.0)):
66
+ count += 1
67
+ return int(count)
68
+
69
+
70
+ def _log_support_rebuild(
71
+ state: PercolationState,
72
+ stage: str,
73
+ reason: str,
74
+ old_grid: np.ndarray,
75
+ new_grid: np.ndarray,
76
+ verbose: bool,
77
+ ) -> None:
78
+ """Bump ``state.rebuild_count`` and print a support-rebuild line when verbose."""
79
+ state.rebuild_count = int(state.rebuild_count or 0) + 1
80
+ if verbose:
81
+ print(
82
+ "Support rebuild:",
83
+ f"stage={stage}, reason={reason}, "
84
+ f"old=[{float(old_grid[-1]):2.5g}, {float(old_grid[0]):2.5g}], "
85
+ f"new=[{float(new_grid[-1]):2.5g}, {float(new_grid[0]):2.5g}], "
86
+ f"n={len(new_grid)}",
87
+ )
88
+
89
+
90
+ def _apply_dynamiczoomwindow_support_update(
91
+ current_active_grid: np.ndarray,
92
+ candidate_grid: np.ndarray,
93
+ support_bank: np.ndarray | None,
94
+ free_support_bank: np.ndarray | None,
95
+ settings,
96
+ *,
97
+ max_new_points: int | None = None,
98
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
99
+ """Apply one support-grid update while keeping dormant scout points inactive."""
100
+ current_active = _temperature_grid(current_active_grid)
101
+ bank = _temperature_grid(support_bank)
102
+ free_bank = _temperature_grid(free_support_bank)
103
+ if bank.size == 0:
104
+ bank = current_active
105
+ else:
106
+ bank = _temperature_grid(np.concatenate((bank, current_active)))
107
+
108
+ known_active = _temperature_grid(np.concatenate((bank, current_active)))
109
+ new_candidates: list[float] = []
110
+ for value in _temperature_grid(candidate_grid):
111
+ numeric = float(value)
112
+ if _temperature_present(known_active, numeric) or _temperature_present(new_candidates, numeric):
113
+ continue
114
+ new_candidates.append(numeric)
115
+
116
+ controller_added = _controller_added_support_points(bank, free_bank)
117
+ updated_bank = bank
118
+ dropped_new_points: list[float] = []
119
+ limit = int(settings.n_action_max if max_new_points is None else max_new_points)
120
+ admitted_candidates: list[float] = []
121
+ for value in new_candidates:
122
+ if _temperature_present(free_bank, float(value)):
123
+ updated_bank = _temperature_grid(np.append(updated_bank, float(value)))
124
+ admitted_candidates.append(float(value))
125
+ continue
126
+ if controller_added >= limit:
127
+ dropped_new_points.append(float(value))
128
+ continue
129
+ updated_bank = _temperature_grid(np.append(updated_bank, float(value)))
130
+ admitted_candidates.append(float(value))
131
+ controller_added += 1
132
+
133
+ active = current_active
134
+ if admitted_candidates:
135
+ active = _temperature_grid(np.concatenate((active, np.asarray(admitted_candidates, dtype=float))))
136
+ if active.size == 0:
137
+ active = current_active
138
+ return active, updated_bank, np.asarray(dropped_new_points, dtype=float)
139
+
140
+
141
+ def _estimate_dynamiczoomwindow_accuracy(
142
+ temperatures: np.ndarray,
143
+ hubble: np.ndarray,
144
+ probabilities: np.ndarray,
145
+ actions: np.ndarray,
146
+ settings,
147
+ pot,
148
+ *,
149
+ previous: dict[str, float | None] | None = None,
150
+ ) -> dict[str, float | None]:
151
+ """Estimate Tperc, Tf and RH together with conservative convergence indicators."""
152
+ from transitionlistener import bubbledynamics as bd
153
+
154
+ estimate: dict[str, float | None] = {
155
+ "Tperc": math.nan,
156
+ "Tperc_uncertainty": math.inf,
157
+ "Tf": math.nan,
158
+ "Tf_uncertainty": math.inf,
159
+ "RH": math.nan,
160
+ "RH_uncertainty": math.inf,
161
+ "Tperc_bracket_width": math.inf,
162
+ "Tf_bracket_width": math.inf,
163
+ }
164
+
165
+ Tperc, Tperc_width, _ = _find_value_crossing(temperatures, probabilities, settings.f_perc)
166
+ estimate["Tperc"] = math.nan if Tperc is None else float(Tperc)
167
+ estimate["Tperc_bracket_width"] = math.inf if Tperc_width is None else float(Tperc_width)
168
+
169
+ Tf_cross, Tf_width, _ = _find_value_crossing(temperatures, probabilities, settings.f_final)
170
+ estimate["Tf_bracket_width"] = math.inf if Tf_width is None else float(Tf_width)
171
+
172
+ temps = np.asarray(temperatures, dtype=float)
173
+ hubble_values = np.asarray(hubble, dtype=float)
174
+ probability_values = np.asarray(probabilities, dtype=float)
175
+ action_values = np.asarray(actions, dtype=float)
176
+
177
+ finite_prob = np.isfinite(probability_values)
178
+ finite_action = np.isfinite(action_values)
179
+ finite_hubble = np.isfinite(hubble_values)
180
+ if (
181
+ np.count_nonzero(finite_prob) >= 4
182
+ and np.count_nonzero(finite_action) >= 4
183
+ and np.count_nonzero(finite_hubble) >= 4
184
+ ):
185
+ try:
186
+ Pint = interpolate.interp1d(
187
+ temps[finite_prob],
188
+ probability_values[finite_prob],
189
+ kind="cubic",
190
+ fill_value=(0.0, 1.0),
191
+ bounds_error=False,
192
+ )
193
+ Sint = interpolate.interp1d(
194
+ temps[finite_action],
195
+ action_values[finite_action],
196
+ kind="cubic",
197
+ fill_value=(-np.inf, np.inf),
198
+ bounds_error=False,
199
+ )
200
+ Hint = interpolate.interp1d(
201
+ temps[finite_hubble],
202
+ hubble_values[finite_hubble],
203
+ kind="cubic",
204
+ fill_value="extrapolate",
205
+ bounds_error=False,
206
+ )
207
+ except Exception:
208
+ Pint = Sint = Hint = None
209
+ else:
210
+ Pint = Sint = Hint = None
211
+
212
+ if Tf_cross is not None:
213
+ estimate["Tf"] = float(Tf_cross)
214
+ if Pint is not None and np.isfinite(float(estimate["Tperc"])):
215
+ try:
216
+ estimate["Tf"] = bd.calcTf(
217
+ float(estimate["Tperc"]),
218
+ float(temps[-1]),
219
+ Pint,
220
+ pot,
221
+ verbose=False,
222
+ )
223
+ except Exception:
224
+ pass
225
+
226
+ if (
227
+ Pint is not None
228
+ and Sint is not None
229
+ and Hint is not None
230
+ and np.isfinite(float(estimate["Tperc"]))
231
+ ):
232
+ try:
233
+ bubble_separation = bd.calcMeanBubbleSeparation(
234
+ float(estimate["Tperc"]),
235
+ float(temps[0]),
236
+ Sint,
237
+ Pint,
238
+ Hint,
239
+ None,
240
+ None,
241
+ verbose=False,
242
+ )
243
+ rh_indicator = float(bubble_separation * float(Hint(float(estimate["Tperc"]))))
244
+ if np.isfinite(rh_indicator) and rh_indicator > 0.0:
245
+ estimate["RH"] = rh_indicator
246
+ except Exception:
247
+ pass
248
+
249
+ previous_Tperc = None if previous is None else previous.get("Tperc")
250
+ previous_Tf = None if previous is None else previous.get("Tf")
251
+ previous_RH = None if previous is None else previous.get("RH")
252
+
253
+ if np.isfinite(float(estimate["Tperc"])):
254
+ bracket_uncertainty = float(estimate["Tperc_bracket_width"]) / max(abs(float(estimate["Tperc"])), 1e-30)
255
+ if previous_Tperc is None or not np.isfinite(previous_Tperc) or not np.isfinite(float(estimate["Tperc"])):
256
+ estimate["Tperc_uncertainty"] = bracket_uncertainty
257
+ else:
258
+ estimate["Tperc_uncertainty"] = max(
259
+ bracket_uncertainty,
260
+ abs(float(estimate["Tperc"]) - float(previous_Tperc))
261
+ / max(abs(float(estimate["Tperc"])), 1e-30),
262
+ )
263
+ if np.isfinite(float(estimate["Tf"])):
264
+ bracket_uncertainty = float(estimate["Tf_bracket_width"]) / max(abs(float(estimate["Tf"])), 1e-30)
265
+ if previous_Tf is None or not np.isfinite(previous_Tf) or not np.isfinite(float(estimate["Tf"])):
266
+ estimate["Tf_uncertainty"] = bracket_uncertainty
267
+ else:
268
+ estimate["Tf_uncertainty"] = max(
269
+ bracket_uncertainty,
270
+ abs(float(estimate["Tf"]) - float(previous_Tf))
271
+ / max(abs(float(estimate["Tf"])), 1e-30),
272
+ )
273
+ if np.isfinite(float(estimate["RH"])):
274
+ estimate["RH_uncertainty"] = (
275
+ 0.0
276
+ if previous_RH is None
277
+ or not np.isfinite(previous_RH)
278
+ or not np.isfinite(float(estimate["RH"]))
279
+ else abs(float(estimate["RH"]) - float(previous_RH))
280
+ / max(abs(float(estimate["RH"])), 1e-30)
281
+ )
282
+
283
+ return estimate
284
+
285
+
286
+ def _dynamiczoomwindow_accuracy_reached(
287
+ estimate: dict[str, float | None],
288
+ settings,
289
+ ) -> bool:
290
+ """Return whether all requested default-path accuracy targets were met."""
291
+ return (
292
+ np.isfinite(float(estimate["Tperc"]))
293
+ and np.isfinite(float(estimate["Tf"]))
294
+ and np.isfinite(float(estimate["RH"]))
295
+ and float(estimate["Tperc_uncertainty"]) <= settings.acc_tperc
296
+ and float(estimate["Tf_uncertainty"]) <= settings.acc_tfinal
297
+ and float(estimate["RH_uncertainty"]) <= settings.acc_rh
298
+ )
299
+
300
+
301
+ def _dynamiczoomwindow_accuracy_warning_message(
302
+ stage: str,
303
+ estimate: dict[str, float | None],
304
+ settings,
305
+ ) -> str:
306
+ """Describe which accuracy goals prevented further early stopping."""
307
+ reasons: list[str] = []
308
+ if not np.isfinite(float(estimate["Tperc"])):
309
+ reasons.append("Tperc could not be bracketed")
310
+ elif float(estimate["Tperc_uncertainty"]) > settings.acc_tperc:
311
+ reasons.append(
312
+ f"Tperc uncertainty {100.0 * float(estimate['Tperc_uncertainty']):2.3g}% > "
313
+ f"{100.0 * settings.acc_tperc:2.3g}%"
314
+ )
315
+
316
+ if not np.isfinite(float(estimate["Tf"])):
317
+ reasons.append("Tf could not be determined")
318
+ elif float(estimate["Tf_uncertainty"]) > settings.acc_tfinal:
319
+ reasons.append(
320
+ f"Tf uncertainty {100.0 * float(estimate['Tf_uncertainty']):2.3g}% > "
321
+ f"{100.0 * settings.acc_tfinal:2.3g}%"
322
+ )
323
+
324
+ if not np.isfinite(float(estimate["RH"])):
325
+ reasons.append("RH could not be determined")
326
+ elif float(estimate["RH_uncertainty"]) > settings.acc_rh:
327
+ reasons.append(
328
+ f"RH uncertainty {100.0 * float(estimate['RH_uncertainty']):2.3g}% > "
329
+ f"{100.0 * settings.acc_rh:2.3g}%"
330
+ )
331
+
332
+ if not reasons:
333
+ reasons.append("the requested accuracy could not be certified")
334
+ return (
335
+ f"Warning: {stage} stopped before reaching all requested accuracy goals. "
336
+ f"Increasing n_action_max above {settings.n_action_max} might help. "
337
+ "Remaining issue(s): " + "; ".join(reasons) + "."
338
+ )
339
+
340
+
341
+ def _try_dynamiczoomwindow_local_refinement(
342
+ temperatures: np.ndarray,
343
+ hubble: np.ndarray,
344
+ probabilities: np.ndarray,
345
+ actions: np.ndarray,
346
+ settings,
347
+ *,
348
+ remaining_budget: int,
349
+ vw: float,
350
+ apply_candidate_grid,
351
+ ) -> tuple[bool, int]:
352
+ """Spend one refinement batch on the most local unresolved feature."""
353
+ if remaining_budget <= 0:
354
+ return False, int(remaining_budget)
355
+
356
+ batch = min(int(settings.n_action_increment), int(remaining_budget))
357
+ hot_shoulder_grid, _ = _build_dynamiczoomwindow_hot_shoulder_grid(
358
+ temperatures,
359
+ probabilities,
360
+ actions,
361
+ hubble,
362
+ settings,
363
+ max_new_points=batch,
364
+ )
365
+ if hot_shoulder_grid is not None and apply_candidate_grid(hot_shoulder_grid, "hot_shoulder_refine"):
366
+ return True, int(remaining_budget)
367
+
368
+ jump_grid = _build_dynamiczoomwindow_unresolved_jump_grid(
369
+ temperatures,
370
+ probabilities,
371
+ settings,
372
+ max_new_points=int(remaining_budget),
373
+ actions=actions,
374
+ hubble=hubble,
375
+ vw=vw,
376
+ )
377
+ if jump_grid is not None and apply_candidate_grid(jump_grid, "jump_refine"):
378
+ return True, int(remaining_budget)
379
+
380
+ return False, int(remaining_budget)
381
+
382
+
383
+ def _try_dynamiczoomwindow_completed_refinement(
384
+ temperatures: np.ndarray,
385
+ hubble: np.ndarray,
386
+ probabilities: np.ndarray,
387
+ actions: np.ndarray,
388
+ settings,
389
+ pot,
390
+ *,
391
+ remaining_budget: int,
392
+ previous_estimate: dict[str, float | None] | None,
393
+ iterator: int,
394
+ stage: str,
395
+ vw: float,
396
+ apply_candidate_grid,
397
+ promote_cached_action_points,
398
+ ) -> tuple[bool, dict[str, float | None] | None]:
399
+ """Run the simplified remeshing order once the current range brackets the transition.
400
+
401
+ The simplified controller still gives priority to the most obviously
402
+ untrustworthy local feature before rebuilding the general anchor mesh:
403
+
404
+ 1. repair a still-underresolved hot-shoulder interval directly,
405
+ 2. repair a still-large neighbouring ``Delta P`` jump directly,
406
+ 3. otherwise rebuild the next support batch from fixed probability anchors
407
+ plus the largest remaining neighbouring ``Delta P`` violations.
408
+ """
409
+ if iterator < settings.maxit:
410
+ refined, remaining_budget = _try_dynamiczoomwindow_local_refinement(
411
+ temperatures,
412
+ hubble,
413
+ probabilities,
414
+ actions,
415
+ settings,
416
+ remaining_budget=remaining_budget,
417
+ vw=vw,
418
+ apply_candidate_grid=apply_candidate_grid,
419
+ )
420
+ if refined:
421
+ return True, None
422
+
423
+ remesh_grid, remesh_reason = _build_dynamiczoomwindow_anchor_remesh_grid(
424
+ temperatures,
425
+ probabilities,
426
+ settings,
427
+ max_new_points=min(int(settings.n_action_increment), int(remaining_budget)) if remaining_budget > 0 else 0,
428
+ actions=actions,
429
+ hubble=hubble,
430
+ vw=vw,
431
+ )
432
+ if remesh_grid is not None and remesh_reason is not None and iterator < settings.maxit and remaining_budget > 0:
433
+ if apply_candidate_grid(remesh_grid, "anchor_remesh"):
434
+ return True, None
435
+
436
+ estimate = _estimate_dynamiczoomwindow_accuracy(
437
+ temperatures,
438
+ hubble,
439
+ probabilities,
440
+ actions,
441
+ settings,
442
+ pot,
443
+ previous=previous_estimate,
444
+ )
445
+ if promote_cached_action_points("cached_action_points"):
446
+ return True, estimate
447
+ if previous_estimate is not None and _dynamiczoomwindow_accuracy_reached(estimate, settings):
448
+ return False, estimate
449
+
450
+ if previous_estimate is not None or remaining_budget <= 0:
451
+ print(_dynamiczoomwindow_accuracy_warning_message(stage, estimate, settings))
452
+ return False, estimate
453
+
454
+
455
+ def _try_dynamiczoomwindow_ordered_support_refinement(
456
+ temperatures: np.ndarray,
457
+ hubble: np.ndarray,
458
+ probabilities: np.ndarray,
459
+ actions: np.ndarray,
460
+ settings,
461
+ *,
462
+ remaining_budget: int,
463
+ vw: float,
464
+ hot_bound: float,
465
+ hot_head_needs_support: bool,
466
+ apply_candidate_grid,
467
+ ) -> tuple[bool, int]:
468
+ """Try the shared adaptive step size support-refinement order before range expansion."""
469
+
470
+ refined_locally, remaining_budget = _try_dynamiczoomwindow_local_refinement(
471
+ temperatures,
472
+ hubble,
473
+ probabilities,
474
+ actions,
475
+ settings,
476
+ remaining_budget=remaining_budget,
477
+ vw=vw,
478
+ apply_candidate_grid=apply_candidate_grid,
479
+ )
480
+ if refined_locally or remaining_budget <= 0:
481
+ return bool(refined_locally), int(remaining_budget)
482
+
483
+ if hot_head_needs_support:
484
+ hot_extend_high = float(temperatures[0]) + (float(hot_bound) - float(temperatures[0])) * max(
485
+ float(settings.rel_increment),
486
+ 0.35,
487
+ )
488
+ hot_head_grid, _ = _build_dynamiczoomwindow_hot_head_grid(
489
+ temperatures,
490
+ probabilities,
491
+ actions,
492
+ hubble,
493
+ settings,
494
+ new_high=min(float(hot_bound), float(hot_extend_high)),
495
+ max_new_points=min(int(settings.n_action_increment), int(remaining_budget)),
496
+ )
497
+ if hot_head_grid is not None and apply_candidate_grid(hot_head_grid, "hot_head_refine"):
498
+ return True, int(remaining_budget)
499
+
500
+ cold_tail_unresolved = bool(probabilities[-1] < settings.f_final)
501
+ if cold_tail_unresolved and remaining_budget > 0:
502
+ rate_peak_grid, _ = _build_dynamiczoomwindow_rate_peak_grid(
503
+ temperatures,
504
+ actions,
505
+ hubble,
506
+ settings,
507
+ max_new_points=min(int(settings.n_action_increment), int(remaining_budget)),
508
+ )
509
+ if rate_peak_grid is not None and apply_candidate_grid(rate_peak_grid, "rate_peak_refine"):
510
+ return True, int(remaining_budget)
511
+
512
+ if probabilities[0] <= settings.f_start and probabilities[-1] >= settings.f_perc and not hot_head_needs_support:
513
+ remesh_grid, _ = _build_dynamiczoomwindow_anchor_remesh_grid(
514
+ temperatures,
515
+ probabilities,
516
+ settings,
517
+ max_new_points=min(int(settings.n_action_increment), int(remaining_budget)),
518
+ actions=actions,
519
+ hubble=hubble,
520
+ vw=vw,
521
+ )
522
+ if remesh_grid is not None and apply_candidate_grid(remesh_grid, "anchor_remesh"):
523
+ return True, int(remaining_budget)
524
+
525
+ return False, int(remaining_budget)
526
+
527
+
528
+ def _dynamiczoomwindow_post_sweep_controller(
529
+ temperatures: np.ndarray,
530
+ hubble: np.ndarray,
531
+ probabilities: np.ndarray,
532
+ actions: np.ndarray,
533
+ settings,
534
+ pot,
535
+ *,
536
+ stage: str,
537
+ iterator: int,
538
+ remaining_budget: int,
539
+ previous_estimate: dict[str, float | None] | None,
540
+ vw: float,
541
+ hot_bound: float,
542
+ apply_candidate_grid,
543
+ promote_cached_action_points,
544
+ promote_hot_head_cached_points,
545
+ profile_residual: float = 0.0,
546
+ profile_rho_tol: float = math.inf,
547
+ verbose: bool = False,
548
+ ) -> tuple[str, dict[str, float | None] | None, int, bool]:
549
+ """Shared adaptive step size controller after one computed P(T) sweep.
550
+
551
+ The stage-specific code is responsible for computing the profile. Once the
552
+ arrays exist, both step 2 and step 3 follow the same support-policy order:
553
+ repair hot onset, accept/refine completed profiles, stop on budget/iteration
554
+ limits, try local support refinement, otherwise ask the caller to range
555
+ expand.
556
+ """
557
+
558
+ hot_head_needs_support, _, _ = _hot_head_underresolved(
559
+ temperatures,
560
+ probabilities,
561
+ actions,
562
+ hubble,
563
+ settings,
564
+ for_acceptance=True,
565
+ )
566
+
567
+ if hot_head_needs_support and promote_hot_head_cached_points():
568
+ return "retry", None, int(remaining_budget), hot_head_needs_support
569
+ if promote_cached_action_points("cached_action_points"):
570
+ return "retry", None, int(remaining_budget), hot_head_needs_support
571
+
572
+ completed = (
573
+ float(probabilities[0]) <= float(settings.f_start)
574
+ and float(probabilities[-1]) >= float(settings.f_final)
575
+ and not hot_head_needs_support
576
+ )
577
+ if completed:
578
+ refinement_previous_estimate = previous_estimate if profile_residual <= profile_rho_tol else None
579
+ if profile_residual > profile_rho_tol and verbose:
580
+ print(
581
+ f"{stage} profile residual remains above tolerance even after same-grid "
582
+ "Picard sweeps. Spending any remaining controller budget on support "
583
+ "refinement before accepting the profile."
584
+ )
585
+ should_retry, previous_estimate = _try_dynamiczoomwindow_completed_refinement(
586
+ temperatures,
587
+ hubble,
588
+ probabilities,
589
+ actions,
590
+ settings,
591
+ pot,
592
+ remaining_budget=remaining_budget,
593
+ previous_estimate=refinement_previous_estimate,
594
+ iterator=iterator,
595
+ stage=stage,
596
+ vw=vw,
597
+ apply_candidate_grid=apply_candidate_grid,
598
+ promote_cached_action_points=promote_cached_action_points,
599
+ )
600
+ if should_retry:
601
+ return "retry", previous_estimate, int(remaining_budget), hot_head_needs_support
602
+ return "accept", previous_estimate, int(remaining_budget), hot_head_needs_support
603
+
604
+ if iterator >= settings.maxit or remaining_budget <= 0:
605
+ print(
606
+ f"Warning: {stage} reached the support-update limit before the full "
607
+ "completion range was resolved. Continuing with the best available grid."
608
+ )
609
+ return "limit", previous_estimate, int(remaining_budget), hot_head_needs_support
610
+
611
+ refined, remaining_budget = _try_dynamiczoomwindow_ordered_support_refinement(
612
+ temperatures,
613
+ hubble,
614
+ probabilities,
615
+ actions,
616
+ settings,
617
+ remaining_budget=remaining_budget,
618
+ vw=vw,
619
+ hot_bound=hot_bound,
620
+ hot_head_needs_support=hot_head_needs_support,
621
+ apply_candidate_grid=apply_candidate_grid,
622
+ )
623
+ if refined:
624
+ return "retry", None, int(remaining_budget), hot_head_needs_support
625
+
626
+ return "range_expand", previous_estimate, int(remaining_budget), hot_head_needs_support
627
+
628
+
629
+ def _dynamiczoomwindow_range_targets(
630
+ temperatures: np.ndarray,
631
+ probabilities: np.ndarray,
632
+ actions: np.ndarray,
633
+ hubble: np.ndarray,
634
+ settings,
635
+ *,
636
+ target_low: float,
637
+ target_high: float,
638
+ iterator: int,
639
+ hot_head_needs_support: bool,
640
+ maxit_margin: int,
641
+ cold_tail_last_temperature: float | None,
642
+ cold_tail_last_probability: float | None,
643
+ cold_tail_last_integral: float | None,
644
+ cold_tail_last_log_gamma_h4: float | None,
645
+ cold_tail_post_peak_streak: int,
646
+ ) -> tuple[float, float, float | None, float | None, float | None, float | None, int, bool]:
647
+ """Choose the next range-expansion bounds for the shared adaptive step size controller."""
648
+
649
+ temperatures = np.asarray(temperatures, dtype=float)
650
+ probabilities = np.asarray(probabilities, dtype=float)
651
+ actions = np.asarray(actions, dtype=float)
652
+ hubble = np.asarray(hubble, dtype=float)
653
+ cold_tail_unresolved = bool(probabilities[-1] < settings.f_final)
654
+
655
+ if (probabilities[0] > settings.f_start or hot_head_needs_support) and not cold_tail_unresolved:
656
+ high = float(temperatures[0]) + (float(target_high) - float(temperatures[0])) * settings.rel_increment
657
+ else:
658
+ high = float(temperatures[0])
659
+
660
+ if not cold_tail_unresolved:
661
+ return (
662
+ high,
663
+ float(temperatures[-1]),
664
+ cold_tail_last_temperature,
665
+ cold_tail_last_probability,
666
+ cold_tail_last_integral,
667
+ cold_tail_last_log_gamma_h4,
668
+ int(cold_tail_post_peak_streak),
669
+ False,
670
+ )
671
+
672
+ if iterator >= settings.maxit - int(maxit_margin):
673
+ low = float(target_low)
674
+ else:
675
+ low, current_integral, current_log_gamma_h4, cold_tail_post_peak_streak = _adaptive_cold_boundary(
676
+ current_low=float(temperatures[-1]),
677
+ target_low=float(target_low),
678
+ current_probability=float(probabilities[-1]),
679
+ current_action=float(actions[-1]),
680
+ current_hubble=float(hubble[-1]),
681
+ settings=settings,
682
+ last_temperature=cold_tail_last_temperature,
683
+ last_probability=cold_tail_last_probability,
684
+ last_integral=cold_tail_last_integral,
685
+ last_log_gamma_h4=cold_tail_last_log_gamma_h4,
686
+ post_peak_streak=cold_tail_post_peak_streak,
687
+ )
688
+ cold_tail_last_temperature = float(temperatures[-1])
689
+ cold_tail_last_probability = float(probabilities[-1])
690
+ cold_tail_last_integral = float(current_integral)
691
+ cold_tail_last_log_gamma_h4 = current_log_gamma_h4
692
+ low = min(float(low), float(temperatures[-1]))
693
+
694
+ return (
695
+ high,
696
+ low,
697
+ cold_tail_last_temperature,
698
+ cold_tail_last_probability,
699
+ cold_tail_last_integral,
700
+ cold_tail_last_log_gamma_h4,
701
+ int(cold_tail_post_peak_streak),
702
+ True,
703
+ )
704
+
705
+
706
+ def _apply_dynamiczoomwindow_candidate_grid(
707
+ active_grid: np.ndarray,
708
+ candidate_grid: np.ndarray,
709
+ support_bank: np.ndarray,
710
+ free_support_bank: np.ndarray,
711
+ settings,
712
+ outdict: dict,
713
+ phase_symmetric,
714
+ phase_broken,
715
+ *,
716
+ tmin: float,
717
+ tmax: float,
718
+ stage: str,
719
+ reset_arrays,
720
+ note_rebuild,
721
+ extra_budget: int = 0,
722
+ ) -> tuple[bool, np.ndarray, np.ndarray, np.ndarray]:
723
+ """Refresh the cached support bank and merge one candidate refinement grid."""
724
+ from transitionlistener import bubbledynamics as bd
725
+
726
+ old_grid = np.asarray(active_grid, dtype=float).copy()
727
+ cached_points = bd._transition_action_outdict_temperatures(
728
+ outdict,
729
+ phase_symmetric,
730
+ phase_broken,
731
+ tmin=tmin,
732
+ tmax=tmax,
733
+ )
734
+ if cached_points.size > 0:
735
+ free_support_bank = _temperature_grid(
736
+ np.concatenate((free_support_bank, cached_points))
737
+ )
738
+
739
+ new_active, updated_bank, dropped_new_points = _apply_dynamiczoomwindow_support_update(
740
+ active_grid,
741
+ candidate_grid,
742
+ support_bank,
743
+ free_support_bank,
744
+ settings,
745
+ max_new_points=int(settings.n_action_max) + max(int(extra_budget), 0),
746
+ )
747
+ if dropped_new_points.size > 0:
748
+ print(
749
+ f"Warning: n_action_max = {settings.n_action_max} blocked "
750
+ f"{len(dropped_new_points)} new controller-added support points during {stage}."
751
+ )
752
+
753
+ if (
754
+ np.asarray(new_active, dtype=float).shape == np.asarray(active_grid, dtype=float).shape
755
+ and (
756
+ np.asarray(new_active, dtype=float).size == 0
757
+ or np.allclose(
758
+ np.asarray(new_active, dtype=float),
759
+ np.asarray(active_grid, dtype=float),
760
+ rtol=0.0,
761
+ atol=1e-14,
762
+ )
763
+ )
764
+ ):
765
+ return False, active_grid, updated_bank, free_support_bank
766
+
767
+ reset_arrays(new_active)
768
+ note_rebuild(old_grid, new_active)
769
+ return True, new_active, updated_bank, free_support_bank
770
+
771
+
772
+ def _promote_dynamiczoomwindow_cached_points(
773
+ active_grid: np.ndarray,
774
+ support_bank: np.ndarray,
775
+ free_support_bank: np.ndarray,
776
+ settings,
777
+ outdict: dict,
778
+ phase_symmetric,
779
+ phase_broken,
780
+ *,
781
+ tmin: float,
782
+ tmax: float,
783
+ promotion_high: float | None = None,
784
+ promotion_low: float | None = None,
785
+ stage: str,
786
+ reason: str,
787
+ reset_arrays,
788
+ note_rebuild,
789
+ ) -> tuple[bool, np.ndarray, np.ndarray, np.ndarray]:
790
+ """Promote newly cached action temperatures into the active support bank."""
791
+ from transitionlistener import bubbledynamics as bd
792
+
793
+ cached_points = bd._transition_action_outdict_temperatures(
794
+ outdict,
795
+ phase_symmetric,
796
+ phase_broken,
797
+ tmin=tmin,
798
+ tmax=tmax,
799
+ )
800
+ pending_points = np.asarray(
801
+ [
802
+ float(value)
803
+ for value in _temperature_grid(cached_points)
804
+ if not _temperature_present(support_bank, float(value))
805
+ ],
806
+ dtype=float,
807
+ )
808
+ if (
809
+ pending_points.size > 0
810
+ and promotion_high is not None
811
+ and promotion_low is not None
812
+ and np.isfinite(float(promotion_high))
813
+ and np.isfinite(float(promotion_low))
814
+ ):
815
+ high = float(max(promotion_high, promotion_low))
816
+ low = float(min(promotion_high, promotion_low))
817
+ scale = max(abs(high), abs(low), 1.0)
818
+ atol = 1e-10 * scale
819
+ pending_points = np.asarray(
820
+ [
821
+ float(value)
822
+ for value in np.asarray(pending_points, dtype=float)
823
+ if low - atol <= float(value) <= high + atol
824
+ ],
825
+ dtype=float,
826
+ )
827
+ pending_points = _temperature_grid(pending_points)
828
+ if pending_points.size == 0:
829
+ return False, active_grid, support_bank, free_support_bank
830
+
831
+ return _apply_dynamiczoomwindow_candidate_grid(
832
+ active_grid,
833
+ pending_points,
834
+ support_bank,
835
+ free_support_bank,
836
+ settings,
837
+ outdict,
838
+ phase_symmetric,
839
+ phase_broken,
840
+ tmin=tmin,
841
+ tmax=tmax,
842
+ stage=stage,
843
+ reset_arrays=reset_arrays,
844
+ note_rebuild=lambda old_grid, new_grid: note_rebuild(reason, old_grid, new_grid),
845
+ )
846
+
847
+
848
+ def _compute_step2_profile(
849
+ state: PercolationState,
850
+ TSYM: np.ndarray,
851
+ previous_probability: np.ndarray,
852
+ settings,
853
+ outdict: dict,
854
+ pot,
855
+ phase_symmetric,
856
+ phase_broken,
857
+ CF: float,
858
+ vw: float,
859
+ verbose: bool,
860
+ ) -> None:
861
+ """Compute the P=0-Hubble profile used by adaptive step size, percolation step 2."""
862
+ from transitionlistener import bubbledynamics as bd
863
+
864
+ TSYM = np.asarray(TSYM, dtype=float)
865
+ previous_probability = np.asarray(previous_probability, dtype=float)
866
+ state.Sr.fill(0)
867
+ state.Hr.fill(0)
868
+ state.Pr.fill(0)
869
+ state.Tb.fill(0)
870
+ action_tail_saturated = False
871
+
872
+ for i, T in enumerate(TSYM):
873
+ rho = bd.energyDensity(pot, phase_symmetric, T)
874
+ state.Hr[i] = bd.HubbleParameter(rho, CF)
875
+ state.Sr[i] = (
876
+ bd.calcAction(pot, T, phase_symmetric, phase_broken, outdict)
877
+ if not action_tail_saturated
878
+ else np.nan
879
+ )
880
+ if (
881
+ i >= 1
882
+ and previous_probability[i] > settings.f_final
883
+ and previous_probability[i - 1] > settings.f_final
884
+ ):
885
+ if verbose and not action_tail_saturated:
886
+ print(
887
+ "WARNING: P > ",
888
+ settings.f_final,
889
+ " at T = ",
890
+ TSYM[i] * CF,
891
+ " GeV detected. Stop computation of action from here on "
892
+ "and only fill in the values for the Hubble rate.",
893
+ )
894
+ action_tail_saturated = True
895
+
896
+ _, Pr_ode = bd.percIntegralODE_full_sweep(
897
+ TSYM,
898
+ state.Hr,
899
+ state.Sr,
900
+ vw=vw,
901
+ pot=pot,
902
+ phase_symmetric=phase_symmetric,
903
+ time_temperature_mode=settings.time_temperature_mode,
904
+ integral_method=settings.integral_method,
905
+ )
906
+ state.Pr[:] = Pr_ode
907
+
908
+ if verbose:
909
+ for i, T in enumerate(TSYM):
910
+ print(
911
+ f"Step {i+1}/{len(TSYM)}, T = {TSYM[i] * CF:2.8g} GeV, "
912
+ f"P_true = {state.Pr[i]:2.5g}, S_3 = {state.Sr[i] * CF:2.5g} GeV, "
913
+ f"S_3/T = {state.Sr[i] / T:2.5g}"
914
+ )
915
+
916
+
917
+ def _compute_step3_profile(
918
+ state: PercolationState,
919
+ TSYM: np.ndarray,
920
+ previous_probability: np.ndarray,
921
+ settings,
922
+ outdict: dict,
923
+ pot,
924
+ phase_symmetric,
925
+ phase_broken,
926
+ CF: float,
927
+ vw: float,
928
+ TBROmin: float,
929
+ TBROmax: float,
930
+ verbose: bool,
931
+ ) -> None:
932
+ """Compute the P-dependent Hubble/Tb profile used by adaptive step size, percolation step 3."""
933
+ from transitionlistener import bubbledynamics as bd
934
+
935
+ previous_probability = np.asarray(previous_probability, dtype=float)
936
+ state.Hr.fill(0)
937
+ state.Pr.fill(0)
938
+ state.Tb.fill(np.nan)
939
+ state.Sr.fill(0)
940
+ profile_saturated = False
941
+
942
+ for i, T in enumerate(TSYM):
943
+ if not profile_saturated:
944
+ eSYM = bd.energyDensity(pot, phase_symmetric, T)
945
+ state.Sr[i] = bd.calcAction(pot, T, phase_symmetric, phase_broken, outdict)
946
+ P = float(previous_probability[i]) if np.isfinite(previous_probability[i]) else 0.0
947
+
948
+ if P <= 1e-12:
949
+ state.Hr[i] = bd.HubbleParameter(eSYM, CF)
950
+ state.Tb[i] = np.nan
951
+ elif i == 0:
952
+ try:
953
+ TBRO = optimize.brentq(
954
+ bd.Tb_criterion,
955
+ TBROmin,
956
+ TBROmax,
957
+ args=(T, phase_symmetric, phase_broken, pot),
958
+ )
959
+ except ValueError as err:
960
+ raise errors.PercolationError(
961
+ "Error in Tb_criterion: "
962
+ f"{err}. The phase tracing or tunneling precision may be too low."
963
+ )
964
+ state.Tb[i] = TBRO
965
+ eBRO = bd.energyDensity(pot, phase_broken, TBRO)
966
+ state.Hr[i] = bd.HubbleParameter(P * eBRO + (1.0 - P) * eSYM, CF)
967
+ else:
968
+ P_prev = float(previous_probability[i - 1]) if np.isfinite(previous_probability[i - 1]) else 0.0
969
+ previous_tb = float(state.Tb[i - 1]) if np.isfinite(state.Tb[i - 1]) else np.nan
970
+ if P_prev < 1e-6 or not np.isfinite(previous_tb):
971
+ try:
972
+ TBRO = optimize.brentq(
973
+ bd.Tb_criterion,
974
+ TBROmin,
975
+ TBROmax,
976
+ args=(T, phase_symmetric, phase_broken, pot),
977
+ )
978
+ except ValueError as err:
979
+ if not np.isfinite(previous_tb):
980
+ raise errors.PercolationError(
981
+ "Error in Tb_criterion for the first nonzero-P support point: "
982
+ f"{err}. The hot pure-symmetric tail may be too close to the "
983
+ "broken-branch tracing boundary."
984
+ )
985
+ TBRO = optimize.brentq(
986
+ bd.entropy_criterion_SYM_BRO,
987
+ TBROmin,
988
+ TBROmax,
989
+ args=(previous_tb, T, TSYM[i - 1], phase_broken, pot),
990
+ )
991
+ else:
992
+ try:
993
+ TBRO = optimize.brentq(
994
+ bd.entropy_criterion_SYM_BRO,
995
+ TBROmin,
996
+ TBROmax,
997
+ args=(previous_tb, T, TSYM[i - 1], phase_broken, pot),
998
+ )
999
+ except ValueError:
1000
+ TBRO = optimize.brentq(
1001
+ bd.Tb_criterion,
1002
+ TBROmin,
1003
+ TBROmax,
1004
+ args=(T, phase_symmetric, phase_broken, pot),
1005
+ )
1006
+ eBRO = bd.energyDensity(pot, phase_broken, TBRO)
1007
+
1008
+ dP = P - P_prev
1009
+ energy_release = eBRO * (P - dP) + dP * eSYM
1010
+ if energy_release / eBRO < 1e-50:
1011
+ state.Tb[i] = TBRO
1012
+ else:
1013
+ try:
1014
+ TBRO = optimize.brentq(
1015
+ bd.energy_criterion_BRO,
1016
+ TBROmin,
1017
+ TBROmax,
1018
+ args=(eSYM, eBRO, P, dP, phase_broken, pot),
1019
+ )
1020
+ state.Tb[i] = TBRO
1021
+ except ValueError:
1022
+ state.Tb[i] = state.Tb[i - 1]
1023
+ TBRO = state.Tb[i]
1024
+ eBRO = bd.energyDensity(pot, phase_broken, TBRO)
1025
+
1026
+ if state.Tb[i] < TSYM[i]:
1027
+ raise ValueError(
1028
+ "There was a problem with the T_bro calculation, "
1029
+ "T_bro < T_sym. This is usually not allowed."
1030
+ )
1031
+
1032
+ state.Hr[i] = bd.HubbleParameter(P * eBRO + (1.0 - P) * eSYM, CF)
1033
+ else:
1034
+ state.Sr[i] = np.nan
1035
+ state.Hr[i] = bd.HubbleParameter(bd.energyDensity(pot, phase_symmetric, T), CF)
1036
+
1037
+ if i >= 1 and previous_probability[i] > settings.f_final and previous_probability[i - 1] > settings.f_final:
1038
+ if verbose and not profile_saturated:
1039
+ print(
1040
+ "WARNING: P > ",
1041
+ settings.f_final,
1042
+ " at T = ",
1043
+ TSYM[i] * CF,
1044
+ " GeV detected. Stop computation of action from here on "
1045
+ "and only fill in the values for the Hubble rate.",
1046
+ )
1047
+ profile_saturated = True
1048
+
1049
+ _, Pr_ode = bd.percIntegralODE_full_sweep(
1050
+ TSYM,
1051
+ state.Hr,
1052
+ state.Sr,
1053
+ vw=vw,
1054
+ pot=pot,
1055
+ phase_symmetric=phase_symmetric,
1056
+ time_temperature_mode=settings.time_temperature_mode,
1057
+ integral_method=settings.integral_method,
1058
+ )
1059
+ state.Pr[:] = Pr_ode
1060
+
1061
+ if verbose:
1062
+ for i, T in enumerate(TSYM):
1063
+ print(
1064
+ f"Step {i+1}/{len(TSYM)}, T = {T * CF:2.8g} GeV, "
1065
+ f"P_true = {state.Pr[i]:2.5g}, S_3 = {state.Sr[i] * CF:2.5g} GeV, "
1066
+ f"S_3/T = {state.Sr[i] / T:2.5g}"
1067
+ )
1068
+
1069
+
1070
+ def _initial_percolation_scan_dynamiczoomwindow(
1071
+ grid: PercolationGrid,
1072
+ settings,
1073
+ outdict: dict,
1074
+ pot,
1075
+ phase_symmetric,
1076
+ phase_broken,
1077
+ CF: float,
1078
+ vw: float,
1079
+ verbose: bool,
1080
+ ) -> PercolationState:
1081
+ """Dynamic zoom-window step 2 with a coarse scan and local window refinement."""
1082
+ from transitionlistener import bubbledynamics as bd
1083
+
1084
+ original_grid = _temperature_grid(grid.TSYM)
1085
+ TSYM = original_grid.copy()
1086
+ # Free support points are cached/scout action temperatures. They may be
1087
+ # promoted into the active grid later, but they do not consume the
1088
+ # controller's new-action budget.
1089
+ free_support_bank = (
1090
+ _temperature_grid(grid.free_support_bank)
1091
+ if grid.free_support_bank is not None
1092
+ else bd._transition_action_outdict_temperatures(
1093
+ outdict,
1094
+ phase_symmetric,
1095
+ phase_broken,
1096
+ tmin=grid.tmin,
1097
+ tmax=grid.tmax,
1098
+ )
1099
+ )
1100
+ state = PercolationState(
1101
+ TSYM=TSYM,
1102
+ Sr=np.zeros_like(TSYM),
1103
+ Hr=np.zeros_like(TSYM),
1104
+ Pr=np.zeros_like(TSYM),
1105
+ Tb=np.zeros_like(TSYM),
1106
+ explored_tmin=float(TSYM[-1]),
1107
+ rebuild_count=0,
1108
+ support_bank=_temperature_grid(TSYM),
1109
+ free_support_bank=free_support_bank,
1110
+ )
1111
+
1112
+ iterator = 1
1113
+ previous_estimate = None
1114
+
1115
+ def _note_rebuild(reason: str, old_grid: np.ndarray, new_grid: np.ndarray) -> None:
1116
+ _log_support_rebuild(state, "step2", reason, old_grid, new_grid, verbose)
1117
+
1118
+ def _reset_active_arrays(new_grid: np.ndarray) -> None:
1119
+ nonlocal TSYM
1120
+ TSYM = np.asarray(new_grid, dtype=float)
1121
+ state.TSYM = TSYM
1122
+ state.Sr = np.zeros_like(TSYM)
1123
+ state.Hr = np.zeros_like(TSYM)
1124
+ state.Pr = np.zeros_like(TSYM)
1125
+ state.Tb = np.zeros_like(TSYM)
1126
+
1127
+ def _apply_candidate_grid(candidate_grid: np.ndarray, reason: str, *, extra_budget: int = 0) -> bool:
1128
+ nonlocal TSYM
1129
+ changed, TSYM, state.support_bank, state.free_support_bank = _apply_dynamiczoomwindow_candidate_grid(
1130
+ TSYM,
1131
+ candidate_grid,
1132
+ state.support_bank,
1133
+ state.free_support_bank,
1134
+ settings,
1135
+ outdict,
1136
+ phase_symmetric,
1137
+ phase_broken,
1138
+ tmin=grid.tmin,
1139
+ tmax=grid.tmax,
1140
+ stage="step 2",
1141
+ reset_arrays=_reset_active_arrays,
1142
+ note_rebuild=lambda old_grid, new_grid: _note_rebuild(reason, old_grid, new_grid),
1143
+ extra_budget=extra_budget,
1144
+ )
1145
+ return changed
1146
+
1147
+ def _promote_cached_action_points(reason: str) -> bool:
1148
+ nonlocal TSYM
1149
+ promotion_bounds = _cached_promotion_bounds(TSYM)
1150
+ changed, TSYM, state.support_bank, state.free_support_bank = _promote_dynamiczoomwindow_cached_points(
1151
+ TSYM,
1152
+ state.support_bank,
1153
+ state.free_support_bank,
1154
+ settings,
1155
+ outdict,
1156
+ phase_symmetric,
1157
+ phase_broken,
1158
+ tmin=grid.tmin,
1159
+ tmax=grid.tmax,
1160
+ promotion_high=None if promotion_bounds is None else promotion_bounds[0],
1161
+ promotion_low=None if promotion_bounds is None else promotion_bounds[1],
1162
+ stage="step 2",
1163
+ reason=reason,
1164
+ reset_arrays=_reset_active_arrays,
1165
+ note_rebuild=_note_rebuild,
1166
+ )
1167
+ return changed
1168
+
1169
+ def _promote_hot_head_cached_points() -> bool:
1170
+ nonlocal TSYM
1171
+ changed, TSYM, state.support_bank, state.free_support_bank = _promote_dynamiczoomwindow_cached_points(
1172
+ TSYM,
1173
+ state.support_bank,
1174
+ state.free_support_bank,
1175
+ settings,
1176
+ outdict,
1177
+ phase_symmetric,
1178
+ phase_broken,
1179
+ tmin=grid.tmin,
1180
+ tmax=grid.tmax,
1181
+ promotion_high=float(grid.tmax),
1182
+ promotion_low=float(TSYM[0]),
1183
+ stage="step 2",
1184
+ reason="hot_head_cached_points",
1185
+ reset_arrays=_reset_active_arrays,
1186
+ note_rebuild=_note_rebuild,
1187
+ )
1188
+ return changed
1189
+
1190
+ if verbose:
1191
+ print("\nPercolation step 2: calculating Tperc assuming P = 0 in the Hubble rate")
1192
+
1193
+ while True:
1194
+ state.explored_tmin = min(float(state.explored_tmin), float(TSYM[-1]))
1195
+ _compute_step2_profile(
1196
+ state,
1197
+ TSYM,
1198
+ np.zeros_like(TSYM),
1199
+ settings,
1200
+ outdict,
1201
+ pot=pot,
1202
+ phase_symmetric=phase_symmetric,
1203
+ phase_broken=phase_broken,
1204
+ CF=CF,
1205
+ vw=vw,
1206
+ verbose=verbose,
1207
+ )
1208
+
1209
+ remaining_budget = max(
1210
+ int(settings.n_action_max) - _controller_added_support_points(state.support_bank, state.free_support_bank),
1211
+ 0,
1212
+ )
1213
+ decision, previous_estimate, remaining_budget, hot_head_needs_support = (
1214
+ _dynamiczoomwindow_post_sweep_controller(
1215
+ TSYM,
1216
+ state.Hr,
1217
+ state.Pr,
1218
+ state.Sr,
1219
+ settings,
1220
+ pot,
1221
+ stage="step 2",
1222
+ iterator=iterator,
1223
+ remaining_budget=remaining_budget,
1224
+ previous_estimate=previous_estimate,
1225
+ vw=vw,
1226
+ hot_bound=grid.tmax,
1227
+ apply_candidate_grid=_apply_candidate_grid,
1228
+ promote_cached_action_points=_promote_cached_action_points,
1229
+ promote_hot_head_cached_points=_promote_hot_head_cached_points,
1230
+ verbose=verbose,
1231
+ )
1232
+ )
1233
+ if decision == "retry":
1234
+ iterator += 1
1235
+ continue
1236
+ if decision in {"accept", "limit"}:
1237
+ break
1238
+
1239
+ (
1240
+ Tmax_new,
1241
+ Tmin_new,
1242
+ state.cold_tail_last_temperature,
1243
+ state.cold_tail_last_probability,
1244
+ state.cold_tail_last_integral,
1245
+ state.cold_tail_last_log_gamma_h4,
1246
+ state.cold_tail_post_peak_streak,
1247
+ _,
1248
+ ) = _dynamiczoomwindow_range_targets(
1249
+ TSYM,
1250
+ state.Pr,
1251
+ state.Sr,
1252
+ state.Hr,
1253
+ settings,
1254
+ target_low=grid.tmin,
1255
+ target_high=grid.tmax,
1256
+ iterator=iterator,
1257
+ hot_head_needs_support=hot_head_needs_support,
1258
+ maxit_margin=2,
1259
+ cold_tail_last_temperature=state.cold_tail_last_temperature,
1260
+ cold_tail_last_probability=state.cold_tail_last_probability,
1261
+ cold_tail_last_integral=state.cold_tail_last_integral,
1262
+ cold_tail_last_log_gamma_h4=state.cold_tail_last_log_gamma_h4,
1263
+ cold_tail_post_peak_streak=state.cold_tail_post_peak_streak,
1264
+ )
1265
+
1266
+ candidate_grid = _build_dynamiczoomwindow_range_grid(
1267
+ TSYM,
1268
+ new_high=Tmax_new,
1269
+ new_low=Tmin_new,
1270
+ max_new_points=_range_expand_batch_size(settings, int(remaining_budget)),
1271
+ )
1272
+ if candidate_grid is None or not _apply_candidate_grid(candidate_grid, "range_expand"):
1273
+ break
1274
+ previous_estimate = None
1275
+ iterator += 1
1276
+
1277
+ return state
1278
+
1279
+
1280
+ def _refine_percolation_temperature_dynamiczoomwindow(
1281
+ state: PercolationState,
1282
+ grid: PercolationGrid,
1283
+ settings,
1284
+ outdict: dict,
1285
+ pot,
1286
+ phase_symmetric,
1287
+ phase_broken,
1288
+ CF: float,
1289
+ vw: float,
1290
+ Tperc_prev: float,
1291
+ rtol: float,
1292
+ verbose: bool,
1293
+ ) -> tuple[float, PercolationState]:
1294
+ """Dynamic zoom-window step 3 with the same focused refinement as step 2."""
1295
+ from transitionlistener import bubbledynamics as bd
1296
+
1297
+ TBROmin = grid.TBROmin
1298
+ TBROmax = grid.TBROmax
1299
+ TSYM = np.asarray(state.TSYM, dtype=float)
1300
+ state.support_bank = _temperature_grid(state.support_bank)
1301
+ state.free_support_bank = _temperature_grid(state.free_support_bank)
1302
+ state.rebuild_count = int(state.rebuild_count or 0)
1303
+ state.cold_tail_post_peak_streak = int(state.cold_tail_post_peak_streak or 0)
1304
+ previous_estimate = None
1305
+ iterator = 1
1306
+ profile_picard_retries = 0
1307
+ profile_rho_tol = max(5.0 * float(settings.acc_rh), 0.02)
1308
+
1309
+ def _note_rebuild(reason: str, old_grid: np.ndarray, new_grid: np.ndarray) -> None:
1310
+ _log_support_rebuild(state, "step3", reason, old_grid, new_grid, verbose)
1311
+
1312
+ def _reset_state_arrays(new_grid: np.ndarray) -> None:
1313
+ nonlocal TSYM, profile_picard_retries
1314
+ TSYM = np.asarray(new_grid, dtype=float)
1315
+ state.TSYM = TSYM
1316
+ state.Sr = np.zeros_like(TSYM)
1317
+ state.Hr = np.zeros_like(TSYM)
1318
+ state.Pr = np.zeros_like(TSYM)
1319
+ state.Tb = np.zeros_like(TSYM)
1320
+ profile_picard_retries = 0
1321
+
1322
+ def _profile_self_consistency_residual() -> float:
1323
+ """Return the current step-3 fixed-point residual on the active broken branch.
1324
+
1325
+ Step 3 updates H(T) from the previous Picard iterate Pprev(T). Once the
1326
+ current iterate P(T) is known, a fully self-consistent solution should
1327
+ satisfy rho(H) ~= P * eBRO(TBRO) + (1 - P) * eSYM(TSYM) over the region
1328
+ where TBRO is still actively evolved. Large deviations mean that Tperc
1329
+ may already look stable while the underlying P/H/TBRO profile has not
1330
+ yet caught up.
1331
+ """
1332
+ active_mask = (
1333
+ np.isfinite(state.Tb)
1334
+ & (state.Tb > 0.0)
1335
+ & np.isfinite(state.Pr)
1336
+ & np.isfinite(state.Hr)
1337
+ )
1338
+ if not np.any(active_mask):
1339
+ return 0.0
1340
+
1341
+ rho_from_h = 3.0 * np.asarray(state.Hr[active_mask], dtype=float) ** 2
1342
+ rho_from_h *= (cn.Mpl_GeV / CF) ** 2 / (8.0 * np.pi)
1343
+
1344
+ tsym_active = np.asarray(TSYM[active_mask], dtype=float)
1345
+ tbro_active = np.asarray(state.Tb[active_mask], dtype=float)
1346
+ e_sym = np.asarray(
1347
+ [bd.energyDensity(pot, phase_symmetric, float(temp)) for temp in tsym_active],
1348
+ dtype=float,
1349
+ )
1350
+ e_bro = np.asarray(
1351
+ [bd.energyDensity(pot, phase_broken, float(temp)) for temp in tbro_active],
1352
+ dtype=float,
1353
+ )
1354
+ rho_target = np.asarray(state.Pr[active_mask], dtype=float) * e_bro
1355
+ rho_target += (1.0 - np.asarray(state.Pr[active_mask], dtype=float)) * e_sym
1356
+ scale = np.maximum(np.maximum(np.abs(rho_target), np.abs(rho_from_h)), 1.0e-30)
1357
+ return float(np.nanmax(np.abs(rho_from_h - rho_target) / scale))
1358
+
1359
+ def _apply_candidate_grid(candidate_grid: np.ndarray, reason: str, *, extra_budget: int = 0) -> bool:
1360
+ nonlocal TSYM
1361
+ changed, TSYM, state.support_bank, state.free_support_bank = _apply_dynamiczoomwindow_candidate_grid(
1362
+ TSYM,
1363
+ candidate_grid,
1364
+ state.support_bank,
1365
+ state.free_support_bank,
1366
+ settings,
1367
+ outdict,
1368
+ phase_symmetric,
1369
+ phase_broken,
1370
+ tmin=grid.tmin,
1371
+ tmax=grid.tmax,
1372
+ stage="step 3",
1373
+ reset_arrays=_reset_state_arrays,
1374
+ note_rebuild=lambda old_grid, new_grid: _note_rebuild(reason, old_grid, new_grid),
1375
+ extra_budget=extra_budget,
1376
+ )
1377
+ return changed
1378
+
1379
+ def _promote_cached_action_points(reason: str) -> bool:
1380
+ nonlocal TSYM
1381
+ promotion_bounds = _cached_promotion_bounds(TSYM)
1382
+ changed, TSYM, state.support_bank, state.free_support_bank = _promote_dynamiczoomwindow_cached_points(
1383
+ TSYM,
1384
+ state.support_bank,
1385
+ state.free_support_bank,
1386
+ settings,
1387
+ outdict,
1388
+ phase_symmetric,
1389
+ phase_broken,
1390
+ tmin=grid.tmin,
1391
+ tmax=grid.tmax,
1392
+ promotion_high=None if promotion_bounds is None else promotion_bounds[0],
1393
+ promotion_low=None if promotion_bounds is None else promotion_bounds[1],
1394
+ stage="step 3",
1395
+ reason=reason,
1396
+ reset_arrays=_reset_state_arrays,
1397
+ note_rebuild=_note_rebuild,
1398
+ )
1399
+ return changed
1400
+
1401
+ def _promote_hot_head_cached_points() -> bool:
1402
+ nonlocal TSYM
1403
+ changed, TSYM, state.support_bank, state.free_support_bank = _promote_dynamiczoomwindow_cached_points(
1404
+ TSYM,
1405
+ state.support_bank,
1406
+ state.free_support_bank,
1407
+ settings,
1408
+ outdict,
1409
+ phase_symmetric,
1410
+ phase_broken,
1411
+ tmin=grid.tmin,
1412
+ tmax=grid.tmax,
1413
+ promotion_high=float(grid.tmax),
1414
+ promotion_low=float(TSYM[0]),
1415
+ stage="step 3",
1416
+ reason="hot_head_cached_points",
1417
+ reset_arrays=_reset_state_arrays,
1418
+ note_rebuild=_note_rebuild,
1419
+ )
1420
+ return changed
1421
+
1422
+ if verbose:
1423
+ print("\nPercolation step 3: calculating Tperc with P-dependent Hubble rate")
1424
+
1425
+ while True:
1426
+ if state.explored_tmin is None:
1427
+ state.explored_tmin = float(TSYM[-1])
1428
+ else:
1429
+ state.explored_tmin = min(float(state.explored_tmin), float(TSYM[-1]))
1430
+
1431
+ if verbose:
1432
+ print(f"Iteration {iterator}, T_perc = {Tperc_prev * CF:2.8g} GeV")
1433
+
1434
+ Pprev = np.asarray(state.Pr, dtype=float).copy()
1435
+ _compute_step3_profile(
1436
+ state,
1437
+ TSYM,
1438
+ Pprev,
1439
+ settings,
1440
+ outdict,
1441
+ pot,
1442
+ phase_symmetric,
1443
+ phase_broken,
1444
+ CF,
1445
+ vw,
1446
+ TBROmin,
1447
+ TBROmax,
1448
+ verbose,
1449
+ )
1450
+
1451
+ Pint = interpolate.interp1d(TSYM, state.Pr)
1452
+ try:
1453
+ Tperc = optimize.brentq(lambda T: Pint(T) - settings.f_perc, TSYM[0], TSYM[-1])
1454
+ except ValueError as err:
1455
+ msg = err.args[0]
1456
+ if msg.startswith("f(a) and f(b) must have different signs") and Pint(TSYM[-1]) < settings.f_perc:
1457
+ raise errors.TooMuchSupercoolingError(
1458
+ "The percolation temperature could not be found because the "
1459
+ "true vacuum fraction only reaches "
1460
+ f"{Pint(TSYM[-1])} < {settings.f_perc} at Tmin = "
1461
+ f"{TSYM[-1] * CF} GeV."
1462
+ )
1463
+ raise errors.PercolationError(err)
1464
+
1465
+ # First demand consistency of the P-dependent Hubble update itself: if
1466
+ # Tperc still moves noticeably between successive step-3 iterations,
1467
+ # continue iterating before judging the support accuracy.
1468
+ if np.isfinite(Tperc) and abs(Tperc - Tperc_prev) / max(abs(Tperc), 1e-30) >= rtol:
1469
+ if iterator >= settings.maxit:
1470
+ print(
1471
+ "Warning: step 3 reached the iteration limit before Tperc fully "
1472
+ "stopped moving under the P-dependent Hubble update. Continuing."
1473
+ )
1474
+ else:
1475
+ Tperc_prev = Tperc
1476
+ iterator += 1
1477
+ continue
1478
+
1479
+ profile_residual = _profile_self_consistency_residual()
1480
+ if profile_residual > profile_rho_tol and profile_picard_retries < 3 and iterator < settings.maxit:
1481
+ if verbose:
1482
+ print(
1483
+ "Step-3 profile not yet self-consistent on the current support grid: "
1484
+ f"max |rho(H) - rho_mix| / rho = {profile_residual:2.5g} > {profile_rho_tol:2.5g}. "
1485
+ "Recomputing P/H/TBRO once more with the same cached actions."
1486
+ )
1487
+ Tperc_prev = Tperc
1488
+ iterator += 1
1489
+ profile_picard_retries += 1
1490
+ continue
1491
+ if profile_residual <= profile_rho_tol:
1492
+ profile_picard_retries = 0
1493
+
1494
+ remaining_budget = max(
1495
+ int(settings.n_action_max) - _controller_added_support_points(state.support_bank, state.free_support_bank),
1496
+ 0,
1497
+ )
1498
+ decision, previous_estimate, remaining_budget, hot_head_needs_support = (
1499
+ _dynamiczoomwindow_post_sweep_controller(
1500
+ TSYM,
1501
+ state.Hr,
1502
+ state.Pr,
1503
+ state.Sr,
1504
+ settings,
1505
+ pot,
1506
+ stage="step 3",
1507
+ iterator=iterator,
1508
+ remaining_budget=remaining_budget,
1509
+ previous_estimate=previous_estimate,
1510
+ vw=vw,
1511
+ hot_bound=grid.tmax,
1512
+ apply_candidate_grid=_apply_candidate_grid,
1513
+ promote_cached_action_points=_promote_cached_action_points,
1514
+ promote_hot_head_cached_points=_promote_hot_head_cached_points,
1515
+ profile_residual=profile_residual,
1516
+ profile_rho_tol=profile_rho_tol,
1517
+ verbose=verbose,
1518
+ )
1519
+ )
1520
+ if decision == "retry":
1521
+ Tperc_prev = Tperc
1522
+ iterator += 1
1523
+ continue
1524
+ if decision == "accept":
1525
+ _raise_if_hot_head_underresolved(
1526
+ TSYM,
1527
+ state.Pr,
1528
+ state.Sr,
1529
+ state.Hr,
1530
+ settings,
1531
+ stage="step 3",
1532
+ outcome="successful",
1533
+ )
1534
+ _raise_if_large_delta_p_underresolved(
1535
+ TSYM,
1536
+ state.Pr,
1537
+ settings,
1538
+ stage="step 3",
1539
+ outcome="successful",
1540
+ )
1541
+ return float(Tperc), state
1542
+
1543
+ if decision == "limit":
1544
+ _raise_if_hot_head_underresolved(
1545
+ TSYM,
1546
+ state.Pr,
1547
+ state.Sr,
1548
+ state.Hr,
1549
+ settings,
1550
+ stage="step 3",
1551
+ outcome="best-available unresolved-tail",
1552
+ )
1553
+ _raise_if_large_delta_p_underresolved(
1554
+ TSYM,
1555
+ state.Pr,
1556
+ settings,
1557
+ stage="step 3",
1558
+ outcome="best-available unresolved-tail",
1559
+ )
1560
+ return float(Tperc), state
1561
+
1562
+ cold_tail_unresolved = bool(state.Pr[-1] < settings.f_final)
1563
+ if cold_tail_unresolved:
1564
+ lower_bound_reached = abs(float(TSYM[-1]) - float(grid.tmin)) <= 1e-12 * max(abs(float(grid.tmin)), 1.0)
1565
+ if lower_bound_reached:
1566
+ _raise_if_hot_head_underresolved(
1567
+ TSYM,
1568
+ state.Pr,
1569
+ state.Sr,
1570
+ state.Hr,
1571
+ settings,
1572
+ stage="step 3",
1573
+ outcome="eternal-inflation",
1574
+ )
1575
+ _raise_if_large_delta_p_underresolved(
1576
+ TSYM,
1577
+ state.Pr,
1578
+ settings,
1579
+ stage="step 3",
1580
+ outcome="eternal-inflation",
1581
+ )
1582
+ raise errors.EternalInflationError(
1583
+ "The transition cannot completely finish because the true vacuum "
1584
+ "fraction at the lowest evaluated temperature stays below f_final: "
1585
+ f"P(Tmin) = {state.Pr[-1]:2.5g} < {settings.f_final:2.5g} at "
1586
+ f"Tmin = {TSYM[-1] * CF:2.5g} GeV."
1587
+ )
1588
+
1589
+ (
1590
+ Tmax_new,
1591
+ Tmin_new,
1592
+ state.cold_tail_last_temperature,
1593
+ state.cold_tail_last_probability,
1594
+ state.cold_tail_last_integral,
1595
+ state.cold_tail_last_log_gamma_h4,
1596
+ state.cold_tail_post_peak_streak,
1597
+ _,
1598
+ ) = _dynamiczoomwindow_range_targets(
1599
+ TSYM,
1600
+ state.Pr,
1601
+ state.Sr,
1602
+ state.Hr,
1603
+ settings,
1604
+ target_low=grid.tmin,
1605
+ target_high=grid.tmax,
1606
+ iterator=iterator,
1607
+ hot_head_needs_support=hot_head_needs_support,
1608
+ maxit_margin=1,
1609
+ cold_tail_last_temperature=state.cold_tail_last_temperature,
1610
+ cold_tail_last_probability=state.cold_tail_last_probability,
1611
+ cold_tail_last_integral=state.cold_tail_last_integral,
1612
+ cold_tail_last_log_gamma_h4=state.cold_tail_last_log_gamma_h4,
1613
+ cold_tail_post_peak_streak=state.cold_tail_post_peak_streak,
1614
+ )
1615
+
1616
+ candidate_grid = _build_dynamiczoomwindow_range_grid(
1617
+ TSYM,
1618
+ new_high=Tmax_new,
1619
+ new_low=Tmin_new,
1620
+ max_new_points=_range_expand_batch_size(settings, int(remaining_budget)),
1621
+ )
1622
+ if candidate_grid is None or not _apply_candidate_grid(candidate_grid, "range_expand"):
1623
+ _raise_if_large_delta_p_underresolved(
1624
+ TSYM,
1625
+ state.Pr,
1626
+ settings,
1627
+ stage="step 3",
1628
+ outcome="best-available unresolved-tail",
1629
+ )
1630
+ return float(Tperc), state
1631
+ Tperc_prev = Tperc
1632
+ iterator += 1