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,592 @@
1
+ """Helpers to visualise grid scans of TransitionListener observables.
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 re
14
+
15
+ from .observability import GW_Sensitivity_Data
16
+ import numpy as np
17
+ import matplotlib.pyplot as plt
18
+ from matplotlib.collections import QuadMesh
19
+ from matplotlib.ticker import FuncFormatter, MaxNLocator
20
+ from .colors import *
21
+ from .plot_settings import plot_settings as TL_plot_settings
22
+ from .plot_settings import add_TL_logo
23
+ from .plot_settings import TLcmap
24
+
25
+ _MASS_GROUP_PREFIX = "mass_spectrum_T0_"
26
+ _MASS_KIND_ORDER = {"boson": 0, "fermion": 1}
27
+
28
+ plt.rcParams.update(TL_plot_settings)
29
+
30
+
31
+ def _as_scalar_or_nan(value) -> float:
32
+ """Convert an arbitrary value to a scalar float when possible."""
33
+ try:
34
+ arr = np.asarray(value)
35
+ except Exception:
36
+ return np.nan
37
+ if arr.size == 0:
38
+ return np.nan
39
+ if arr.size == 1:
40
+ try:
41
+ return float(np.squeeze(arr))
42
+ except Exception:
43
+ return np.nan
44
+ return np.nan
45
+
46
+
47
+ def _detectable_from_snr(snr: float, threshold: float) -> bool:
48
+ """Infer detectability from SNR when legacy scan exports omit the flag."""
49
+ return bool(
50
+ np.isfinite(snr)
51
+ and np.isfinite(threshold)
52
+ and snr > 0.0
53
+ and threshold > 0.0
54
+ and snr > threshold
55
+ )
56
+
57
+
58
+ def _prepare_axes(ax : plt.Axes):
59
+ """Apply common styling to the axes of a grid plot."""
60
+ ax.tick_params(axis='both', which='both',
61
+ direction="in", width=0.5, zorder=1000)
62
+ ax.xaxis.set_ticks_position('both')
63
+ ax.yaxis.set_ticks_position('both')
64
+ for axis in ["top", 'bottom','left','right']:
65
+ ax.spines[axis].set_linewidth(0.5)
66
+ ax.spines[axis].set_zorder(10000)
67
+
68
+
69
+ def _pow10_tick_formatter(value: float, _pos: float) -> str:
70
+ """Format a log10-space value as 10^x."""
71
+ if not np.isfinite(value):
72
+ return ""
73
+ rounded = int(np.round(value))
74
+ if abs(value - rounded) < 1e-8:
75
+ return rf"$10^{{{rounded}}}$"
76
+ return rf"$10^{{{value:.1f}}}$"
77
+
78
+
79
+ def _style_log_colorbar(cbar) -> None:
80
+ """Display colorbar ticks (stored in log10 space) as powers of ten."""
81
+ cbar.locator = MaxNLocator(nbins=5, integer=True)
82
+ cbar.formatter = FuncFormatter(_pow10_tick_formatter)
83
+ cbar.update_ticks()
84
+
85
+ def _finish_plot(fig : plt.Figure, ax: plt.Axes, xy_plot_names: tuple[str, str],
86
+ im : QuadMesh, scale: str, title: str, save_path: str):
87
+ """Configure the axes of a grid plot and save the figure."""
88
+ _prepare_axes(ax)
89
+ fig.colorbar(im, ax=ax)
90
+ ax.set_title(title)
91
+ ax.set_xlabel(xy_plot_names[0])
92
+ ax.set_ylabel(xy_plot_names[1])
93
+ if scale == 'linlog':
94
+ ax.set_yscale('log')
95
+ if scale == 'loglin':
96
+ ax.set_xscale('log')
97
+ if scale == 'loglog':
98
+ ax.set_xscale('log')
99
+ ax.set_yscale('log')
100
+ add_TL_logo(loc="upper right", ax=ax)
101
+ plt.savefig(save_path)
102
+ plt.close(fig)
103
+
104
+
105
+ class plot2dData(GW_Sensitivity_Data):
106
+ """Render two-dimensional scan outputs as heatmaps and diagnostic plots."""
107
+
108
+ def __init__(
109
+ self,
110
+ data_dict_array,
111
+ x,
112
+ y,
113
+ scale,
114
+ xy_plot_names,
115
+ foldername,
116
+ derived_params_names,
117
+ derived_params_plot_names,
118
+ overview_title,
119
+ show_scan_points=True,
120
+ ):
121
+ """Cache the scan data and pre-compute helper arrays for plotting.
122
+
123
+ Parameters
124
+ ----------
125
+ data_dict_array : numpy.ndarray
126
+ ``(N, N)`` grid of dictionaries produced by ``run_TL``.
127
+ x, y : numpy.ndarray
128
+ Grid coordinates for the scan axes.
129
+ scale : str
130
+ Axis scaling mode. One of ``\"linlin\"``, ``\"linlog\"``, ``\"loglin\"`` or
131
+ ``\"loglog\"``.
132
+ xy_plot_names : tuple[str, str]
133
+ Labels for the x and y axes (LaTeX strings supported).
134
+ foldername : str
135
+ Output directory where exported figures and tables are written.
136
+ derived_params_names : Iterable[str]
137
+ Keys of derived observables to extract from ``strongestTransitionObservables``.
138
+ derived_params_plot_names : Iterable[str]
139
+ Human-readable labels corresponding to ``derived_params_names``.
140
+ overview_title : str
141
+ Title used in the overview figure.
142
+ """
143
+ super().__init__()
144
+ self.foldername = foldername
145
+ self.data_dict_array = data_dict_array
146
+ self.scale = scale
147
+ self.xy_plot_names = xy_plot_names
148
+ self.derived_params_names = derived_params_names
149
+ self.derived_params_plot_names = derived_params_plot_names
150
+ self.overview_title = overview_title
151
+ self.show_scan_points = show_scan_points
152
+
153
+ if self.scale == "linlin":
154
+ self.x = np.append(x, 2*x[-1] - x[-2])
155
+ self.y = np.append(y, 2*y[-1] - y[-2])
156
+ elif self.scale == "linlog":
157
+ self.x = np.append(x, 2*x[-1] - x[-2])
158
+ self.y = np.append(
159
+ y, y[-1] + 10**(np.log10(y[-1]) - np.log10(y[0])) * (y[1] - y[0]))
160
+ elif self.scale == "loglin":
161
+ self.x = np.append(
162
+ x, x[-1] + 10**(np.log10(x[-1]) - np.log10(x[0])) * (x[1] - x[0]))
163
+ self.y = np.append(y, 2*y[-1] - y[-2])
164
+ elif self.scale == "loglog":
165
+ self.x = np.append(
166
+ x, x[-1] + 10**(np.log10(x[-1]) - np.log10(x[0])) * (x[1] - x[0]))
167
+ self.y = np.append(
168
+ y, y[-1] + 10**(np.log10(y[-1]) - np.log10(y[0])) * (y[1] - y[0]))
169
+
170
+ self.gridsize = len(x)
171
+ self.SNR, self.detectable, self.logL = self.retrieve_observability_data()
172
+ self.derived_params_array_dict = self.retrieve_derived_params()
173
+ self.add_spec_info = self.retrieve_add_spec_info()
174
+
175
+ self.log_params = dict()
176
+ self.lin_params = dict()
177
+
178
+ for name in self.derived_params_names:
179
+ with np.errstate(invalid="ignore", divide="ignore"):
180
+ self.log_params[name] = np.log10(self.derived_params_array_dict[name])
181
+ self.lin_params[name] = self.derived_params_array_dict[name]
182
+
183
+ def retrieve_observability_data(self):
184
+ """Extract detector SNRs, detectability flags and PTA log-likelihoods."""
185
+ n = self.gridsize
186
+ SNR = np.full((n, n, len(self.det_names)), np.nan)
187
+ detectable = np.full((n, n, len(self.det_names)), False)
188
+ logL = np.full((n, n, len(self.PTA_names)), np.nan)
189
+
190
+ for i in range(n):
191
+ for j in range(n):
192
+ if "observability" in self.data_dict_array[i, j]:
193
+ obs_dict = self.data_dict_array[i, j]["observability"]
194
+ for k, dn in enumerate(self.det_names):
195
+ if dn + "_SNR" in obs_dict:
196
+ SNR[i, j, k] = _as_scalar_or_nan(
197
+ obs_dict[dn + "_SNR"]
198
+ )
199
+ if dn + "_detectable" in obs_dict:
200
+ detectable[i, j, k] = bool(
201
+ _as_scalar_or_nan(
202
+ obs_dict[dn + "_detectable"]
203
+ )
204
+ )
205
+ else:
206
+ detectable[i, j, k] = _detectable_from_snr(
207
+ SNR[i, j, k],
208
+ self.det[dn]["thr"],
209
+ )
210
+ for l, pl in enumerate(self.PTA_labels):
211
+ if "logL_" + pl in obs_dict:
212
+ logL[i, j, l] = _as_scalar_or_nan(
213
+ obs_dict["logL_" + pl]
214
+ )
215
+ return SNR, detectable, logL
216
+
217
+ def retrieve_derived_params(self):
218
+ """Collect derived parameters and error codes from the scan output."""
219
+ n = self.gridsize
220
+ derived_params = dict()
221
+ for name in self.derived_params_names:
222
+ derived_params[name] = np.full((n, n), np.nan, dtype=float)
223
+ derived_params["error"] = np.full((n, n), np.nan, dtype=float)
224
+
225
+ for i in range(n):
226
+ for j in range(n):
227
+ for name in self.derived_params_names:
228
+ if "strongestTransitionObservables" in self.data_dict_array[i, j]:
229
+ if name in self.data_dict_array[i, j]["strongestTransitionObservables"]:
230
+ derived_params[name][i, j] = _as_scalar_or_nan(
231
+ self.data_dict_array[i, j]["strongestTransitionObservables"][name]
232
+ )
233
+ if "error" in self.data_dict_array[i, j]:
234
+ derived_params["error"][i, j] = _as_scalar_or_nan(
235
+ self.data_dict_array[i, j]["error"]
236
+ )
237
+ return derived_params
238
+
239
+ def retrieve_add_spec_info(self):
240
+ """Gather additional observability metadata that is not an SNR or log-likelihood."""
241
+ n = self.gridsize
242
+
243
+ # Identify which auxiliary quantities are available across the grid.
244
+ add_spec_names = set()
245
+ latex_labels: dict[str, str | None] = {}
246
+ text_labels: dict[str, str | None] = {}
247
+ kind_labels: dict[str, str | None] = {}
248
+ for i in range(n):
249
+ for j in range(n):
250
+ obs_dict = self.data_dict_array[i, j].get("observability", {})
251
+ for key in obs_dict:
252
+ if any(marker in key for marker in ["SNR", "logL", "detectable"]):
253
+ continue
254
+ if key.endswith("_lin"):
255
+ base_name = key[:-4]
256
+ add_spec_names.add(base_name)
257
+ latex_labels.setdefault(base_name, obs_dict.get(f"{base_name}_latex"))
258
+ text_labels.setdefault(base_name, obs_dict.get(f"{base_name}_text"))
259
+ kind_labels.setdefault(base_name, obs_dict.get(f"{base_name}_kind"))
260
+
261
+ mass_group_members: list[tuple[int, int, str, str | None]] = []
262
+ for name in add_spec_names:
263
+ if not name.startswith(_MASS_GROUP_PREFIX):
264
+ continue
265
+ suffix = name[len(_MASS_GROUP_PREFIX):]
266
+ parts = suffix.split("_", 1)
267
+ if len(parts) != 2:
268
+ continue
269
+ kind, index_str = parts
270
+ try:
271
+ index = int(index_str)
272
+ except ValueError:
273
+ continue
274
+ order = _MASS_KIND_ORDER.get(kind, 99)
275
+ mass_group_members.append((order, index, name, kind))
276
+
277
+ groups = {}
278
+ if mass_group_members:
279
+ groups["mass_spectrum_T0"] = [
280
+ {"name": name, "index": index, "kind": kind}
281
+ for order, index, name, kind in sorted(mass_group_members)
282
+ ]
283
+
284
+ add_spec_info_lin = {
285
+ name: np.full((n, n), np.nan, dtype=float) for name in add_spec_names
286
+ }
287
+ add_spec_info_log10 = {
288
+ name: np.full((n, n), np.nan, dtype=float) for name in add_spec_names
289
+ }
290
+
291
+ for i in range(n):
292
+ for j in range(n):
293
+ obs_dict = self.data_dict_array[i, j].get("observability", {})
294
+ for name in add_spec_names:
295
+ lin_key = f"{name}_lin"
296
+ log_key = f"{name}_log10"
297
+ if lin_key in obs_dict:
298
+ add_spec_info_lin[name][i, j] = _as_scalar_or_nan(obs_dict[lin_key])
299
+ if log_key in obs_dict:
300
+ add_spec_info_log10[name][i, j] = _as_scalar_or_nan(obs_dict[log_key])
301
+ if name not in latex_labels or latex_labels[name] is None:
302
+ latex_labels[name] = obs_dict.get(f"{name}_latex", latex_labels.get(name))
303
+ if name not in text_labels or text_labels[name] is None:
304
+ text_labels[name] = obs_dict.get(f"{name}_text", text_labels.get(name))
305
+ if name not in kind_labels or kind_labels[name] is None:
306
+ kind_labels[name] = obs_dict.get(f"{name}_kind", kind_labels.get(name))
307
+
308
+ add_spec_info = {
309
+ "lin": add_spec_info_lin,
310
+ "log10": add_spec_info_log10,
311
+ "latex": {name: latex_labels.get(name, name) for name in add_spec_names},
312
+ "text": {name: text_labels.get(name) for name in add_spec_names},
313
+ "kind": {name: kind_labels.get(name) for name in add_spec_names},
314
+ "groups": groups,
315
+ }
316
+ return add_spec_info
317
+
318
+ def plot_SNRs(self):
319
+ """Save heatmaps of the signal-to-noise ratio for every detector."""
320
+ for dn in self.det_names:
321
+ detector_index = self.det_names.index(dn)
322
+ logSNR = np.log10(self.SNR[:, :, detector_index])
323
+ detectable = self.detectable[:, :, detector_index]
324
+ blackdots = np.full_like(logSNR, 1)
325
+ blackdots[~detectable] = 0
326
+ logSNR[~detectable] = np.nan
327
+
328
+ fig, ax = plt.subplots()
329
+ ax.pcolormesh(self.x, self.y, blackdots.T, cmap="gist_gray")
330
+ im = ax.pcolormesh(self.x, self.y, logSNR.T, cmap=TLcmap)
331
+ _finish_plot(fig, ax, self.xy_plot_names,
332
+ im, self.scale,
333
+ R"$\mathrm{log}_{10}$ SNR for "+self.det[dn]["plot_name"],
334
+ self.foldername+"SNR_"+dn+".pdf")
335
+
336
+ def plot_logLs(self):
337
+ """Save heatmaps of the PTA log-likelihood for every pulsar collaboration."""
338
+ for pl in self.PTA_labels:
339
+ detector_index = self.PTA_labels.index(pl)
340
+ logL = self.logL [:, :, detector_index]
341
+ fig, ax = plt.subplots()
342
+ im = ax.pcolormesh(self.x, self.y, logL.T, cmap=TLcmap)
343
+ _finish_plot(fig, ax, self.xy_plot_names,
344
+ im, self.scale,
345
+ R"$\log L$ for "+pl,
346
+ self.foldername+"logL_"+pl+".pdf")
347
+
348
+ def save_SNRs(self):
349
+ """Export the log10 SNR grids as plain-text tables."""
350
+ for dn in self.det_names:
351
+ detector_index = self.det_names.index(dn)
352
+ np.seterr(invalid="ignore")
353
+ logSNR = np.log10(self.SNR[:, :, detector_index])
354
+ np.seterr(invalid="warn")
355
+ np.savetxt(self.foldername+"log_SNR_"+dn+".txt", logSNR)
356
+
357
+ def save_logLs(self):
358
+ """Export the PTA log-likelihood grids as plain-text tables."""
359
+ for i, pl in enumerate(self.PTA_labels):
360
+ logL = self.logL[:, :, i]
361
+ np.savetxt(self.foldername+"log_L_"+pl+".txt", logL)
362
+
363
+ def plot_add_infos(self):
364
+ """Visualise any auxiliary observability fields that were stored."""
365
+ # This method plots both the additional spectra information and the error keys
366
+ add_spec_info_lin = self.add_spec_info["lin"]
367
+ add_spec_info_log10 = self.add_spec_info["log10"]
368
+ add_spec_info_latex = self.add_spec_info["latex"]
369
+ mass_group = {
370
+ member.get("name")
371
+ for member in self.add_spec_info.get("groups", {}).get("mass_spectrum_T0", [])
372
+ }
373
+
374
+ for pname in add_spec_info_lin.keys():
375
+ if pname in mass_group:
376
+ continue
377
+ param_lin = add_spec_info_lin[pname]
378
+ param_log10 = add_spec_info_log10.get(pname, np.full_like(param_lin, np.nan))
379
+ param_latex = add_spec_info_latex.get(pname, pname)
380
+
381
+ if np.all(np.isnan(param_lin)) and np.all(np.isnan(param_log10)):
382
+ continue
383
+ fig, ax = plt.subplots()
384
+ im = ax.pcolormesh(self.x, self.y, param_lin.T, cmap=TLcmap)
385
+ _finish_plot(fig, ax, self.xy_plot_names,
386
+ im, self.scale,
387
+ param_latex,
388
+ self.foldername+"Add_info_"+pname+".pdf")
389
+
390
+ if not np.all(np.isnan(param_log10)):
391
+ fig, ax = plt.subplots()
392
+ im = ax.pcolormesh(self.x, self.y, param_log10.T, cmap=TLcmap)
393
+ _finish_plot(fig, ax, self.xy_plot_names,
394
+ im, self.scale,
395
+ r"$\log_{10}$" + param_latex,
396
+ self.foldername+"Add_info_log10_"+pname+".pdf")
397
+
398
+ def plot_mass_spectrum(self):
399
+ """Render the zero-temperature mass spectrum as per-particle grids."""
400
+ groups = self.add_spec_info.get("groups", {})
401
+ mass_group = groups.get("mass_spectrum_T0", [])
402
+ if not mass_group:
403
+ return
404
+
405
+ latex_labels = self.add_spec_info["latex"]
406
+ text_labels = self.add_spec_info.get("text", {})
407
+
408
+ def _math_content(label: str) -> str:
409
+ if label.startswith("$") and label.endswith("$") and len(label) >= 2:
410
+ return label[1:-1]
411
+ return label
412
+
413
+ def _safe_component(component: str | None, fallback: str) -> str:
414
+ if not component:
415
+ return fallback
416
+ safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", component.strip())
417
+ safe = safe.strip("._")
418
+ return safe or fallback
419
+
420
+ for member in mass_group:
421
+ name = member.get("name")
422
+ index = member.get("index", 0)
423
+ kind = member.get("kind", "boson")
424
+ latex_label = latex_labels.get(name, name)
425
+ math_label = _math_content(latex_label)
426
+ text_label = text_labels.get(name)
427
+ file_stub = _safe_component(text_label, f"{kind}_{index:02d}")
428
+
429
+ linear_title = rf"${math_label} / \text{{GeV}}$"
430
+ log_title = rf"$\log_{{10}} {math_label} / \text{{GeV}}$"
431
+
432
+ lin_values = self.add_spec_info["lin"].get(name)
433
+ if lin_values is not None and not np.all(np.isnan(lin_values)):
434
+ fig, ax = plt.subplots()
435
+ im = ax.pcolormesh(self.x, self.y, lin_values.T, cmap=TLcmap)
436
+ _finish_plot(
437
+ fig,
438
+ ax,
439
+ self.xy_plot_names,
440
+ im,
441
+ self.scale,
442
+ linear_title,
443
+ self.foldername + f"Mass_spectrum_T0_lin_{file_stub}.pdf",
444
+ )
445
+
446
+ log_values = self.add_spec_info["log10"].get(name)
447
+ if log_values is not None and not np.all(np.isnan(log_values)):
448
+ fig, ax = plt.subplots()
449
+ im = ax.pcolormesh(self.x, self.y, log_values.T, cmap=TLcmap)
450
+ _finish_plot(
451
+ fig,
452
+ ax,
453
+ self.xy_plot_names,
454
+ im,
455
+ self.scale,
456
+ log_title,
457
+ self.foldername + f"Mass_spectrum_T0_log10_{file_stub}.pdf",
458
+ )
459
+
460
+ def plot_errors(self):
461
+ """Plot the error-code grid released by the CLI."""
462
+ errors = self.derived_params_array_dict["error"]
463
+ fig, ax = plt.subplots()
464
+ im = ax.pcolormesh(self.x, self.y, errors.T, cmap=TLcmap)
465
+
466
+ _finish_plot(fig, ax, self.xy_plot_names,
467
+ im, self.scale,
468
+ "Errorcodes",
469
+ self.foldername+"Errors.pdf")
470
+
471
+ def save_add_infos(self):
472
+ """Save the additional observability information as plain-text tables."""
473
+ add_spec_info_lin = self.add_spec_info["lin"]
474
+ add_spec_info_log10 = self.add_spec_info["log10"]
475
+ if not add_spec_info_lin:
476
+ return
477
+
478
+ mass_group = {
479
+ member.get("name")
480
+ for member in self.add_spec_info.get("groups", {}).get("mass_spectrum_T0", [])
481
+ }
482
+
483
+ for pname, grid in add_spec_info_lin.items():
484
+ if pname in mass_group:
485
+ continue
486
+ np.savetxt(self.foldername+"Add_info_lin_"+pname+".txt", grid)
487
+ if pname in add_spec_info_log10:
488
+ np.savetxt(
489
+ self.foldername+"Add_info_log10_"+pname+".txt",
490
+ add_spec_info_log10[pname],
491
+ )
492
+
493
+ def plot_overview(self, overview_detector_name="LISA",
494
+ overview_param_names= ("alpha", "RH", "Treh_SM_GeV"),
495
+ overview_param_plot_names=(R"$\alpha_\mathrm{tot}$",
496
+ R"$RH_\mathrm{perc}$", R"$T^{\mathrm{reh}}_{\mathrm{SM}}$ / $\mathrm{GeV}$"),
497
+ figsize=None):
498
+ """Create a four-panel summary containing three observables and one SNR."""
499
+
500
+ # Define parameters to plot (3 GW params + 1 SNR)
501
+ param_names = overview_param_names
502
+ param_plot_names = list(overview_param_plot_names)
503
+ SNR_name = overview_detector_name
504
+ detector_label = str(overview_detector_name).replace("_", " ")
505
+ SNR_plot_name = f"SNR for {detector_label}"
506
+
507
+ # prepare 3 params
508
+ log_overview_params = dict()
509
+ for name in param_names:
510
+ log_overview_params[name] = self.log_params[name]
511
+
512
+ # prepare SNR
513
+ detector_index = self.det_names.index(SNR_name)
514
+ logSNR = np.log10(self.SNR[:, :, detector_index])
515
+ detectable = self.detectable[:, :, detector_index]
516
+ blackdots = np.full_like(logSNR, 1)
517
+ blackdots[~detectable] = 0
518
+ logSNR[~detectable] = np.nan
519
+
520
+ # Prepare axes
521
+ fig, axs = plt.subplots(2, 2, figsize=figsize)
522
+ ax1 = axs[0, 0]
523
+ ax2 = axs[0, 1]
524
+ ax3 = axs[1, 0]
525
+ ax4 = axs[1, 1]
526
+ for ax in (ax1, ax2, ax3, ax4):
527
+ ax.set_xlabel(self.xy_plot_names[0])
528
+ ax.set_ylabel(self.xy_plot_names[1])
529
+ if self.scale == 'linlog':
530
+ ax.set_yscale('log')
531
+ if self.scale == 'loglin':
532
+ ax.set_xscale('log')
533
+ if self.scale == 'loglog':
534
+ ax.set_xscale('log')
535
+ ax.set_yscale('log')
536
+ ax.tick_params(axis='both', which='both', direction="in", width=0.5, zorder=1000)
537
+ ax.xaxis.set_ticks_position('both')
538
+ ax.yaxis.set_ticks_position('both')
539
+ for axis in ["top", 'bottom','left','right']:
540
+ ax.spines[axis].set_linewidth(0.5)
541
+ ax.spines[axis].set_zorder(10000)
542
+
543
+ # Plot 3 params
544
+ GW_axes = (ax1, ax2, ax3)
545
+ for i, ax in enumerate(GW_axes):
546
+ im = GW_axes[i].pcolormesh(
547
+ self.x, self.y, log_overview_params[param_names[i]].T, cmap=TLcmap)
548
+ cbar = fig.colorbar(im, ax=GW_axes[i])
549
+ _style_log_colorbar(cbar)
550
+ GW_axes[i].set_title(param_plot_names[i])
551
+
552
+ # Plot SNR
553
+ ax4.set_title(SNR_plot_name)
554
+ ax4.pcolormesh(self.x, self.y, blackdots.T, cmap="gist_gray")
555
+ im4 = ax4.pcolormesh(self.x, self.y, logSNR.T, cmap=TLcmap)
556
+ cbar4 = fig.colorbar(im4, ax=ax4)
557
+ _style_log_colorbar(cbar4)
558
+ fig.suptitle(self.overview_title)
559
+ plt.tight_layout()
560
+
561
+ # Savefig
562
+ add_TL_logo(loc="upper right", ax=ax4)
563
+ plt.savefig(self.foldername+"overview_plot.pdf")
564
+ plt.close(fig)
565
+
566
+ def plot_log_params(self):
567
+ """Plot the logarithmic version of every derived parameter."""
568
+ # Log plots
569
+ for name, plotname in zip(self.derived_params_names, self.derived_params_plot_names):
570
+ # skip parameters whose name contains "WARNING"
571
+ if "WARNING" in name or "step" in name:
572
+ continue
573
+
574
+ fig, ax = plt.subplots()
575
+ im = ax.pcolormesh(self.x, self.y, self.log_params[name].T, cmap=TLcmap)
576
+ _finish_plot(fig, ax, self.xy_plot_names,
577
+ im, self.scale, R"$\mathrm{log}_{10}$ "+plotname,
578
+ self.foldername+"log_plot_"+name+".pdf")
579
+
580
+ def plot_lin_params(self):
581
+ """Plot the linear-scale version of every derived parameter."""
582
+ for name, plotname in zip(self.derived_params_names, self.derived_params_plot_names):
583
+ fig, ax = plt.subplots()
584
+ im = ax.pcolormesh(self.x, self.y, self.lin_params[name].T, cmap=TLcmap)
585
+ if "WARNING" in name or "step" in name:
586
+ _finish_plot(fig, ax, self.xy_plot_names,
587
+ im, self.scale, plotname,
588
+ self.foldername+name+".pdf")
589
+ else:
590
+ _finish_plot(fig, ax, self.xy_plot_names,
591
+ im, self.scale, plotname,
592
+ self.foldername+"lin_plot_"+name+".pdf")