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,581 @@
1
+ """Single-point evaluation logic with staged plotting callbacks.
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
+ from __future__ import annotations
12
+
13
+ import numpy as np
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+
17
+ from transitionlistener import plots
18
+ from transitionlistener import observability as obs
19
+ from transitionlistener.helper_functions import load_potential
20
+ from transitionlistener import console
21
+
22
+ from .logging_utils import Logger
23
+ from .pipeline import (
24
+ _compute_single_point,
25
+ _build_result_from_context,
26
+ _handle_single_point_error,
27
+ )
28
+
29
+
30
+ _STAGE_NAME_TO_LEVEL = {
31
+ "potential": 0,
32
+ "phases": 1,
33
+ "transitions": 2,
34
+ "observables": 3,
35
+ "gw": 4,
36
+ }
37
+
38
+
39
+ _MASS_GROUP_PREFIX = "mass_spectrum_T0_"
40
+ _MASS_KIND_ORDER = {"boson": 0, "fermion": 1}
41
+
42
+
43
+ def _plot_required_stage(plot_key: str, spec: dict) -> int:
44
+ """Return the minimum pipeline stage needed for a given plot."""
45
+ if plot_key == "potential":
46
+ return 0
47
+ if plot_key == "action":
48
+ return 1
49
+ if plot_key == "energy_density":
50
+ return 1
51
+ if plot_key == "phases":
52
+ include = bool(spec.get("include_transitions?", True))
53
+ return 3 if include else 1
54
+ if plot_key in {"profile", "profileV", "dofs", "percolation"}:
55
+ return 3
56
+ if plot_key == "sensitivities":
57
+ return 0
58
+ if plot_key == "gw_spectrum":
59
+ return 4
60
+ return 4
61
+
62
+
63
+ def _format_numeric_value(key: str, value) -> str:
64
+ """Render numeric output in a consistent fashion."""
65
+ if isinstance(value, (bool, np.bool_)):
66
+ return str(bool(value))
67
+ if isinstance(value, (int, np.integer)):
68
+ return str(int(value))
69
+ if isinstance(value, (float, np.floating)):
70
+ if not np.isfinite(value):
71
+ return "-"
72
+ if key in {"step", "total_steps"}:
73
+ return str(int(round(value)))
74
+ return f"{float(value):.4e}"
75
+ return str(value)
76
+
77
+
78
+ def _format_warning_value(value) -> str:
79
+ """Render warning flags as booleans."""
80
+ if isinstance(value, (float, np.floating)) and not np.isfinite(value):
81
+ return "False"
82
+ if isinstance(value, (int, np.integer, bool, np.bool_)):
83
+ return str(bool(value))
84
+ return str(bool(value))
85
+
86
+
87
+ @dataclass
88
+ class PlotPlan:
89
+ """Information about requested plots and their pipeline requirements."""
90
+
91
+ specs: dict[str, dict]
92
+ ordered_keys: list[str]
93
+ wants_gw_spectrum: bool
94
+ stage_requirements: dict[str, int]
95
+
96
+
97
+ def _build_plot_plan(conf) -> PlotPlan:
98
+ """Extract plot configuration from the user configuration."""
99
+ plot_specs = conf.additional_plots or {}
100
+ active_plot_keys = [
101
+ key
102
+ for key, spec in plot_specs.items()
103
+ if isinstance(spec, dict) and spec.get("plot?", False)
104
+ ]
105
+ wants_gw_spectrum = "gw_spectrum" in active_plot_keys
106
+ ordered_non_gw_keys = [
107
+ key for key in plot_specs.keys() if key in active_plot_keys and key != "gw_spectrum"
108
+ ]
109
+ stage_requirements = {
110
+ key: _plot_required_stage(key, plot_specs[key])
111
+ for key in ordered_non_gw_keys
112
+ }
113
+ return PlotPlan(
114
+ specs=plot_specs,
115
+ ordered_keys=ordered_non_gw_keys,
116
+ wants_gw_spectrum=wants_gw_spectrum,
117
+ stage_requirements=stage_requirements,
118
+ )
119
+
120
+
121
+ class PlotExecutor:
122
+ """Manage additional plots and spectrum rendering during the pipeline."""
123
+
124
+ def __init__(self, conf, input_params: dict, plan: PlotPlan, outpath: str, *, verbose: bool = False):
125
+ """Store plot requests and the pipeline context needed to satisfy them."""
126
+ self._conf = conf
127
+ self._input_params = input_params
128
+ self._plan = plan
129
+ self._outpath = outpath
130
+ self._verbose = verbose
131
+ self._executed: set[str] = set()
132
+
133
+ def stage_callback(self, stage_name: str, ctx: dict):
134
+ """Pipeline hook that triggers plots once prerequisites are met."""
135
+ current_level = _STAGE_NAME_TO_LEVEL.get(stage_name, -1)
136
+ for key in self._plan.ordered_keys:
137
+ if key in self._executed:
138
+ continue
139
+ required_stage = self._plan.stage_requirements.get(key, 4)
140
+ if required_stage <= current_level:
141
+ self._run_additional_plot(key, ctx)
142
+
143
+ if stage_name == "gw" and self._plan.wants_gw_spectrum and "gw_spectrum" not in self._executed:
144
+ self._run_gw_spectrum(ctx)
145
+
146
+ def mark_gw_skipped(self, message: str, colour: str = "red"):
147
+ """Helper to emit a message when the GW spectrum cannot be produced."""
148
+ if self._plan.wants_gw_spectrum and "gw_spectrum" not in self._executed:
149
+ console.print(f"[bold {colour}]{message}[/bold {colour}]")
150
+ self._executed.add("gw_spectrum")
151
+
152
+ def _run_additional_plot(self, key: str, ctx: dict):
153
+ """Execute one configured additional plot and degrade gracefully on failure."""
154
+ self._executed.add(key)
155
+ spec = self._plan.specs.get(key, {})
156
+ try:
157
+ self._run_additional_plot_impl(key, spec, ctx)
158
+ except Exception as err:
159
+ console.print(
160
+ f"[bold yellow]Skipping additional plot '{key}': {err}[/bold yellow]"
161
+ )
162
+
163
+ def _run_additional_plot_impl(self, key: str, spec: dict, ctx: dict):
164
+ """Dispatch one additional-plot request to the matching plotting routine."""
165
+ if key == "sensitivities":
166
+ console.print(f"[bold green]Create {key} plot...[/bold green]")
167
+ gw = obs.GW_Sensitivity_Data()
168
+ gw.foldername = self._outpath
169
+ plots.plotSensitivities(gw, showplot=False, call_from_spectrum=False)
170
+ console.print(f"[bold green]Done: {key} plot.[/bold green]")
171
+ return
172
+
173
+ plotter = plots.TLPlots(
174
+ self._input_params,
175
+ self._conf.modelfile,
176
+ self._conf.potential_name,
177
+ self._outpath,
178
+ self._conf.plot_description,
179
+ potential=ctx.get("pot"),
180
+ phases=ctx.get("phases"),
181
+ transitions=ctx.get("transitions"),
182
+ transition_observables=ctx.get("transition_observables"),
183
+ verbose=self._verbose,
184
+ )
185
+
186
+ console.print(f"[bold green]Create {key} plot...[/bold green]")
187
+ if key == "action":
188
+ phase_indices_cfg = spec.get("phase_indices", [0, 1])
189
+ if not isinstance(phase_indices_cfg, (list, tuple)) or len(phase_indices_cfg) != 2:
190
+ raise ValueError(
191
+ "additional_plots.action.phase_indices must be a list of two phase keys."
192
+ )
193
+ phase_indices = [phase_indices_cfg[0], phase_indices_cfg[1]]
194
+ plotter.plotAction(
195
+ np.array(spec.get("Tmin_GeV", 0), dtype=float),
196
+ np.array(spec.get("Tmax_GeV", 0), dtype=float),
197
+ phase_indices,
198
+ np.array(spec.get("n", 100), dtype=int),
199
+ )
200
+ elif key == "potential":
201
+ plotter.plotPotential(
202
+ np.array(spec.get("T_GeV", 0), dtype=float),
203
+ np.array(spec.get("phi_ranges_GeV", [0, 1]), dtype=float),
204
+ np.array(spec.get("n", 100), dtype=int),
205
+ )
206
+ elif key == "phases":
207
+ include_transitions = bool(spec.get("include_transitions?", True))
208
+ plotter.plotPhases(
209
+ Tmin_GeV=np.array(spec.get("Tmin_GeV", np.nan), dtype=float),
210
+ Tmax_GeV=np.array(spec.get("Tmax_GeV", np.nan), dtype=float),
211
+ include_transitions=include_transitions,
212
+ plot_squaresum=spec.get("plot_squaresum", True)
213
+ )
214
+ elif key == "profileV":
215
+ plotter.plotProfileV(
216
+ np.array(spec.get("field_index_1", 0), dtype=int),
217
+ np.array(spec.get("field_index_2", 1), dtype=int),
218
+ )
219
+ elif key == "energy_density":
220
+ plotter.plotEnergyDensity(
221
+ np.array(spec.get("Tmin_GeV", np.nan), dtype=float),
222
+ np.array(spec.get("Tmax_GeV", np.nan), dtype=float),
223
+ )
224
+ elif key == "dofs":
225
+ plotter.plotDOFs(
226
+ np.array(spec.get("Tmin_GeV", np.nan), dtype=float),
227
+ np.array(spec.get("Tmax_GeV", np.nan), dtype=float),
228
+ )
229
+ elif key == "profile":
230
+ plotter.plotProfile()
231
+ elif key == "percolation":
232
+ n_action_cfg = spec.get("n_action", None)
233
+ n_action = None if n_action_cfg is None else int(np.asarray(n_action_cfg, dtype=int))
234
+ plotter.plotPercolation(
235
+ Tmin_GeV=np.array(spec.get("Tmin_GeV", np.nan), dtype=float),
236
+ Tmax_GeV=np.array(spec.get("Tmax_GeV", np.nan), dtype=float),
237
+ n_action=n_action,
238
+ )
239
+ console.print(f"[bold green]Done: {key} plot.[/bold green]")
240
+
241
+ def _run_gw_spectrum(self, ctx: dict):
242
+ """Render the GW spectrum for the strongest transition when available."""
243
+ strongest = ctx.get("strongest_transition_observables") or {}
244
+ if not strongest:
245
+ self.mark_gw_skipped("Skipping GW spectrum plot because no valid transition was found.", colour="yellow")
246
+ return
247
+ console.print(f"[bold green]Create the gw_spectrum plot[/bold green]")
248
+ plots.plotGWSpectrum(strongest, showplot=False, foldername=self._outpath, legendfontsize=5)
249
+ console.print(f"[bold green]Done: gw_spectrum plot.[/bold green]")
250
+ self._executed.add("gw_spectrum")
251
+
252
+
253
+ def _write_input_parameters(outpath: str, input_params: dict):
254
+ """Write the per-point input parameter dictionary to disk.
255
+
256
+ Numeric entries are rendered with the tabular ``{:e}`` format we use
257
+ throughout. Non-numeric entries (string-valued runtime options like
258
+ ``precision_mode`` or ``percolation_algorithm_mode``) are written as
259
+ plain ``str(value)``; without this fallback the writer would die with
260
+ ``could not convert string to float`` whenever such an option lands in
261
+ the input dict alongside the physical model parameters.
262
+ """
263
+ with open(outpath + "0_Input_params.txt", "w", encoding="utf-8") as file:
264
+ for name, value in input_params.items():
265
+ try:
266
+ numeric = float(value)
267
+ except (TypeError, ValueError):
268
+ file.write("{:<22} {}\n".format(name, str(value)))
269
+ continue
270
+ file.write("{:<22} {:e}\n".format(name, numeric))
271
+
272
+
273
+ def _write_transition_outputs(outpath: str, conf, result: dict) -> float:
274
+ """Write the strongest-transition observables and warning flags to disk."""
275
+ transition_names = list(conf.derived_params.keys())
276
+ strongest = result.get("strongestTransitionObservables", {}) if isinstance(result, dict) else {}
277
+ transition_values = [strongest.get(name, np.nan) for name in transition_names]
278
+
279
+ raw_error_value = result.get("error", np.nan) if isinstance(result, dict) else np.nan
280
+ if isinstance(raw_error_value, np.ndarray):
281
+ error_scalar = raw_error_value.flatten()[0] if raw_error_value.size else np.nan
282
+ else:
283
+ error_scalar = raw_error_value
284
+
285
+ all_names = transition_names + ["error"]
286
+ all_values = transition_values + [error_scalar]
287
+ entries = {name: value for name, value in zip(all_names, all_values)}
288
+
289
+ warning_keys = sorted(
290
+ (k for k in entries if k.upper().startswith("WARNING")),
291
+ key=lambda k: (k.lower(), k),
292
+ )
293
+ error_value = entries.get("error", np.nan)
294
+ numerical_keys = sorted(
295
+ (k for k in entries if k not in warning_keys and k != "error"),
296
+ key=lambda k: (k.lower(), k),
297
+ )
298
+
299
+ with open(outpath + "1_All_params.txt", "w", encoding="utf-8") as file:
300
+ for key in numerical_keys:
301
+ file.write("{:<22} {}\n".format(key, _format_numeric_value(key, entries[key])))
302
+
303
+ if numerical_keys and (warning_keys or "error" in entries):
304
+ file.write("\n")
305
+
306
+ if warning_keys:
307
+ file.write("Warnings:\n")
308
+ warning_labels = [
309
+ key.split("WARNING:", 1)[1] if ":" in key else key
310
+ for key in warning_keys
311
+ ]
312
+ max_len = max(len(label) for label in warning_labels)
313
+ for key in warning_keys:
314
+ label = key.split("WARNING:", 1)[1] if ":" in key else key
315
+ file.write(f" {label:<{max_len}} {_format_warning_value(entries[key])}\n")
316
+
317
+ if warning_keys and "error" in entries:
318
+ file.write("\n")
319
+
320
+ if "error" in entries:
321
+ if isinstance(error_value, (float, np.floating)):
322
+ error_str = "-" if not np.isfinite(error_value) else f"{float(error_value):.4e}"
323
+ elif isinstance(error_value, (int, np.integer)):
324
+ error_str = str(int(error_value))
325
+ else:
326
+ error_str = str(error_value)
327
+ file.write("{:<22} {}\n".format("error", error_str))
328
+
329
+ return error_scalar
330
+
331
+
332
+ def _write_observability_outputs(outpath: str, result: dict, had_error: bool):
333
+ """Write detector-by-detector SNR and observability summaries to disk."""
334
+ if had_error:
335
+ msg = "Skipping observability output because errors were detected."
336
+ console.print(f"[bold red]{msg}[/bold red]")
337
+ return
338
+
339
+ observability = result.get("observability", {}) if isinstance(result, dict) else {}
340
+ if not observability:
341
+ return
342
+
343
+ detectors: list[str] = []
344
+ for key in observability.keys():
345
+ detector = None
346
+ if key.endswith("_SNR"):
347
+ detector = key[:-4]
348
+ elif key.endswith("_detectable"):
349
+ detector = key[:-11]
350
+ if detector and detector not in detectors:
351
+ detectors.append(detector)
352
+
353
+ with open(outpath + "2_Observability.txt", "w", encoding="utf-8") as file:
354
+ file.write(
355
+ "{:<20} {:<15} {:<31} {:<12}\n".format(
356
+ "Observatory",
357
+ "SNR",
358
+ "Should have been observed already?",
359
+ "Observable?",
360
+ )
361
+ )
362
+ file.write("-" * 90 + "\n")
363
+ for det in detectors:
364
+ snr = observability.get(f"{det}_SNR", float("nan"))
365
+ detectable = observability.get(f"{det}_detectable", False)
366
+ is_existing = det in obs.EXISTING_OBSERVATORIES
367
+ should_str = (
368
+ _format_numeric_value("detectable", detectable)
369
+ if is_existing
370
+ else "-"
371
+ )
372
+ observable_str = (
373
+ _format_numeric_value("detectable", detectable)
374
+ if not is_existing
375
+ else "-"
376
+ )
377
+ file.write(
378
+ "{:<20} {:<15} {:<31} {:<12}\n".format(
379
+ det,
380
+ _format_numeric_value("snr", snr),
381
+ should_str,
382
+ observable_str,
383
+ )
384
+ )
385
+ file.write("\n")
386
+
387
+ logL_keys = [k for k in observability if k.startswith("logL_")]
388
+ printed_keys = set()
389
+ if logL_keys:
390
+ file.write("PTA likelihood summary:\n")
391
+ file.write("{:<16} {:>6} {:<12} {:>12} {:>12} {:>10} {:>18}\n".format(
392
+ "PTA", "Bins", "Variant", "lnL", "Delta lnL", "Sigma", "Within 3 sigma?"
393
+ ))
394
+ file.write("-" * 90 + "\n")
395
+ for label, settings in obs.PTA_LIKELIHOOD_SETTINGS.items():
396
+ bins = int(settings["nbins"])
397
+ display_name = obs.PTA_DISPLAY_NAMES.get(label, label)
398
+ first_row = True
399
+ for variant in obs.PTA_LIKELIHOOD_VARIANTS:
400
+ lnL_key = f"lnL_{variant}_{label}"
401
+ delta_key = f"delta_lnL_{variant}_{label}"
402
+ sigma_key = f"sigma_{variant}_{label}"
403
+ within_key = f"within_3sigma_{variant}_{label}"
404
+ printed_keys.update({lnL_key, delta_key, sigma_key, within_key})
405
+
406
+ lnL_val = observability.get(lnL_key, np.nan)
407
+ delta_val = observability.get(delta_key, np.nan)
408
+ sigma_val = observability.get(sigma_key, np.nan)
409
+ within_val = observability.get(within_key, np.nan)
410
+
411
+ lnL_str = f"{lnL_val:.2f}" if np.isfinite(lnL_val) else "nan"
412
+ delta_str = f"{delta_val:.2f}" if np.isfinite(delta_val) else "nan"
413
+ sigma_str = f"{sigma_val:.2f}" if np.isfinite(sigma_val) else "nan"
414
+ within_str = ""
415
+ if variant == "PTArcade":
416
+ if isinstance(within_val, (bool, np.bool_)):
417
+ within_str = str(bool(within_val))
418
+ elif np.isfinite(within_val):
419
+ within_str = str(bool(within_val < obs.DELTA_LNL_WITHIN_THRESHOLD))
420
+
421
+ name_col = display_name if first_row else ""
422
+ file.write("{:<16} {:>6} {:<12} {:>12} {:>12} {:>10} {:>18}\n".format(
423
+ name_col, bins, variant, lnL_str, delta_str, sigma_str, within_str
424
+ ))
425
+ first_row = False
426
+ file.write("\n")
427
+
428
+ written_keys = {
429
+ f"{det}_SNR" for det in detectors
430
+ } | {f"{det}_detectable" for det in detectors}
431
+ if logL_keys:
432
+ written_keys.update(logL_keys)
433
+ written_keys.update(printed_keys)
434
+
435
+ additional_rows: list[tuple[str, str]] = []
436
+ additional_written: set[str] = set()
437
+ mass_rows: list[tuple[tuple[int, int], str, str, str]] = []
438
+ for key, value in observability.items():
439
+ if not key.endswith("_lin"):
440
+ continue
441
+ base_key = key[:-4]
442
+ additional_written.add(key)
443
+ log_key = f"{base_key}_log10"
444
+ latex_key = f"{base_key}_latex"
445
+ text_key = f"{base_key}_text"
446
+ kind_key = f"{base_key}_kind"
447
+ for meta_key in (log_key, latex_key, text_key, kind_key, base_key):
448
+ if meta_key in observability:
449
+ additional_written.add(meta_key)
450
+
451
+ if base_key.startswith(_MASS_GROUP_PREFIX):
452
+ suffix = base_key[len(_MASS_GROUP_PREFIX):]
453
+ kind_hint = observability.get(kind_key, "boson")
454
+ try:
455
+ kind_suffix, index_str = suffix.split("_", 1)
456
+ except ValueError:
457
+ kind_suffix, index_str = kind_hint, "0"
458
+ try:
459
+ index_val = int(index_str)
460
+ except ValueError:
461
+ index_val = 0
462
+ kind = str(kind_hint or kind_suffix)
463
+ order = (_MASS_KIND_ORDER.get(kind, 99), index_val)
464
+ text_label = observability.get(text_key)
465
+ latex_label = observability.get(latex_key, text_label or base_key)
466
+ formatted_mass = _format_numeric_value(base_key, value)
467
+ mass_rows.append((order, str(text_label or base_key), str(latex_label), formatted_mass))
468
+ continue
469
+
470
+ additional_rows.append((base_key, _format_numeric_value(base_key, value)))
471
+
472
+ if additional_rows:
473
+ file.write("Additional information on GW spectrum:\n")
474
+ file.write("{:<30} {:>15}\n".format("Quantity", "Value"))
475
+ file.write("-" * 48 + "\n")
476
+ for name, formatted in additional_rows:
477
+ file.write("{:<30} {:>15}\n".format(name, formatted))
478
+ file.write("\n")
479
+
480
+ written_keys.update(additional_written)
481
+
482
+ if mass_rows:
483
+ file.write("Zero-temperature mass spectrum (GeV):\n")
484
+ file.write("{:<30} {:>15}\n".format("Particle", "Mass"))
485
+ file.write("-" * 48 + "\n")
486
+ for _order, text_label, latex_label, formatted in sorted(mass_rows, key=lambda item: item[0]):
487
+ display = text_label
488
+ file.write("{:<30} {:>15}\n".format(display, formatted))
489
+ file.write("\n")
490
+
491
+ remaining_keys = [k for k in observability if k not in written_keys]
492
+
493
+ warning_keys = sorted(
494
+ (k for k in remaining_keys if k.upper().startswith("WARNING")),
495
+ key=lambda k: (k.lower(), k),
496
+ )
497
+ error_key = "error" if "error" in remaining_keys else None
498
+ numerical_keys = sorted(
499
+ (k for k in remaining_keys if k not in warning_keys and k != error_key),
500
+ key=lambda k: (k.lower(), k),
501
+ )
502
+
503
+ for key in numerical_keys:
504
+ file.write("{:<22} {}\n".format(key, _format_numeric_value(key, observability.get(key))))
505
+
506
+ if numerical_keys and (warning_keys or error_key):
507
+ file.write("\n")
508
+
509
+ if warning_keys:
510
+ file.write("Warnings:\n")
511
+ warning_labels = [
512
+ key.split("WARNING:", 1)[1] if ":" in key else key
513
+ for key in warning_keys
514
+ ]
515
+ max_len = max(len(label) for label in warning_labels)
516
+ for key in warning_keys:
517
+ label = key.split("WARNING:", 1)[1] if ":" in key else key
518
+ file.write(f" {label:<{max_len}} {_format_warning_value(observability.get(key))}\n")
519
+
520
+ if warning_keys and error_key:
521
+ file.write("\n")
522
+
523
+ if error_key:
524
+ value = observability.get(error_key)
525
+ if isinstance(value, (float, np.floating)) and not np.isfinite(value):
526
+ value_str = "-"
527
+ elif isinstance(value, (float, np.floating)):
528
+ value_str = f"{float(value):.4e}"
529
+ elif isinstance(value, (int, np.integer)):
530
+ value_str = str(int(value))
531
+ else:
532
+ value_str = str(value)
533
+ file.write("{:<22} {}\n".format("error", value_str))
534
+
535
+
536
+ def single(conf, verbose: bool = False):
537
+ """Evaluate a single parameter point and generate the requested plots."""
538
+ outpath = conf.output_path
539
+ Path(outpath).mkdir(parents=True, exist_ok=True)
540
+
541
+ input_params = {key: val for key, val in conf.params.items()}
542
+ plot_plan = _build_plot_plan(conf)
543
+ plot_executor = PlotExecutor(conf, input_params, plot_plan, outpath, verbose=verbose)
544
+
545
+ potential = load_potential(conf.modelfile, conf.potential_name)
546
+ errorlogger = Logger(outpath + "error.log")
547
+ resultlogger = Logger(outpath + "result.log")
548
+
549
+ stage_callback = (
550
+ plot_executor.stage_callback
551
+ if (plot_plan.ordered_keys or plot_plan.wants_gw_spectrum)
552
+ else None
553
+ )
554
+
555
+ context, error = _compute_single_point(
556
+ input_params,
557
+ potential,
558
+ conf.timeout,
559
+ verbose,
560
+ include_smbhb=False,
561
+ max_stage=4,
562
+ stage_callback=stage_callback,
563
+ )
564
+
565
+ if error is not None:
566
+ result = _handle_single_point_error(error, input_params, errorlogger, context)
567
+ plot_executor.mark_gw_skipped(
568
+ "Skipping GW spectrum plot because errors were detected before the spectrum stage."
569
+ )
570
+ else:
571
+ result = _build_result_from_context(context)
572
+ observability = result.get("observability", {})
573
+ line = str(input_params) + ":" + str(observability)
574
+ resultlogger.log(line + "\n")
575
+
576
+ console.print("[bold green]Phase transition analysis done. Saving the results...[/bold green]")
577
+ _write_input_parameters(outpath, input_params)
578
+
579
+ error_scalar = _write_transition_outputs(outpath, conf, result)
580
+ had_error = not np.isnan(error_scalar)
581
+ _write_observability_outputs(outpath, result, had_error)
@@ -0,0 +1,61 @@
1
+ """Shared global state and runtime configuration for the TL interface.
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
+ from __future__ import annotations
12
+ import signal
13
+ import warnings
14
+ import numpy as np
15
+ from transitionlistener.errors import Timeout
16
+
17
+ DEBUGMODE: bool = False
18
+ TIMEOUT: int = 300
19
+ model = None
20
+ potential = None
21
+ errorlogger = None
22
+ resultlogger = None
23
+ config = None
24
+
25
+ np.random.seed(19971104)
26
+
27
+ _SUPPRESSED_WARNINGS = [
28
+ "invalid value encountered in subtract",
29
+ "overflow encountered in add",
30
+ "invalid value encountered in multiply",
31
+ "overflow encountered in multiply",
32
+ "divide by zero encountered in log",
33
+ "invalid value encountered in scalar subtract",
34
+ "overflow encountered in scalar multiply",
35
+ "divide by zero encountered in scalar divide",
36
+ "invalid value encountered in scalar divide",
37
+ "invalid value encountered in scalar multiply",
38
+ "invalid value encountered in divide",
39
+ "divide by zero encountered in divide",
40
+ "invalid value encountered in scalar divide",
41
+ ]
42
+
43
+
44
+ def set_debugmode(enabled: bool) -> None:
45
+ """Toggle debug mode and update warning filters accordingly."""
46
+ global DEBUGMODE
47
+ DEBUGMODE = bool(enabled)
48
+ warnings.resetwarnings()
49
+ if not DEBUGMODE:
50
+ for warning in _SUPPRESSED_WARNINGS:
51
+ warnings.filterwarnings("ignore", message=f".*{warning}.*", category=RuntimeWarning)
52
+
53
+
54
+ set_debugmode(DEBUGMODE)
55
+
56
+ def timeout_handler(signum, frame): # pragma: no cover - deterministic signal hook
57
+ """Raise a :class:`Timeout` when a POSIX alarm fires."""
58
+ msg = f"The calculation timed out after {TIMEOUT} seconds."
59
+ raise Timeout(msg)
60
+
61
+ signal.signal(signal.SIGALRM, timeout_handler)