TransitionListener 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transitionlistener/__init__.py +21 -0
- transitionlistener/bubbledynamics.py +2250 -0
- transitionlistener/bubbledynamics_fixedstep.py +2929 -0
- transitionlistener/check_installation.py +309 -0
- transitionlistener/ckm.py +91 -0
- transitionlistener/colors.py +288 -0
- transitionlistener/config.py +185 -0
- transitionlistener/constants.py +64 -0
- transitionlistener/counterterms/__init__.py +230 -0
- transitionlistener/counterterms/cw.py +298 -0
- transitionlistener/counterterms/tensors.py +343 -0
- transitionlistener/counterterms/twohdm.py +520 -0
- transitionlistener/counterterms/twohdm_solver.py +374 -0
- transitionlistener/errors.py +315 -0
- transitionlistener/examples/bp_et.yaml +64 -0
- transitionlistener/examples/bp_lisa.yaml +70 -0
- transitionlistener/examples/bp_muares.yaml +69 -0
- transitionlistener/examples/bp_pta.yaml +64 -0
- transitionlistener/examples/example_dark_photon.yaml +73 -0
- transitionlistener/examples/example_grid.yaml +55 -0
- transitionlistener/examples/example_line.yaml +51 -0
- transitionlistener/examples/example_point.yaml +73 -0
- transitionlistener/examples/example_random_conformal.yaml +42 -0
- transitionlistener/examples/example_ultranest.yaml +39 -0
- transitionlistener/finiteT.py +468 -0
- transitionlistener/generic_potential.py +1521 -0
- transitionlistener/gridplots.py +592 -0
- transitionlistener/gwfopt.py +604 -0
- transitionlistener/helper_functions.py +843 -0
- transitionlistener/hydrodynamics.py +647 -0
- transitionlistener/interface/__init__.py +95 -0
- transitionlistener/interface/check.py +51 -0
- transitionlistener/interface/cli.py +211 -0
- transitionlistener/interface/config_loader.py +264 -0
- transitionlistener/interface/logging_utils.py +30 -0
- transitionlistener/interface/output_schema.py +296 -0
- transitionlistener/interface/pipeline.py +300 -0
- transitionlistener/interface/samplers.py +565 -0
- transitionlistener/interface/scans.py +468 -0
- transitionlistener/interface/single_point.py +581 -0
- transitionlistener/interface/state.py +61 -0
- transitionlistener/lineplots.py +597 -0
- transitionlistener/logo/TL-logo_large.png +0 -0
- transitionlistener/logo/TL-logo_small.png +0 -0
- transitionlistener/models/TL_2HDM.py +512 -0
- transitionlistener/models/TL_conformal_dark_u1.py +202 -0
- transitionlistener/models/TL_dark_U1.py +198 -0
- transitionlistener/models/TL_dark_U1_g_parameterization.py +198 -0
- transitionlistener/models/TL_dark_flipflop.py +274 -0
- transitionlistener/models/templatePotential.py +260 -0
- transitionlistener/nucleation.py +412 -0
- transitionlistener/observability.py +790 -0
- transitionlistener/particles.py +436 -0
- transitionlistener/pathDeformation.py +1462 -0
- transitionlistener/percolation_adaptive_gridbuilders.py +1385 -0
- transitionlistener/percolation_adaptive_rate.py +1049 -0
- transitionlistener/percolation_adaptivestepsize.py +1632 -0
- transitionlistener/percolation_fixedstepsize.py +618 -0
- transitionlistener/phases.py +1499 -0
- transitionlistener/plot_settings.py +121 -0
- transitionlistener/plots.py +1781 -0
- transitionlistener/runtime_options.py +278 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/bandwidths.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/density.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/freqs.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log.txt +16 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rhogrid.npy +0 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/log10rholabels.txt +30 -0
- transitionlistener/tab_data/30f_fs{hd}_100fDMGP_ceffyl/pulsar_list.txt +1 -0
- transitionlistener/tab_data/HLV-O2-PLI.csv +82 -0
- transitionlistener/tab_data/HLV-O2-noise.csv +83 -0
- transitionlistener/tab_data/HLVK-design-PLI.csv +61 -0
- transitionlistener/tab_data/HLVK-design-noise.csv +83 -0
- transitionlistener/tab_data/NG15_PLI_hc.csv +31 -0
- transitionlistener/tab_data/NG15_noise_hc.csv +56 -0
- transitionlistener/tab_data/PLI-dataset.csv +327 -0
- transitionlistener/tab_data/energydensities.dat +1001 -0
- transitionlistener/tab_data/finiteT_b.dat.txt +1999 -0
- transitionlistener/tab_data/finiteT_f.dat.txt +1000 -0
- transitionlistener/tab_data/geff_SM.dat +10009 -0
- transitionlistener/tab_data/muAres_f_Hz_PLI_h2OmegaGW.csv +131 -0
- transitionlistener/tab_data/muAres_f_Hz_h2Omega_noise.csv +99 -0
- transitionlistener/tab_data/noise-dataset.csv +327 -0
- transitionlistener/tab_data/numberdensities.dat +1001 -0
- transitionlistener/tab_data/pressures.dat +1001 -0
- transitionlistener/thermodynamics.py +686 -0
- transitionlistener/transitionObservables.py +1199 -0
- transitionlistener/transitionObservables_fixedstep.py +976 -0
- transitionlistener/transitions.py +1000 -0
- transitionlistener/tunneling1D.py +910 -0
- transitionlistener-2.0.0.dist-info/METADATA +817 -0
- transitionlistener-2.0.0.dist-info/RECORD +95 -0
- transitionlistener-2.0.0.dist-info/WHEEL +4 -0
- transitionlistener-2.0.0.dist-info/entry_points.txt +2 -0
- transitionlistener-2.0.0.dist-info/licenses/LICENSE.txt +674 -0
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
"""Sensitivity curves and observability metrics for gravitational wave experiments.
|
|
2
|
+
|
|
3
|
+
Part of TransitionListener v2.0
|
|
4
|
+
Documentation: https://tasillo.de/TransitionListener/
|
|
5
|
+
|
|
6
|
+
Authors:
|
|
7
|
+
Jonas Matuszak <jonas.matuszak@kit.edu>
|
|
8
|
+
Carlo Tasillo <carlo.tasillo@ific.uv.es>
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from collections import OrderedDict
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
from scipy import integrate
|
|
16
|
+
try: # Optional PTA dependencies; skipped if environment lacks tempo2/ptarcade stack.
|
|
17
|
+
from ptarcade import signal_builder # type: ignore
|
|
18
|
+
from enterprise.signals.parameter import Uniform
|
|
19
|
+
_PTA_IMPORT_ERROR: Exception | None = None
|
|
20
|
+
except Exception as exc: # pragma: no cover - optional dependency
|
|
21
|
+
signal_builder = None # type: ignore
|
|
22
|
+
Uniform = None # type: ignore
|
|
23
|
+
_PTA_IMPORT_ERROR = exc
|
|
24
|
+
|
|
25
|
+
from . import console, print
|
|
26
|
+
from .colors import *
|
|
27
|
+
from .constants import *
|
|
28
|
+
|
|
29
|
+
import rich
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _require_pta_dependencies() -> None:
|
|
33
|
+
"""Raise a clear error if PTA dependencies are unavailable."""
|
|
34
|
+
if _PTA_IMPORT_ERROR is not None:
|
|
35
|
+
raise RuntimeError(
|
|
36
|
+
"PTA likelihood evaluation requires ptarcade/enterprise/tempo2; "
|
|
37
|
+
"set up tempo2 and ptarcade or disable PTA calculations."
|
|
38
|
+
) from _PTA_IMPORT_ERROR
|
|
39
|
+
|
|
40
|
+
# Mapping of PTA likelihood settings keyed by the label used throughout the codebase.
|
|
41
|
+
PTA_LIKELIHOOD_SETTINGS = OrderedDict(
|
|
42
|
+
# Best-fit likelihoods were obtained from PTArcade ultranest scans of
|
|
43
|
+
# first-order phase-transition GW spectra following arXiv:2403.03723.
|
|
44
|
+
[
|
|
45
|
+
(
|
|
46
|
+
"NG15_14bins",
|
|
47
|
+
{
|
|
48
|
+
"pta_name": "NG15",
|
|
49
|
+
"nbins": 14,
|
|
50
|
+
"threshold": -110.0,
|
|
51
|
+
"bestfit_lnL": -103.12759208991564,
|
|
52
|
+
},
|
|
53
|
+
),
|
|
54
|
+
(
|
|
55
|
+
"NG15_5bins",
|
|
56
|
+
{
|
|
57
|
+
"pta_name": "NG15",
|
|
58
|
+
"nbins": 5,
|
|
59
|
+
"threshold": -35.0,
|
|
60
|
+
"bestfit_lnL": -29.55312949665563,
|
|
61
|
+
},
|
|
62
|
+
),
|
|
63
|
+
(
|
|
64
|
+
"NG12_5bins",
|
|
65
|
+
{
|
|
66
|
+
"pta_name": "NG12",
|
|
67
|
+
"nbins": 5,
|
|
68
|
+
"threshold": -45.0,
|
|
69
|
+
"bestfit_lnL": -36.89179733242301,
|
|
70
|
+
},
|
|
71
|
+
),
|
|
72
|
+
(
|
|
73
|
+
"IPTA2_13bins",
|
|
74
|
+
{
|
|
75
|
+
"pta_name": "IPTA2",
|
|
76
|
+
"nbins": 13,
|
|
77
|
+
"threshold": -115.0,
|
|
78
|
+
"bestfit_lnL": -105.66902507253809,
|
|
79
|
+
},
|
|
80
|
+
),
|
|
81
|
+
]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
PTA_LIKELIHOOD_VARIANTS = ("PTArcade", "mock", "smooth")
|
|
85
|
+
|
|
86
|
+
PTA_DISPLAY_NAMES = {
|
|
87
|
+
"NG15_14bins": "NANOGrav 15yr",
|
|
88
|
+
"NG15_5bins": "NANOGrav 15yr",
|
|
89
|
+
"NG12_5bins": "NANOGrav 12.5yr",
|
|
90
|
+
"IPTA2_13bins": "IPTA DR2",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
MOCK_LNL_REFERENCE = {
|
|
94
|
+
"f_peak_Hz_log10": -7.70241036708,
|
|
95
|
+
"h2OmegaGW_peak_log10": -8.134671889762169,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
EXISTING_OBSERVATORIES = {
|
|
99
|
+
"EPTA_18_yrs",
|
|
100
|
+
"NANOGrav_11_yrs",
|
|
101
|
+
"NANOGrav_15_yrs",
|
|
102
|
+
"HLV_O2"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
DELTA_LNL_WITHIN_THRESHOLD = 4.5
|
|
106
|
+
|
|
107
|
+
class GW_Sensitivity_Data:
|
|
108
|
+
"""
|
|
109
|
+
Class used to import the sensitivity data curves of GW observaories.
|
|
110
|
+
|
|
111
|
+
Attributes
|
|
112
|
+
----------
|
|
113
|
+
self.det_f : float array
|
|
114
|
+
Frequencies from the detector data
|
|
115
|
+
self.det_data : float array
|
|
116
|
+
Detector sensitivity curves
|
|
117
|
+
self.det_names : string tuple
|
|
118
|
+
Deterctor names, ordered as in input file
|
|
119
|
+
self.det_colors : string tuple
|
|
120
|
+
Colors for the detector sensitivities in plots, ordered as in input file
|
|
121
|
+
self.det : dictionary
|
|
122
|
+
Dicitonary for the sensitivitie with the detector name as key
|
|
123
|
+
self.det_thr : float tuple
|
|
124
|
+
Expected detector threshold SNRs
|
|
125
|
+
self.det_tobs : float tuple
|
|
126
|
+
Expected detector observation times
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self):
|
|
130
|
+
"""Load sensitivity and power-law integrated curves for supported detectors."""
|
|
131
|
+
data_path = os.path.join(os.path.dirname(__file__), "tab_data")
|
|
132
|
+
|
|
133
|
+
def _csv_path(filename: str) -> str:
|
|
134
|
+
return os.path.join(data_path, filename)
|
|
135
|
+
|
|
136
|
+
detectors = self._load_ground_space_detectors(_csv_path)
|
|
137
|
+
|
|
138
|
+
det_names = (
|
|
139
|
+
"SKA_5_yrs",
|
|
140
|
+
"SKA_10_yrs",
|
|
141
|
+
"SKA_20_yrs",
|
|
142
|
+
"EPTA_18_yrs",
|
|
143
|
+
"NANOGrav_11_yrs",
|
|
144
|
+
"NANOGrav_15_yrs",
|
|
145
|
+
"LISA",
|
|
146
|
+
"B-DECIGO",
|
|
147
|
+
"DECIGO",
|
|
148
|
+
"BBO",
|
|
149
|
+
"ET",
|
|
150
|
+
"muAres",
|
|
151
|
+
"HLV_O2",
|
|
152
|
+
"HLVK_design",
|
|
153
|
+
)
|
|
154
|
+
mu_ares = self._load_mu_ares(_csv_path)
|
|
155
|
+
ground_based = self._load_ground_based(_csv_path)
|
|
156
|
+
ng15 = self._load_ng15(_csv_path)
|
|
157
|
+
|
|
158
|
+
combined_curves = {}
|
|
159
|
+
for curves in (detectors["curves"], mu_ares["curves"], ground_based["curves"], ng15["curves"]):
|
|
160
|
+
combined_curves.update(curves)
|
|
161
|
+
det_files = {name: combined_curves[name] for name in det_names}
|
|
162
|
+
|
|
163
|
+
det_plot_names = (
|
|
164
|
+
r"SKA $5 \, \mathrm{yr}$",
|
|
165
|
+
r"SKA $10 \, \mathrm{yr}$",
|
|
166
|
+
r"SKA $20 \, \mathrm{yr}$",
|
|
167
|
+
r"EPTA $18 \, \mathrm{yr}$",
|
|
168
|
+
r"NANOGrav $11 \, \mathrm{yr}$",
|
|
169
|
+
r"NANOGrav $15 \, \mathrm{yr}$",
|
|
170
|
+
"LISA",
|
|
171
|
+
"B-DECIGO",
|
|
172
|
+
"DECIGO",
|
|
173
|
+
"BBO",
|
|
174
|
+
"ET",
|
|
175
|
+
r"$\mu$Ares",
|
|
176
|
+
"LIGO-VIRGO O2",
|
|
177
|
+
"HLVK design",
|
|
178
|
+
)
|
|
179
|
+
det_single_obs_names = {"LISA", "B-DECIGO", "ET", "muAres"}
|
|
180
|
+
det_no_plot = {"EPTA_18_yrs", "NANOGrav_11_yrs", "B-DECIGO", "DECIGO", "HLVK_design"}
|
|
181
|
+
det_colors = (
|
|
182
|
+
"#FFD700", # SKA_5_yrs – bright gold
|
|
183
|
+
"#FF7F00", # SKA_10_yrs – vivid orange
|
|
184
|
+
"#E8000B", # SKA_20_yrs – pure red
|
|
185
|
+
"white", # EPTA_18_yrs (hidden)
|
|
186
|
+
"white", # NANOGrav_11_yrs (hidden)
|
|
187
|
+
"#C0105A", # NANOGrav_15_yrs – crimson-magenta
|
|
188
|
+
"#4477AA", # LISA – blue
|
|
189
|
+
"white", # B-DECIGO (hidden)
|
|
190
|
+
"white", # DECIGO (hidden)
|
|
191
|
+
"#66CCEE", # BBO – cyan
|
|
192
|
+
"#AA4499", # ET – purple
|
|
193
|
+
"#00C2AB", # muAres – teal
|
|
194
|
+
"#002147", # HLV_O2 – very dark navy blue
|
|
195
|
+
"white", # HLVK_design (hidden)
|
|
196
|
+
)
|
|
197
|
+
det_zorder = (3, 2, 1, 5, 6, 4, 5, 3, 1, 2, 3, 1, 7, 8)
|
|
198
|
+
det_thr = (4, 4, 4, 1.19, 0.697, 5, 10, 8, 10, 10, 5, 10, 1, 1)
|
|
199
|
+
det_tobs = (5*yr, 10*yr, 20*yr, 18*yr, 11*yr, 15*yr, 4*yr, 4*yr, 4*yr, 4*yr, 5*yr, 7*yr, 1*yr, 1*yr)
|
|
200
|
+
|
|
201
|
+
self.det = {}
|
|
202
|
+
for i, name in enumerate(det_names):
|
|
203
|
+
f_noise, noise, f_PLI, PLI = det_files[name]
|
|
204
|
+
self.det[name] = {
|
|
205
|
+
"plot_name": det_plot_names[i],
|
|
206
|
+
"f_noise": f_noise,
|
|
207
|
+
"noise": noise,
|
|
208
|
+
"f_PLI": f_PLI,
|
|
209
|
+
"PLI": PLI,
|
|
210
|
+
"thr": det_thr[i],
|
|
211
|
+
"tobs": det_tobs[i],
|
|
212
|
+
"color": det_colors[i],
|
|
213
|
+
"zorder": det_zorder[i],
|
|
214
|
+
"no_plot": name in det_no_plot,
|
|
215
|
+
"single_obs": name in det_single_obs_names,
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
self.det_names = det_names
|
|
219
|
+
self.PTA_labels = tuple(PTA_LIKELIHOOD_SETTINGS.keys())
|
|
220
|
+
self.PTA_names = tuple(
|
|
221
|
+
str(cfg["pta_name"]) for cfg in PTA_LIKELIHOOD_SETTINGS.values()
|
|
222
|
+
)
|
|
223
|
+
self.PTA_bins = tuple(int(cfg["nbins"]) for cfg in PTA_LIKELIHOOD_SETTINGS.values())
|
|
224
|
+
self.foldername = ""
|
|
225
|
+
|
|
226
|
+
def _load_ground_space_detectors(self, path_func):
|
|
227
|
+
"""Load SKA/LISA-style detector curves (Breitbach & Schwaller et al., 1811.11175)."""
|
|
228
|
+
det_PLI_f, *det_PLI_data = np.genfromtxt(path_func("PLI-dataset.csv"), delimiter=",", unpack=True, skip_header=1)
|
|
229
|
+
det_noise_f, *det_noise_data = np.genfromtxt(path_func("noise-dataset.csv"), delimiter=",", unpack=True, skip_header=1)
|
|
230
|
+
names = (
|
|
231
|
+
"SKA_5_yrs",
|
|
232
|
+
"SKA_10_yrs",
|
|
233
|
+
"SKA_20_yrs",
|
|
234
|
+
"EPTA_18_yrs",
|
|
235
|
+
"NANOGrav_11_yrs",
|
|
236
|
+
"NANOGrav_15_yrs",
|
|
237
|
+
"LISA",
|
|
238
|
+
"B-DECIGO",
|
|
239
|
+
"DECIGO",
|
|
240
|
+
"BBO",
|
|
241
|
+
"ET",
|
|
242
|
+
)
|
|
243
|
+
curves = {
|
|
244
|
+
"SKA_5_yrs": (det_noise_f, det_noise_data[0], det_PLI_f, det_PLI_data[0]),
|
|
245
|
+
"SKA_10_yrs": (det_noise_f, det_noise_data[1], det_PLI_f, det_PLI_data[1]),
|
|
246
|
+
"SKA_20_yrs": (det_noise_f, det_noise_data[2], det_PLI_f, det_PLI_data[2]),
|
|
247
|
+
"EPTA_18_yrs": (det_noise_f, det_noise_data[3], det_PLI_f, det_PLI_data[3]),
|
|
248
|
+
"NANOGrav_11_yrs": (det_noise_f, det_noise_data[4], det_PLI_f, det_PLI_data[4]),
|
|
249
|
+
"LISA": (det_noise_f, det_noise_data[5], det_PLI_f, det_PLI_data[5]),
|
|
250
|
+
"B-DECIGO": (det_noise_f, det_noise_data[6], det_PLI_f, det_PLI_data[6]),
|
|
251
|
+
"DECIGO": (det_noise_f, det_noise_data[7], det_PLI_f, det_PLI_data[7]),
|
|
252
|
+
"BBO": (det_noise_f, det_noise_data[8], det_PLI_f, det_PLI_data[8]),
|
|
253
|
+
"ET": (det_noise_f, det_noise_data[9], det_PLI_f, det_PLI_data[9]),
|
|
254
|
+
}
|
|
255
|
+
return {"names": names, "curves": curves}
|
|
256
|
+
|
|
257
|
+
def _load_mu_ares(self, path_func):
|
|
258
|
+
"""Load μAres sensitivity and PLI curves (Sesana et al., 1908.11391)."""
|
|
259
|
+
det_muAres_PLI_f, det_muAres_PLI_data = np.genfromtxt(path_func("muAres_f_Hz_PLI_h2OmegaGW.csv"), delimiter=";", unpack=True)
|
|
260
|
+
det_muAres_noise_f, det_muAres_noise_data = np.genfromtxt(path_func("muAres_f_Hz_h2Omega_noise.csv"), delimiter=";", unpack=True)
|
|
261
|
+
curves = {
|
|
262
|
+
"muAres": (det_muAres_noise_f, det_muAres_noise_data, det_muAres_PLI_f, det_muAres_PLI_data)
|
|
263
|
+
}
|
|
264
|
+
return {"names": ("muAres",), "curves": curves}
|
|
265
|
+
|
|
266
|
+
def _load_ground_based(self, path_func):
|
|
267
|
+
"""Load the LIGO/Virgo design and O(2) curves (Schmitz et al., 2002.04615)."""
|
|
268
|
+
det_HLV_O2_PLI_f, det_HLV_O2_PLI_data = np.genfromtxt(path_func("HLV-O2-PLI.csv"), delimiter=";", unpack=True)
|
|
269
|
+
det_HLV_O2_noise_f, det_HLV_O2_noise_data = np.genfromtxt(path_func("HLV-O2-noise.csv"), delimiter=";", unpack=True)
|
|
270
|
+
det_HLVK_design_PLI_f, det_HLVK_design_PLI_data = np.genfromtxt(path_func("HLVK-design-PLI.csv"), delimiter=";", unpack=True)
|
|
271
|
+
det_HLVK_design_noise_f, det_HLVK_design_noise_data = np.genfromtxt(path_func("HLVK-design-noise.csv"), delimiter=";", unpack=True)
|
|
272
|
+
curves = {
|
|
273
|
+
"HLV_O2": (det_HLV_O2_noise_f, det_HLV_O2_noise_data, det_HLV_O2_PLI_f, det_HLV_O2_PLI_data),
|
|
274
|
+
"HLVK_design": (
|
|
275
|
+
det_HLVK_design_noise_f,
|
|
276
|
+
det_HLVK_design_noise_data,
|
|
277
|
+
det_HLVK_design_PLI_f,
|
|
278
|
+
det_HLVK_design_PLI_data,
|
|
279
|
+
),
|
|
280
|
+
}
|
|
281
|
+
return {"names": ("HLV_O2", "HLVK_design"), "curves": curves}
|
|
282
|
+
|
|
283
|
+
def _load_ng15(self, path_func):
|
|
284
|
+
"""Load the NANOGrav 15yr data and convert it to h^2Ω_GW (NANOGrav Collab., 2306.16218)."""
|
|
285
|
+
det_NG15_PLI_f, det_NG15_PLI_data_hc = np.genfromtxt(path_func("NG15_PLI_hc.csv"), delimiter=",", unpack=True, skip_header=1)
|
|
286
|
+
det_NG15_noise_f, det_NG15_noise_data_hc = np.genfromtxt(path_func("NG15_noise_hc.csv"), delimiter=",", unpack=True, skip_header=1)
|
|
287
|
+
|
|
288
|
+
def _hc_to_h2OmegaGW(f_Hz, h_c):
|
|
289
|
+
return 2 * np.pi**2 / 3.0 * (f_Hz**2) * (h_c**2) / H100_Hz**2
|
|
290
|
+
|
|
291
|
+
det_NG15_PLI_f = det_NG15_PLI_f[::-1]
|
|
292
|
+
det_NG15_noise_f = det_NG15_noise_f[::-1]
|
|
293
|
+
det_NG15_PLI_data_hc = det_NG15_PLI_data_hc[::-1]
|
|
294
|
+
det_NG15_noise_data_hc = det_NG15_noise_data_hc[::-1]
|
|
295
|
+
det_NG15_PLI_data = _hc_to_h2OmegaGW(det_NG15_PLI_f, det_NG15_PLI_data_hc)
|
|
296
|
+
det_NG15_noise_data = _hc_to_h2OmegaGW(det_NG15_noise_f, det_NG15_noise_data_hc)
|
|
297
|
+
|
|
298
|
+
curves = {
|
|
299
|
+
"NANOGrav_15_yrs": (det_NG15_noise_f, det_NG15_noise_data, det_NG15_PLI_f, det_NG15_PLI_data)
|
|
300
|
+
}
|
|
301
|
+
return {"names": ("NANOGrav_15_yrs",), "curves": curves}
|
|
302
|
+
|
|
303
|
+
# ==================================================
|
|
304
|
+
# PTArcade compatible config and signal models
|
|
305
|
+
# ==================================================
|
|
306
|
+
|
|
307
|
+
class PTAConfig:
|
|
308
|
+
"""Minimal PTArcade-style configuration container used by the PTA likelihood wrappers."""
|
|
309
|
+
|
|
310
|
+
def __init__(self, nfreqBins=14, PTA="NG15"):
|
|
311
|
+
"""Store the PTA dataset label and the number of GW frequency bins."""
|
|
312
|
+
self.pta_data = PTA # "NG15" or "NG12" or "IPTA2"
|
|
313
|
+
self.corr = False
|
|
314
|
+
self.red_components = 30
|
|
315
|
+
self.gwb_components = nfreqBins
|
|
316
|
+
self.bhb_th_prior = True
|
|
317
|
+
self.mod_sel = False
|
|
318
|
+
self.A_bhb_logmin = False
|
|
319
|
+
self.A_bhb_logmax = False
|
|
320
|
+
|
|
321
|
+
class PTAModel:
|
|
322
|
+
"""Adapter exposing a TransitionListener spectrum in the format expected by PTArcade."""
|
|
323
|
+
|
|
324
|
+
def __init__(self, spectrum):
|
|
325
|
+
"""Store the stochastic background spectrum callable and metadata."""
|
|
326
|
+
self.spectrum = spectrum
|
|
327
|
+
self.smbhb = False
|
|
328
|
+
self.parameters = {}
|
|
329
|
+
|
|
330
|
+
class PTAModel_SMBHB:
|
|
331
|
+
"""PTArcade adapter including the nuisance parameters for an SMBHB foreground."""
|
|
332
|
+
|
|
333
|
+
def __init__(self, spectrum):
|
|
334
|
+
"""Store the spectrum and freeze the astrophysical nuisance priors."""
|
|
335
|
+
self.spectrum = spectrum
|
|
336
|
+
self.smbhb = False
|
|
337
|
+
self.parameters = {
|
|
338
|
+
# The ranges are irrelevant for the likelihood calculation
|
|
339
|
+
'A': Uniform(0, 0)('A'),
|
|
340
|
+
'gamma': Uniform(0, 0)('gamma')
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class Observability(GW_Sensitivity_Data):
|
|
346
|
+
"""Compute SNRs, PTA likelihoods and derived quantities for a GW spectrum."""
|
|
347
|
+
|
|
348
|
+
def __init__(self, gwspec_dict: dict, verbose: bool=False,
|
|
349
|
+
include_smbhb: bool=False):
|
|
350
|
+
"""Store the supplied spectrum and immediately evaluate all observables."""
|
|
351
|
+
super(Observability, self).__init__()
|
|
352
|
+
self.gwspec_dict = gwspec_dict
|
|
353
|
+
self.verbose = verbose
|
|
354
|
+
|
|
355
|
+
if include_smbhb:
|
|
356
|
+
self.my_spec = gwspec_dict["h2OmegaGW_with_astrobg_fun"]
|
|
357
|
+
else:
|
|
358
|
+
self.my_spec = gwspec_dict["h2OmegaGW_fun"]
|
|
359
|
+
self.my_spec_with_astrobg = gwspec_dict["h2OmegaGW_with_astrobg_fun"]
|
|
360
|
+
self.add_spec_info = gwspec_dict["additional_spec_info"]
|
|
361
|
+
|
|
362
|
+
self.h2OmegaGW = gwspec_dict["h2OmegaGW"]
|
|
363
|
+
self.h2OmegaGW_with_astrobg = gwspec_dict["h2OmegaGW_with_astrobg"]
|
|
364
|
+
|
|
365
|
+
self.calc_SNR()
|
|
366
|
+
self.pta_likelihoods = {label: {} for label in self.PTA_labels}
|
|
367
|
+
self.ptas = {}
|
|
368
|
+
if signal_builder is not None and _PTA_IMPORT_ERROR is None:
|
|
369
|
+
for label, settings in PTA_LIKELIHOOD_SETTINGS.items():
|
|
370
|
+
if include_smbhb:
|
|
371
|
+
input_payload = {
|
|
372
|
+
"config": PTAConfig(
|
|
373
|
+
int(settings["nbins"]),
|
|
374
|
+
str(settings["pta_name"]),
|
|
375
|
+
),
|
|
376
|
+
"model": PTAModel_SMBHB(self.my_spec_with_astrobg),
|
|
377
|
+
}
|
|
378
|
+
else:
|
|
379
|
+
input_payload = {
|
|
380
|
+
"config": PTAConfig(
|
|
381
|
+
nfreqBins=int(settings["nbins"]),
|
|
382
|
+
PTA=str(settings["pta_name"]),
|
|
383
|
+
),
|
|
384
|
+
"model": PTAModel(self.my_spec),
|
|
385
|
+
}
|
|
386
|
+
self.ptas[label] = signal_builder.ceffyl_builder(input_payload)
|
|
387
|
+
if include_smbhb:
|
|
388
|
+
self.calc_PTA_logL_with_astronoise(
|
|
389
|
+
gwspec_dict.astroparams_dict["A"],
|
|
390
|
+
gwspec_dict.astroparams_dict["gamma"],
|
|
391
|
+
)
|
|
392
|
+
else:
|
|
393
|
+
self.calc_PTA_logL()
|
|
394
|
+
elif self.verbose:
|
|
395
|
+
msg = "[yellow]PTA likelihood dependencies unavailable; continuing with SNR-only observability outputs.[/yellow]"
|
|
396
|
+
if _PTA_IMPORT_ERROR is not None:
|
|
397
|
+
msg += f"\n [dim]Import error: {_PTA_IMPORT_ERROR}[/dim]"
|
|
398
|
+
console.print(msg)
|
|
399
|
+
self.calc_DNeff_GW()
|
|
400
|
+
self.observability_dict = self.return_gw_info_dict()
|
|
401
|
+
|
|
402
|
+
if self.verbose:
|
|
403
|
+
self.print_additional_spec_info()
|
|
404
|
+
|
|
405
|
+
def __iter__(self):
|
|
406
|
+
"""Enable iteration over the assembled observability dictionary."""
|
|
407
|
+
return iter(self.observability_dict)
|
|
408
|
+
|
|
409
|
+
def __getitem__(self, key: str):
|
|
410
|
+
"""Access individual observables by key."""
|
|
411
|
+
return self.observability_dict[key]
|
|
412
|
+
|
|
413
|
+
def __len__(self):
|
|
414
|
+
"""Return the number of entries in the observability dictionary."""
|
|
415
|
+
return len(self.observability_dict)
|
|
416
|
+
|
|
417
|
+
def keys(self):
|
|
418
|
+
"""Expose the keys present in the observability dictionary."""
|
|
419
|
+
return self.observability_dict.keys()
|
|
420
|
+
|
|
421
|
+
def _get_additional_value(self, key: str, variant: str) -> float:
|
|
422
|
+
"""Helper to access derived additional-spectrum values safely."""
|
|
423
|
+
entry = self.add_spec_info.get(key, {})
|
|
424
|
+
if isinstance(entry, dict):
|
|
425
|
+
value = entry.get(variant, np.nan)
|
|
426
|
+
if isinstance(value, (float, np.floating, int, np.integer)):
|
|
427
|
+
return float(value)
|
|
428
|
+
return float("nan")
|
|
429
|
+
|
|
430
|
+
def _flatten_additional_spec_info(self) -> OrderedDict[str, float | str]:
|
|
431
|
+
"""Return an ordered mapping with lin/log/latex variants for output."""
|
|
432
|
+
flattened: OrderedDict[str, float | str] = OrderedDict()
|
|
433
|
+
for key, entry in self.add_spec_info.items():
|
|
434
|
+
if not isinstance(entry, dict):
|
|
435
|
+
continue
|
|
436
|
+
lin = entry.get("lin", np.nan)
|
|
437
|
+
log10 = entry.get("log10", np.nan)
|
|
438
|
+
latex = entry.get("latex")
|
|
439
|
+
|
|
440
|
+
if np.isscalar(lin):
|
|
441
|
+
flattened[f"{key}_lin"] = float(lin)
|
|
442
|
+
else:
|
|
443
|
+
flattened[f"{key}_lin"] = lin
|
|
444
|
+
|
|
445
|
+
if np.isscalar(log10):
|
|
446
|
+
flattened[f"{key}_log10"] = float(log10)
|
|
447
|
+
else:
|
|
448
|
+
flattened[f"{key}_log10"] = log10
|
|
449
|
+
|
|
450
|
+
if latex is not None:
|
|
451
|
+
flattened[f"{key}_latex"] = str(latex)
|
|
452
|
+
|
|
453
|
+
return flattened
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def calc_SNR(self):
|
|
457
|
+
"""Compute the signal-to-noise ratio for each detector sensitivity curve."""
|
|
458
|
+
def calc_SNR_for_det(detector_name):
|
|
459
|
+
sensitivity = self.det[detector_name]["noise"]
|
|
460
|
+
freq = self.det[detector_name]["f_noise"]
|
|
461
|
+
frac = np.array(
|
|
462
|
+
(self.my_spec(freq)/sensitivity)**2)
|
|
463
|
+
integral = integrate.simpson(frac, freq)
|
|
464
|
+
tobs = self.det[detector_name]["tobs"]
|
|
465
|
+
snr2 = 2. * tobs * integral
|
|
466
|
+
if self.det[detector_name]["single_obs"]:
|
|
467
|
+
snr2 /= 2.
|
|
468
|
+
return np.sqrt(snr2)
|
|
469
|
+
|
|
470
|
+
SNR = np.zeros(len(self.det_names))
|
|
471
|
+
detectable = np.zeros(len(self.det_names), dtype=bool)
|
|
472
|
+
for i, name in enumerate(self.det_names):
|
|
473
|
+
SNR[i] = calc_SNR_for_det(name)
|
|
474
|
+
log_thr = np.log10(self.det[name]["thr"])
|
|
475
|
+
detectable[i] = bool(np.log10(SNR[i]) > log_thr)
|
|
476
|
+
|
|
477
|
+
if self.verbose:
|
|
478
|
+
existing_detectors = {
|
|
479
|
+
"EPTA_18_yrs",
|
|
480
|
+
"NANOGrav_11_yrs",
|
|
481
|
+
"NANOGrav_15_yrs",
|
|
482
|
+
"HLV_O2",
|
|
483
|
+
}
|
|
484
|
+
table = rich.table.Table(
|
|
485
|
+
title="SNR VALUES AND OBSERVABILITY",
|
|
486
|
+
title_justify="left",
|
|
487
|
+
box=rich.box.ROUNDED,
|
|
488
|
+
)
|
|
489
|
+
table.add_column("Detector", style="cyan", no_wrap=True)
|
|
490
|
+
table.add_column("SNR", style="orange1", justify="right")
|
|
491
|
+
table.add_column(
|
|
492
|
+
"Should have been observed already?",
|
|
493
|
+
style="green",
|
|
494
|
+
justify="center",
|
|
495
|
+
)
|
|
496
|
+
table.add_column("Observable?", style="green", justify="center")
|
|
497
|
+
|
|
498
|
+
for n, v, d in zip(self.det_names, SNR, detectable):
|
|
499
|
+
existing = n in EXISTING_OBSERVATORIES
|
|
500
|
+
mark = "[green]\u2714[/green]" if d else "[red]\u274C[/red]"
|
|
501
|
+
observed_col = mark if existing else ""
|
|
502
|
+
future_col = mark if not existing else ""
|
|
503
|
+
table.add_row(n, f"{v:.4e}", observed_col, future_col)
|
|
504
|
+
console.print("\n", table)
|
|
505
|
+
|
|
506
|
+
self.SNR = SNR
|
|
507
|
+
self.detectable = detectable
|
|
508
|
+
return SNR
|
|
509
|
+
|
|
510
|
+
def calc_DNeff_GW(self):
|
|
511
|
+
"""Compute the contribution to the effective number of relativistic species from GWs."""
|
|
512
|
+
def integrand(x):
|
|
513
|
+
return self.my_spec(x)/x # h2OmegaGW(f) / f as a function of f_Hz
|
|
514
|
+
|
|
515
|
+
f_min = 1.5e-11 # Hz
|
|
516
|
+
f_max = 1e10 # Hz
|
|
517
|
+
freqs = np.logspace(np.log(f_min), np.log(f_max), 100000)
|
|
518
|
+
integrand_values = integrand(freqs)
|
|
519
|
+
h2OmegaGW = integrate.simpson(integrand_values, freqs)
|
|
520
|
+
DNeff_GW = (8/7) * (11/4)**(4/3) * h2OmegaGW / Omega_gamma_h2
|
|
521
|
+
self.DNeff_GW = DNeff_GW
|
|
522
|
+
self.add_spec_info["DNeff_GW"] = {
|
|
523
|
+
"lin": float(DNeff_GW),
|
|
524
|
+
"log10": self._safe_log10(DNeff_GW),
|
|
525
|
+
"latex": r"$\Delta N_\mathrm{eff}^{\mathrm{GW}}$",
|
|
526
|
+
"label": "DNeff_GW",
|
|
527
|
+
}
|
|
528
|
+
return DNeff_GW
|
|
529
|
+
|
|
530
|
+
def calc_PTA_logL(self):
|
|
531
|
+
"""Evaluate PTA log-likelihoods using PTArcade for the stored spectrum."""
|
|
532
|
+
_require_pta_dependencies()
|
|
533
|
+
|
|
534
|
+
logL_values: dict[str, float] = {}
|
|
535
|
+
for label in PTA_LIKELIHOOD_SETTINGS:
|
|
536
|
+
pta = self.ptas[label]
|
|
537
|
+
logL_values[label] = float(pta.ln_likelihood(np.array([])))
|
|
538
|
+
|
|
539
|
+
self._populate_pta_likelihoods(logL_values)
|
|
540
|
+
|
|
541
|
+
if self.verbose:
|
|
542
|
+
self._print_pta_likelihood_table()
|
|
543
|
+
|
|
544
|
+
return self.logL_PTAs
|
|
545
|
+
|
|
546
|
+
def _populate_pta_likelihoods(self, logL_values: dict[str, float]):
|
|
547
|
+
"""Store PTA likelihood variants, mock approximations, and derived metrics."""
|
|
548
|
+
self.logL_PTAs = np.array(
|
|
549
|
+
[logL_values.get(label, np.nan) for label in PTA_LIKELIHOOD_SETTINGS]
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
pta_likelihoods = {}
|
|
553
|
+
for label, settings in PTA_LIKELIHOOD_SETTINGS.items():
|
|
554
|
+
actual = logL_values.get(label, np.nan)
|
|
555
|
+
threshold = float(settings["threshold"])
|
|
556
|
+
bestfit = float(settings["bestfit_lnL"])
|
|
557
|
+
mock = self._compute_mock_likelihood(threshold)
|
|
558
|
+
smooth = self._compute_smoothened_likelihood(actual, mock, threshold)
|
|
559
|
+
|
|
560
|
+
variant_data = {}
|
|
561
|
+
for variant, value in (
|
|
562
|
+
("PTArcade", actual),
|
|
563
|
+
("mock", mock),
|
|
564
|
+
("smooth", smooth),
|
|
565
|
+
):
|
|
566
|
+
delta = self._compute_delta_lnL(value, bestfit)
|
|
567
|
+
sigma = self._delta_to_sigma(delta)
|
|
568
|
+
within = self._within_three_sigma(delta) if variant == "PTArcade" else None
|
|
569
|
+
variant_data[variant] = {
|
|
570
|
+
"lnL": value,
|
|
571
|
+
"delta_lnL": delta,
|
|
572
|
+
"sigma": sigma,
|
|
573
|
+
"within_3sigma": within,
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
pta_likelihoods[label] = variant_data
|
|
577
|
+
|
|
578
|
+
self.pta_likelihoods = pta_likelihoods
|
|
579
|
+
|
|
580
|
+
def _compute_mock_likelihood(self, threshold: float) -> float:
|
|
581
|
+
"""Compute the mock likelihood using peak frequency and amplitude."""
|
|
582
|
+
log10_h2 = self._get_additional_value("h2OmegaGW_peak", "log10")
|
|
583
|
+
log10_f = self._get_additional_value("f_peak_Hz", "log10")
|
|
584
|
+
|
|
585
|
+
if not (np.isfinite(log10_h2) and np.isfinite(log10_f)):
|
|
586
|
+
return np.nan
|
|
587
|
+
|
|
588
|
+
diff_h2 = log10_h2 - MOCK_LNL_REFERENCE["h2OmegaGW_peak_log10"]
|
|
589
|
+
diff_f = log10_f - MOCK_LNL_REFERENCE["f_peak_Hz_log10"]
|
|
590
|
+
return threshold - diff_h2 ** 2 - diff_f ** 2
|
|
591
|
+
|
|
592
|
+
@staticmethod
|
|
593
|
+
def _compute_smoothened_likelihood(actual: float, mock: float, threshold: float) -> float:
|
|
594
|
+
"""Return PTArcade likelihood when above threshold, else the mock approximation."""
|
|
595
|
+
if np.isfinite(actual) and actual >= threshold:
|
|
596
|
+
return actual
|
|
597
|
+
return mock if np.isfinite(mock) else actual
|
|
598
|
+
|
|
599
|
+
@staticmethod
|
|
600
|
+
def _compute_delta_lnL(value: float, bestfit: float) -> float:
|
|
601
|
+
"""Return the log-likelihood distance to the best-fit value."""
|
|
602
|
+
if not np.isfinite(value):
|
|
603
|
+
return np.inf
|
|
604
|
+
return bestfit - value
|
|
605
|
+
|
|
606
|
+
@staticmethod
|
|
607
|
+
def _delta_to_sigma(delta_lnL: float) -> float:
|
|
608
|
+
"""Translate a log-likelihood difference into an approximate sigma distance."""
|
|
609
|
+
if not np.isfinite(delta_lnL):
|
|
610
|
+
return np.inf
|
|
611
|
+
return np.sqrt(2.0 * max(delta_lnL, 0.0))
|
|
612
|
+
|
|
613
|
+
@staticmethod
|
|
614
|
+
def _within_three_sigma(delta_lnL: float) -> bool:
|
|
615
|
+
"""Return True if the delta log-likelihood is within the 3-sigma criterion."""
|
|
616
|
+
return bool(np.isfinite(delta_lnL) and delta_lnL < DELTA_LNL_WITHIN_THRESHOLD)
|
|
617
|
+
|
|
618
|
+
def _print_pta_likelihood_table(self):
|
|
619
|
+
"""Pretty-print PTA likelihood variants and their derived metrics."""
|
|
620
|
+
table = rich.table.Table(
|
|
621
|
+
title="LOG LIKELIHOODS FOR PULSAR TIMING ARRAYS",
|
|
622
|
+
title_justify="left",
|
|
623
|
+
box=rich.box.ROUNDED,
|
|
624
|
+
)
|
|
625
|
+
table.add_column("PTA", style="cyan", no_wrap=True)
|
|
626
|
+
table.add_column("Bins", style="cyan", justify="center")
|
|
627
|
+
table.add_column("Variant", style="magenta", no_wrap=True)
|
|
628
|
+
table.add_column("lnL", style="orange1", justify="right")
|
|
629
|
+
table.add_column("Δ lnL", style="orange1", justify="right")
|
|
630
|
+
table.add_column("σ", style="orange1", justify="right")
|
|
631
|
+
table.add_column("Within 3σ?", style="green", justify="center")
|
|
632
|
+
|
|
633
|
+
for label, settings in PTA_LIKELIHOOD_SETTINGS.items():
|
|
634
|
+
variant_data = self.pta_likelihoods.get(label, {})
|
|
635
|
+
bins = int(settings["nbins"])
|
|
636
|
+
display_name = PTA_DISPLAY_NAMES.get(label, label)
|
|
637
|
+
for idx, variant in enumerate(PTA_LIKELIHOOD_VARIANTS):
|
|
638
|
+
metrics = variant_data.get(variant, {})
|
|
639
|
+
lnL_val = metrics.get("lnL", np.nan)
|
|
640
|
+
delta_val = metrics.get("delta_lnL", np.nan)
|
|
641
|
+
sigma_val = metrics.get("sigma", np.nan)
|
|
642
|
+
within = metrics.get("within_3sigma") if variant == "PTArcade" else None
|
|
643
|
+
|
|
644
|
+
lnL_str = self._format_float(lnL_val, decimals=2)
|
|
645
|
+
delta_str = self._format_float(delta_val, decimals=2)
|
|
646
|
+
sigma_str = self._format_float(sigma_val, decimals=2)
|
|
647
|
+
mark = ""
|
|
648
|
+
if variant == "PTArcade" and isinstance(within, bool):
|
|
649
|
+
mark = "[green]\u2714[/green]" if within else "[red]\u274C[/red]"
|
|
650
|
+
|
|
651
|
+
end_section = variant == PTA_LIKELIHOOD_VARIANTS[-1]
|
|
652
|
+
table.add_row(
|
|
653
|
+
display_name if idx == 0 else "",
|
|
654
|
+
str(bins),
|
|
655
|
+
variant,
|
|
656
|
+
lnL_str,
|
|
657
|
+
delta_str,
|
|
658
|
+
sigma_str,
|
|
659
|
+
f"{mark}",
|
|
660
|
+
end_section=end_section,
|
|
661
|
+
)
|
|
662
|
+
console.print("\n", table)
|
|
663
|
+
|
|
664
|
+
@staticmethod
|
|
665
|
+
def _format_float(value: float, decimals: int = 10) -> str:
|
|
666
|
+
"""Format floating-point values for readable table output."""
|
|
667
|
+
if not np.isfinite(value):
|
|
668
|
+
return "nan"
|
|
669
|
+
return f"{value:.{decimals}f}"
|
|
670
|
+
|
|
671
|
+
@staticmethod
|
|
672
|
+
def _format_scientific(value: float) -> str:
|
|
673
|
+
"""Format a floating-point value using scientific notation."""
|
|
674
|
+
if not np.isfinite(value):
|
|
675
|
+
return "nan"
|
|
676
|
+
return f"{value:.4e}"
|
|
677
|
+
|
|
678
|
+
@staticmethod
|
|
679
|
+
def _safe_log10(value: float) -> float:
|
|
680
|
+
"""Safely compute the base-10 logarithm of a positive value."""
|
|
681
|
+
try:
|
|
682
|
+
numeric = float(value)
|
|
683
|
+
except (TypeError, ValueError):
|
|
684
|
+
return float("nan")
|
|
685
|
+
if numeric > 0:
|
|
686
|
+
return float(np.log10(numeric))
|
|
687
|
+
return float("nan")
|
|
688
|
+
|
|
689
|
+
def calc_PTA_logL_with_astronoise(self, A: float, gamma: float):
|
|
690
|
+
"""Evaluate PTA log-likelihoods including an astrophysical background."""
|
|
691
|
+
_require_pta_dependencies()
|
|
692
|
+
|
|
693
|
+
logL_values: dict[str, float] = {}
|
|
694
|
+
for label in PTA_LIKELIHOOD_SETTINGS:
|
|
695
|
+
pta = self.ptas[label]
|
|
696
|
+
logL_values[label] = float(pta.ln_likelihood(np.array([A, gamma])))
|
|
697
|
+
|
|
698
|
+
self._populate_pta_likelihoods(logL_values)
|
|
699
|
+
|
|
700
|
+
if self.verbose:
|
|
701
|
+
self._print_pta_likelihood_table()
|
|
702
|
+
|
|
703
|
+
return self.logL_PTAs
|
|
704
|
+
|
|
705
|
+
def print_additional_spec_info(self):
|
|
706
|
+
"""Render a table summarising derived GW spectrum properties."""
|
|
707
|
+
table = rich.table.Table(
|
|
708
|
+
title="Additional information on GW spectrum",
|
|
709
|
+
title_justify="left",
|
|
710
|
+
show_header=True,
|
|
711
|
+
header_style="bold",
|
|
712
|
+
box=rich.box.ROUNDED,
|
|
713
|
+
)
|
|
714
|
+
table.add_column("Quantity", style="cyan", no_wrap=True)
|
|
715
|
+
table.add_column("Value", style="orange1", justify="right")
|
|
716
|
+
|
|
717
|
+
for key, entry in self.add_spec_info.items():
|
|
718
|
+
if not isinstance(entry, dict):
|
|
719
|
+
continue
|
|
720
|
+
label = entry.get("label", key)
|
|
721
|
+
lin = entry.get("lin", np.nan)
|
|
722
|
+
table.add_row(
|
|
723
|
+
str(label),
|
|
724
|
+
self._format_scientific(lin),
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
console.print("\n", table)
|
|
728
|
+
|
|
729
|
+
def initialise_gw_info_dict(self):
|
|
730
|
+
"""Initialise an empty return object.
|
|
731
|
+
|
|
732
|
+
Parameters
|
|
733
|
+
----------
|
|
734
|
+
|
|
735
|
+
Returns
|
|
736
|
+
-------
|
|
737
|
+
|
|
738
|
+
"""
|
|
739
|
+
gw_info_dict = {}
|
|
740
|
+
# Add SNR and detectable for each detector
|
|
741
|
+
for name in self.det_names:
|
|
742
|
+
gw_info_dict[f"{name}_SNR"] = np.nan
|
|
743
|
+
gw_info_dict[f"{name}_detectable"] = False
|
|
744
|
+
# Add logL for each PTA
|
|
745
|
+
for label in self.PTA_labels:
|
|
746
|
+
variant_data = self.pta_likelihoods.get(label, {})
|
|
747
|
+
actual_metrics = variant_data.get("PTArcade", {})
|
|
748
|
+
gw_info_dict[f"lnL_{label}"] = np.nan
|
|
749
|
+
for variant in PTA_LIKELIHOOD_VARIANTS:
|
|
750
|
+
metrics = variant_data.get(variant, {})
|
|
751
|
+
gw_info_dict[f"lnL_{variant}_{label}"] = np.nan
|
|
752
|
+
gw_info_dict[f"delta_lnL_{variant}_{label}"] = np.nan
|
|
753
|
+
gw_info_dict[f"sigma_{variant}_{label}"] = np.nan
|
|
754
|
+
gw_info_dict[f"within_3sigma_{variant}_{label}"] = np.nan
|
|
755
|
+
# Add additional spectrum info
|
|
756
|
+
for key, value in self._flatten_additional_spec_info().items():
|
|
757
|
+
gw_info_dict[key] = value
|
|
758
|
+
# Add DNeff
|
|
759
|
+
gw_info_dict["DNeff_GW"] = np.nan
|
|
760
|
+
return gw_info_dict
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def return_gw_info_dict(self):
|
|
764
|
+
"""Collect SNRs, likelihoods and auxiliary info into a result dictionary."""
|
|
765
|
+
gw_info_dict = {}
|
|
766
|
+
# Add SNR and detectable for each detector
|
|
767
|
+
for name, snr, detectable in zip(self.det_names, self.SNR, self.detectable):
|
|
768
|
+
gw_info_dict[f"{name}_SNR"] = snr
|
|
769
|
+
gw_info_dict[f"{name}_detectable"] = detectable
|
|
770
|
+
# Add logL for each PTA
|
|
771
|
+
for label in self.PTA_labels:
|
|
772
|
+
variant_data = self.pta_likelihoods.get(label, {})
|
|
773
|
+
actual_metrics = variant_data.get("PTArcade", {})
|
|
774
|
+
gw_info_dict[f"lnL_{label}"] = actual_metrics.get("lnL", np.nan)
|
|
775
|
+
for variant in PTA_LIKELIHOOD_VARIANTS:
|
|
776
|
+
metrics = variant_data.get(variant, {})
|
|
777
|
+
gw_info_dict[f"lnL_{variant}_{label}"] = metrics.get("lnL", np.nan)
|
|
778
|
+
gw_info_dict[f"delta_lnL_{variant}_{label}"] = metrics.get("delta_lnL", np.nan)
|
|
779
|
+
gw_info_dict[f"sigma_{variant}_{label}"] = metrics.get("sigma", np.nan)
|
|
780
|
+
within_val = metrics.get("within_3sigma", np.nan)
|
|
781
|
+
if isinstance(within_val, (bool, np.bool_)):
|
|
782
|
+
gw_info_dict[f"within_3sigma_{variant}_{label}"] = bool(within_val)
|
|
783
|
+
else:
|
|
784
|
+
gw_info_dict[f"within_3sigma_{variant}_{label}"] = np.nan
|
|
785
|
+
# Add additional spectrum info
|
|
786
|
+
for key, value in self._flatten_additional_spec_info().items():
|
|
787
|
+
gw_info_dict[key] = value
|
|
788
|
+
# Add DNeff
|
|
789
|
+
gw_info_dict["DNeff_GW"] = self.DNeff_GW
|
|
790
|
+
return gw_info_dict
|