lisaanalysistools 1.1.20__cp39-cp39-macosx_15_0_arm64.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.
- lisaanalysistools/git_version.py +7 -0
- lisaanalysistools-1.1.20.dist-info/METADATA +281 -0
- lisaanalysistools-1.1.20.dist-info/RECORD +48 -0
- lisaanalysistools-1.1.20.dist-info/WHEEL +5 -0
- lisaanalysistools-1.1.20.dist-info/licenses/LICENSE +201 -0
- lisatools/.dylibs/libgcc_s.1.1.dylib +0 -0
- lisatools/.dylibs/libstdc++.6.dylib +0 -0
- lisatools/__init__.py +90 -0
- lisatools/_version.py +34 -0
- lisatools/analysiscontainer.py +474 -0
- lisatools/cutils/Detector.cu +307 -0
- lisatools/cutils/Detector.hpp +84 -0
- lisatools/cutils/__init__.py +129 -0
- lisatools/cutils/global.hpp +28 -0
- lisatools/cutils/pycppdetector.pyx +256 -0
- lisatools/datacontainer.py +312 -0
- lisatools/detector.py +867 -0
- lisatools/diagnostic.py +990 -0
- lisatools/git_version.py.in +7 -0
- lisatools/orbit_files/equalarmlength-orbits-best-fit-to-esa.h5 +0 -0
- lisatools/orbit_files/equalarmlength-orbits.h5 +0 -0
- lisatools/orbit_files/esa-trailing-orbits.h5 +0 -0
- lisatools/sampling/__init__.py +0 -0
- lisatools/sampling/likelihood.py +882 -0
- lisatools/sampling/moves/__init__.py +0 -0
- lisatools/sampling/moves/skymodehop.py +110 -0
- lisatools/sampling/prior.py +646 -0
- lisatools/sampling/stopping.py +320 -0
- lisatools/sampling/utility.py +411 -0
- lisatools/sensitivity.py +1554 -0
- lisatools/sources/__init__.py +6 -0
- lisatools/sources/bbh/__init__.py +1 -0
- lisatools/sources/bbh/waveform.py +106 -0
- lisatools/sources/defaultresponse.py +37 -0
- lisatools/sources/emri/__init__.py +1 -0
- lisatools/sources/emri/waveform.py +79 -0
- lisatools/sources/gb/__init__.py +1 -0
- lisatools/sources/gb/waveform.py +69 -0
- lisatools/sources/utils.py +459 -0
- lisatools/sources/waveformbase.py +41 -0
- lisatools/stochastic.py +327 -0
- lisatools/utils/__init__.py +0 -0
- lisatools/utils/constants.py +54 -0
- lisatools/utils/exceptions.py +95 -0
- lisatools/utils/parallelbase.py +11 -0
- lisatools/utils/utility.py +122 -0
- lisatools_backend_cpu/git_version.py +7 -0
- lisatools_backend_cpu/pycppdetector.cpython-39-darwin.so +0 -0
lisatools/diagnostic.py
ADDED
|
@@ -0,0 +1,990 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import warnings
|
|
3
|
+
from typing import Optional, Any, Tuple, List
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
|
|
7
|
+
from eryn.utils import TransformContainer
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import cupy as cp
|
|
13
|
+
|
|
14
|
+
except (ModuleNotFoundError, ImportError):
|
|
15
|
+
import numpy as cp
|
|
16
|
+
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
from .sensitivity import get_sensitivity, SensitivityMatrix
|
|
20
|
+
from .datacontainer import DataResidualArray
|
|
21
|
+
from .utils.utility import get_array_module
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def inner_product(
|
|
25
|
+
sig1: np.ndarray | list | DataResidualArray,
|
|
26
|
+
sig2: np.ndarray | list | DataResidualArray,
|
|
27
|
+
dt: Optional[float] = None,
|
|
28
|
+
df: Optional[float] = None,
|
|
29
|
+
f_arr: Optional[float] = None,
|
|
30
|
+
psd: Optional[str | None | np.ndarray | SensitivityMatrix] = "LISASens",
|
|
31
|
+
psd_args: Optional[tuple] = (),
|
|
32
|
+
psd_kwargs: Optional[dict] = {},
|
|
33
|
+
normalize: Optional[bool | str] = False,
|
|
34
|
+
complex: Optional[bool] = False,
|
|
35
|
+
) -> float | complex:
|
|
36
|
+
"""Compute the inner product between two signals weighted by a psd.
|
|
37
|
+
|
|
38
|
+
The inner product between time series :math:`a(t)` and :math:`b(t)` is
|
|
39
|
+
|
|
40
|
+
.. math::
|
|
41
|
+
|
|
42
|
+
\langle a | b \\rangle = 2\int_{f_\\text{min}}^{f_\\text{max}} \\frac{\\tilde{a}(f)^*\\tilde{b}(f) + \\tilde{a}(f)\\tilde{b}(f)^*}{S_n(f)} df\ \ ,
|
|
43
|
+
|
|
44
|
+
where :math:`\\tilde{a}(f)` is the Fourier transform of :math:`a(t)` and :math:`S_n(f)` is the one-sided Power Spectral Density of the noise.
|
|
45
|
+
|
|
46
|
+
The inner product can be left complex using the ``complex`` kwarg.
|
|
47
|
+
|
|
48
|
+
**GPU Capability**: Pass CuPy arrays rather than NumPy arrays.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
sig1: First signal to use for the inner product.
|
|
52
|
+
Can be time-domain or frequency-domain.
|
|
53
|
+
Must be 1D ``np.ndarray``, list of 1D ``np.ndarray``s, or 2D ``np.ndarray``
|
|
54
|
+
across channels with shape ``(nchannels, data length)``.
|
|
55
|
+
sig2: Second signal to use for the inner product.
|
|
56
|
+
Can be time-domain or frequency-domain.
|
|
57
|
+
Must be 1D ``np.ndarray``, list of 1D ``np.ndarray``s, or 2D ``np.ndarray``
|
|
58
|
+
across channels with shape ``(nchannels, data length)``.
|
|
59
|
+
dt: Time step in seconds. If provided, assumes time-domain signals.
|
|
60
|
+
df: Constant frequency spacing. This will assume a frequency domain signal with constant frequency spacing.
|
|
61
|
+
f_arr: Array of specific frequencies at which the signal is given.
|
|
62
|
+
psd: Indicator of what psd to use. If a ``str``, this will be passed as the ``sens_fn`` kwarg to :func:`get_sensitivity`.
|
|
63
|
+
If ``None``, it will be an array of ones. Or, you can pass a 1D ``np.ndarray`` of psd values that must be the same length
|
|
64
|
+
as the frequency domain signals.
|
|
65
|
+
psd_args: Arguments to pass to the psd function if ``type(psd) == str``.
|
|
66
|
+
psd_kwargs: Keyword arguments to pass to the psd function if ``type(psd) == str``.
|
|
67
|
+
normalize: Normalize the inner product. If ``True``, it will normalize the square root of the product of individual signal inner products.
|
|
68
|
+
You can also pass ``"sig1"`` or ``"sig2"`` to normalize with respect to one signal.
|
|
69
|
+
complex: If ``True``, return the complex value of the inner product rather than just its real-valued part.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Inner product value.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
# initial input checks and setup
|
|
76
|
+
sig1 = DataResidualArray(sig1, dt=dt, f_arr=f_arr, df=df)
|
|
77
|
+
sig2 = DataResidualArray(sig2, dt=dt, f_arr=f_arr, df=df)
|
|
78
|
+
|
|
79
|
+
if sig1.nchannels != sig2.nchannels:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Signal 1 has {sig1.nchannels} channels. Signal 2 has {sig2.nchannels} channels. Must be the same."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
xp = get_array_module(sig1[0])
|
|
85
|
+
|
|
86
|
+
# checks
|
|
87
|
+
for i in range(sig1.nchannels):
|
|
88
|
+
if not type(sig1[0]) == type(sig1[i]) and type(sig1[0]) == type(sig2[i]):
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"Array in sig1, index 0 sets array module. Not all arrays match that module type (Numpy or Cupy)"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if sig1.data_length != sig2.data_length:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"The two signals are two different lengths. Must be the same length."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
freqs = xp.asarray(sig1.f_arr)
|
|
99
|
+
|
|
100
|
+
# get psd weighting
|
|
101
|
+
if not isinstance(psd, SensitivityMatrix):
|
|
102
|
+
psd = SensitivityMatrix(freqs, [psd], *psd_args, **psd_kwargs)
|
|
103
|
+
|
|
104
|
+
operational_sets = []
|
|
105
|
+
|
|
106
|
+
if psd.ndim == 3:
|
|
107
|
+
assert psd.shape[0] == psd.shape[1] == sig1.shape[0] == sig2.shape[0]
|
|
108
|
+
|
|
109
|
+
for i in range(psd.shape[0]):
|
|
110
|
+
for j in range(i, psd.shape[1]):
|
|
111
|
+
factor = 1.0 if i == j else 2.0
|
|
112
|
+
operational_sets.append(
|
|
113
|
+
dict(factor=factor, sig1_ind=i, sig2_ind=j, psd_ind=(i, j))
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
elif psd.ndim == 2 and psd.shape[0] > 1:
|
|
117
|
+
assert psd.shape[0] == sig1.shape[0] == sig2.shape[0]
|
|
118
|
+
for i in range(psd.shape[0]):
|
|
119
|
+
operational_sets.append(dict(factor=1.0, sig1_ind=i, sig2_ind=i, psd_ind=i))
|
|
120
|
+
|
|
121
|
+
elif psd.ndim == 2 and psd.shape[0] == 1:
|
|
122
|
+
for i in range(sig1.shape[0]):
|
|
123
|
+
operational_sets.append(dict(factor=1.0, sig1_ind=i, sig2_ind=i, psd_ind=0))
|
|
124
|
+
|
|
125
|
+
else:
|
|
126
|
+
raise ValueError("# TODO")
|
|
127
|
+
|
|
128
|
+
if complex:
|
|
129
|
+
func = lambda x: x
|
|
130
|
+
else:
|
|
131
|
+
func = xp.real
|
|
132
|
+
|
|
133
|
+
# initialize
|
|
134
|
+
out = 0.0
|
|
135
|
+
x = freqs
|
|
136
|
+
|
|
137
|
+
# account for hp and hx if included in time domain signal
|
|
138
|
+
for op_set in operational_sets:
|
|
139
|
+
factor = op_set["factor"]
|
|
140
|
+
|
|
141
|
+
temp1 = sig1[op_set["sig1_ind"]]
|
|
142
|
+
temp2 = sig2[op_set["sig2_ind"]]
|
|
143
|
+
inv_psd_tmp = psd.invC[op_set["psd_ind"]]
|
|
144
|
+
|
|
145
|
+
ind_start = 1 if np.isnan(inv_psd_tmp[0]) else 0
|
|
146
|
+
|
|
147
|
+
y = (
|
|
148
|
+
func(temp1[ind_start:].conj() * temp2[ind_start:]) * inv_psd_tmp[ind_start:]
|
|
149
|
+
) # assumes right summation rule
|
|
150
|
+
# df is sunk into trapz
|
|
151
|
+
tmp_out = factor * 4 * xp.trapz(y, x=x[ind_start:])
|
|
152
|
+
out += tmp_out
|
|
153
|
+
|
|
154
|
+
# normalize the inner produce
|
|
155
|
+
normalization_value = 1.0
|
|
156
|
+
if normalize is True:
|
|
157
|
+
norm1 = inner_product(
|
|
158
|
+
sig1,
|
|
159
|
+
sig1,
|
|
160
|
+
psd=psd,
|
|
161
|
+
normalize=False,
|
|
162
|
+
)
|
|
163
|
+
norm2 = inner_product(
|
|
164
|
+
sig2,
|
|
165
|
+
sig2,
|
|
166
|
+
psd=psd,
|
|
167
|
+
normalize=False,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
normalization_value = np.sqrt(norm1 * norm2)
|
|
171
|
+
|
|
172
|
+
elif isinstance(normalize, str):
|
|
173
|
+
if normalize == "sig1":
|
|
174
|
+
sig_to_normalize = sig1
|
|
175
|
+
|
|
176
|
+
elif normalize == "sig2":
|
|
177
|
+
sig_to_normalize = sig2
|
|
178
|
+
|
|
179
|
+
else:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
"If normalizing with respect to sig1 or sig2, normalize kwarg must either be 'sig1' or 'sig2'."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
normalization_value = inner_product(
|
|
185
|
+
sig_to_normalize,
|
|
186
|
+
sig_to_normalize,
|
|
187
|
+
psd=psd,
|
|
188
|
+
normalize=False,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
elif normalize is not False:
|
|
192
|
+
raise ValueError("Normalize must be True, False, 'sig1', or 'sig2'.")
|
|
193
|
+
|
|
194
|
+
out /= normalization_value
|
|
195
|
+
|
|
196
|
+
# remove from cupy if needed
|
|
197
|
+
try:
|
|
198
|
+
out = out.item()
|
|
199
|
+
except AttributeError:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
# add copy function to complex value for compatibility
|
|
203
|
+
if complex:
|
|
204
|
+
out = np.complex128(out)
|
|
205
|
+
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def residual_source_likelihood_term(
|
|
210
|
+
data_res_arr: DataResidualArray, **kwargs: dict
|
|
211
|
+
) -> float | complex:
|
|
212
|
+
"""Calculate the source term in the Likelihood for a data residual (d - h).
|
|
213
|
+
|
|
214
|
+
The source term in the likelihood is given by,
|
|
215
|
+
|
|
216
|
+
.. math::
|
|
217
|
+
|
|
218
|
+
\\log{\\mathcal{L}}_\\text{src} = -\\frac{1}{2}\\langle \\vec{d} - \\vec{h} | \\vec{d} - \\vec{h}\\rangle.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
data_res_arr: Data residual.
|
|
222
|
+
**kwargs: Keyword arguments to pass to :func:`inner_product`.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Source term Likelihood value.
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
kwargs["normalize"] = False
|
|
229
|
+
ip_val = inner_product(data_res_arr, data_res_arr, **kwargs)
|
|
230
|
+
return -1 / 2.0 * ip_val
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def noise_likelihood_term(psd: SensitivityMatrix) -> float:
|
|
234
|
+
"""Calculate the noise term in the Likelihood.
|
|
235
|
+
|
|
236
|
+
The noise term in the likelihood is given by,
|
|
237
|
+
|
|
238
|
+
.. math::
|
|
239
|
+
|
|
240
|
+
\\log{\\mathcal{L}}_n = -\\sum \\log{\\vec{S}_n}.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
psd: Sensitivity information.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
Noise term Likelihood value.
|
|
247
|
+
|
|
248
|
+
"""
|
|
249
|
+
fix = np.isnan(psd[:]) | np.isinf(psd[:])
|
|
250
|
+
assert np.sum(fix) == np.prod(psd.shape[:-1]) or np.sum(fix) == 0
|
|
251
|
+
# TODO: check on this / add warning
|
|
252
|
+
detC = psd.detC
|
|
253
|
+
keep = (detC != 0.0) & (~np.isinf(detC)) & (~np.isnan(detC))
|
|
254
|
+
nl_val = -np.sum(np.log(np.abs(detC[keep])))
|
|
255
|
+
return nl_val
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def residual_full_source_and_noise_likelihood(
|
|
259
|
+
data_res_arr: DataResidualArray,
|
|
260
|
+
psd: str | None | np.ndarray | SensitivityMatrix,
|
|
261
|
+
**kwargs: dict,
|
|
262
|
+
) -> float | complex:
|
|
263
|
+
"""Calculate the full Likelihood including noise and source terms.
|
|
264
|
+
|
|
265
|
+
The noise term is calculated with :func:`noise_likelihood_term`.
|
|
266
|
+
|
|
267
|
+
The source term is calcualted with :func:`residual_source_likelihood_term`.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
data_res_arr: Data residual.
|
|
271
|
+
psd: Sensitivity information.
|
|
272
|
+
**kwargs: Keyword arguments to pass to :func:`inner_product`.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Full Likelihood value.
|
|
276
|
+
|
|
277
|
+
"""
|
|
278
|
+
if not isinstance(psd, SensitivityMatrix):
|
|
279
|
+
# TODO: maybe adjust so it can take a list just like Sensitivity matrix
|
|
280
|
+
psd = SensitivityMatrix(data_res_arr.f_arr, [psd], **kwargs)
|
|
281
|
+
|
|
282
|
+
# remove key
|
|
283
|
+
for key in "psd", "psd_args", "psd_kwargs":
|
|
284
|
+
if key in kwargs:
|
|
285
|
+
kwargs.pop(key)
|
|
286
|
+
|
|
287
|
+
rslt = residual_source_likelihood_term(data_res_arr, psd=psd, **kwargs)
|
|
288
|
+
|
|
289
|
+
nlt = noise_likelihood_term(psd)
|
|
290
|
+
return nlt + rslt
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def data_signal_source_likelihood_term(
|
|
294
|
+
data_arr: DataResidualArray, sig_arr: DataResidualArray, **kwargs: dict
|
|
295
|
+
) -> float | complex:
|
|
296
|
+
"""Calculate the source term in the Likelihood for separate signal and data.
|
|
297
|
+
|
|
298
|
+
The source term in the likelihood is given by,
|
|
299
|
+
|
|
300
|
+
.. math::
|
|
301
|
+
|
|
302
|
+
\\log{\\mathcal{L}}_\\text{src} = -\\frac{1}{2}\\left(\\langle \\vec{d} | \\vec{d}\\rangle + \\langle \\vec{h} | \\vec{h}\\rangle - 2\\langle \\vec{d} | \\vec{h}\\rangle \\right)\ \ .
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
data_arr: Data.
|
|
306
|
+
sig_arr: Signal.
|
|
307
|
+
**kwargs: Keyword arguments to pass to :func:`inner_product`.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
Source term Likelihood value.
|
|
311
|
+
|
|
312
|
+
"""
|
|
313
|
+
kwargs["normalize"] = False
|
|
314
|
+
d_h = inner_product(data_arr, sig_arr, **kwargs)
|
|
315
|
+
h_h = inner_product(sig_arr, sig_arr, **kwargs)
|
|
316
|
+
d_d = inner_product(data_arr, data_arr, **kwargs)
|
|
317
|
+
return -1 / 2.0 * (d_d + h_h - 2 * d_h)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def data_signal_full_source_and_noise_likelihood(
|
|
321
|
+
data_arr: DataResidualArray,
|
|
322
|
+
sig_arr: DataResidualArray,
|
|
323
|
+
psd: str | None | np.ndarray | SensitivityMatrix,
|
|
324
|
+
**kwargs: dict,
|
|
325
|
+
) -> float | complex:
|
|
326
|
+
"""Calculate the full Likelihood including noise and source terms.
|
|
327
|
+
|
|
328
|
+
Here, the signal is treated separate from the data.
|
|
329
|
+
|
|
330
|
+
The noise term is calculated with :func:`noise_likelihood_term`.
|
|
331
|
+
|
|
332
|
+
The source term is calcualted with :func:`data_signal_source_likelihood_term`.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
data_arr: Data.
|
|
336
|
+
sig_arr: Signal.
|
|
337
|
+
psd: Sensitivity information.
|
|
338
|
+
**kwargs: Keyword arguments to pass to :func:`inner_product`.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Full Likelihood value.
|
|
342
|
+
|
|
343
|
+
"""
|
|
344
|
+
if not isinstance(psd, SensitivityMatrix):
|
|
345
|
+
# TODO: maybe adjust so it can take a list just like Sensitivity matrix
|
|
346
|
+
psd = SensitivityMatrix(data_arr.f_arr, [psd], **kwargs)
|
|
347
|
+
|
|
348
|
+
# remove key
|
|
349
|
+
for key in "psd", "psd_args", "psd_kwargs":
|
|
350
|
+
if key in kwargs:
|
|
351
|
+
kwargs.pop(key)
|
|
352
|
+
|
|
353
|
+
rslt = data_signal_source_likelihood_term(data_arr, sig_arr, psd=psd, **kwargs)
|
|
354
|
+
|
|
355
|
+
nlt = noise_likelihood_term(psd)
|
|
356
|
+
|
|
357
|
+
return nlt + rslt
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def snr(
|
|
361
|
+
sig1: np.ndarray | list | DataResidualArray,
|
|
362
|
+
*args: Any,
|
|
363
|
+
data: Optional[np.ndarray | list | DataResidualArray] = None,
|
|
364
|
+
**kwargs: Any,
|
|
365
|
+
) -> float:
|
|
366
|
+
"""Compute the snr between two signals weighted by a psd.
|
|
367
|
+
|
|
368
|
+
The signal-to-noise ratio of a signal is :math:`\\sqrt{\\langle a|a\\rangle}`.
|
|
369
|
+
|
|
370
|
+
This will be the optimal SNR if ``data==None``. If a data array is given, it will be the observed
|
|
371
|
+
SNR: :math:`\\langle d|a\\rangle/\\sqrt{\\langle a|a\\rangle}`.
|
|
372
|
+
|
|
373
|
+
**GPU Capability**: Pass CuPy arrays rather than NumPy arrays.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
sig1: Signal to use as the templatefor the SNR.
|
|
377
|
+
Can be time-domain or frequency-domain.
|
|
378
|
+
Must be 1D ``np.ndarray``, list of 1D ``np.ndarray``s, or 2D ``np.ndarray``
|
|
379
|
+
across channels with shape ``(nchannels, data length)``.
|
|
380
|
+
*args: Arguments to pass to :func:`inner_product`.
|
|
381
|
+
data: Data becomes the ``sig2`` argument to :func:`inner_product`.
|
|
382
|
+
**kwargs: Keyword arguments to pass to :func:`inner_product`.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Optimal or detected SNR value (depending on ``data`` kwarg).
|
|
386
|
+
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
# get optimal SNR
|
|
390
|
+
opt_snr = np.sqrt(inner_product(sig1, sig1, *args, **kwargs).real)
|
|
391
|
+
if data is None:
|
|
392
|
+
return opt_snr
|
|
393
|
+
|
|
394
|
+
else:
|
|
395
|
+
# if inputed data, calculate detected SNR
|
|
396
|
+
det_snr = inner_product(sig1, data, *args, **kwargs).real / opt_snr
|
|
397
|
+
return det_snr
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def h_var_p_eps(
|
|
401
|
+
step: float,
|
|
402
|
+
waveform_model: callable,
|
|
403
|
+
params: np.ndarray | list,
|
|
404
|
+
index: int,
|
|
405
|
+
parameter_transforms: Optional[TransformContainer] = None,
|
|
406
|
+
waveform_args: Optional[tuple] = (),
|
|
407
|
+
waveform_kwargs: Optional[dict] = {},
|
|
408
|
+
) -> np.ndarray: # TODO: check this
|
|
409
|
+
"""Calculate the waveform with a perturbation step of the variable V[i]
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
waveform_model: Callable function to the waveform generator with signature ``(*params, **waveform_kwargs)``.
|
|
413
|
+
params: Source parameters that are over derivatives (not in fill dict of parameter transforms)
|
|
414
|
+
step: Absolute step size for variable of interest.
|
|
415
|
+
index: Index to parameter of interest.
|
|
416
|
+
parameter_transforms: `TransformContainer <https://mikekatz04.github.io/Eryn/user/utils.html#eryn.utils.TransformContainer>`_ object to transform from the derivative parameter basis
|
|
417
|
+
to the waveform parameter basis. This class can also fill in fixed parameters where the derivatives are not being taken.
|
|
418
|
+
waveform_args: args (beyond parameters) for the waveform generator.
|
|
419
|
+
waveform_kwargs: kwargs for the waveform generation.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Perturbation to the waveform in the given parameter. Will always be 2D array with shape ``(num channels, data length)``
|
|
423
|
+
|
|
424
|
+
"""
|
|
425
|
+
params_p_eps = params.copy()
|
|
426
|
+
params_p_eps[index] += step
|
|
427
|
+
|
|
428
|
+
if parameter_transforms is not None:
|
|
429
|
+
# transform
|
|
430
|
+
params_p_eps = parameter_transforms.transform_base_parameters(
|
|
431
|
+
params_p_eps[None, :]
|
|
432
|
+
)[0]
|
|
433
|
+
|
|
434
|
+
args_in = tuple(params_p_eps) + tuple(waveform_args)
|
|
435
|
+
dh = waveform_model(*args_in, **waveform_kwargs)
|
|
436
|
+
|
|
437
|
+
# adjust output based on waveform model output
|
|
438
|
+
# needs to be 2D array
|
|
439
|
+
if (isinstance(dh, np.ndarray) or isinstance(dh, cp.ndarray)) and dh.ndim == 1:
|
|
440
|
+
xp = get_array_module(dh)
|
|
441
|
+
dh = xp.atleast_2d(dh)
|
|
442
|
+
|
|
443
|
+
elif isinstance(dh, list):
|
|
444
|
+
xp = get_array_module(dh[0])
|
|
445
|
+
dh = xp.asarray(dh)
|
|
446
|
+
|
|
447
|
+
return dh
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def dh_dlambda(
|
|
451
|
+
eps: float,
|
|
452
|
+
*args: tuple,
|
|
453
|
+
more_accurate: Optional[bool] = True,
|
|
454
|
+
**kwargs: dict,
|
|
455
|
+
) -> np.ndarray:
|
|
456
|
+
"""Derivative of the waveform
|
|
457
|
+
|
|
458
|
+
Calculate the derivative of the waveform with precision of order (step^4)
|
|
459
|
+
with respect to the variable V in the i direction.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
eps: Absolute **derivative** step size for variable of interest.
|
|
463
|
+
*args: Arguments passed to :func:`h_var_p_eps`.
|
|
464
|
+
more_accurate: If ``True``, run a more accurate derivate requiring 2x more waveform generations.
|
|
465
|
+
**kwargs: Keyword arguments passed to :func:`h_var_p_eps`.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Numerical derivative of the waveform with respect to a varibale of interest. Will be 2D array
|
|
469
|
+
with shape ``(num channels, data length)``.
|
|
470
|
+
|
|
471
|
+
"""
|
|
472
|
+
if more_accurate:
|
|
473
|
+
# Derivative of the Waveform
|
|
474
|
+
# up
|
|
475
|
+
h_I_up_2eps = h_var_p_eps(2 * eps, *args, **kwargs)
|
|
476
|
+
h_I_up_eps = h_var_p_eps(eps, *args, **kwargs)
|
|
477
|
+
# down
|
|
478
|
+
h_I_down_2eps = h_var_p_eps(-2 * eps, *args, **kwargs)
|
|
479
|
+
h_I_down_eps = h_var_p_eps(-eps, *args, **kwargs)
|
|
480
|
+
|
|
481
|
+
# make sure they are all the same length
|
|
482
|
+
ind_max = np.min(
|
|
483
|
+
[
|
|
484
|
+
h_I_up_2eps.shape[1],
|
|
485
|
+
h_I_up_eps.shape[1],
|
|
486
|
+
h_I_down_2eps.shape[1],
|
|
487
|
+
h_I_down_eps.shape[1],
|
|
488
|
+
]
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# error scales as eps^4
|
|
492
|
+
dh_I = (
|
|
493
|
+
-h_I_up_2eps[:, :ind_max]
|
|
494
|
+
+ h_I_down_2eps[:, :ind_max]
|
|
495
|
+
+ 8 * (h_I_up_eps[:, :ind_max] - h_I_down_eps[:, :ind_max])
|
|
496
|
+
) / (12 * eps)
|
|
497
|
+
|
|
498
|
+
else:
|
|
499
|
+
# Derivative of the Waveform
|
|
500
|
+
# up
|
|
501
|
+
h_I_up_eps = h_var_p_eps(eps, *args, **kwargs)
|
|
502
|
+
# down
|
|
503
|
+
h_I_down_eps = h_var_p_eps(-eps, *args, **kwargs)
|
|
504
|
+
|
|
505
|
+
ind_max = np.min([h_I_up_eps.shape[1], h_I_down_eps.shape[1]])
|
|
506
|
+
|
|
507
|
+
# TODO: check what error scales as.
|
|
508
|
+
dh_I = (h_I_up_eps[:, :ind_max] - h_I_down_eps[:, :ind_max]) / (2 * eps)
|
|
509
|
+
# Time thta it takes for one variable: approx 5 minutes
|
|
510
|
+
|
|
511
|
+
return dh_I
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def info_matrix(
|
|
515
|
+
eps: float | np.ndarray,
|
|
516
|
+
waveform_model: callable,
|
|
517
|
+
params: np.ndarray | list,
|
|
518
|
+
deriv_inds: Optional[np.ndarray | list] = None,
|
|
519
|
+
inner_product_kwargs: Optional[dict] = {},
|
|
520
|
+
return_derivs: Optional[bool] = False,
|
|
521
|
+
**kwargs: dict,
|
|
522
|
+
) -> np.ndarray | Tuple[np.ndarray, list]:
|
|
523
|
+
"""Calculate Information Matrix.
|
|
524
|
+
|
|
525
|
+
This calculates the information matrix for a given waveform model at a given set of parameters.
|
|
526
|
+
The inverse of the information matrix gives the covariance matrix.
|
|
527
|
+
|
|
528
|
+
This is also referred to as the Fisher information matrix, but @MichaelKatz has chosen to leave out the name because of `this <https://www.newstatesman.com/long-reads/2020/07/ra-fisher-and-science-hatred>`_.
|
|
529
|
+
|
|
530
|
+
The info matrix is given by:
|
|
531
|
+
|
|
532
|
+
.. math::
|
|
533
|
+
|
|
534
|
+
M_{ij} = \\langle h_i | h_j \\rangle \\text{ with } h_i = \\frac{\\partial h}{\\partial \\lambda_i}.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
eps: Absolute **derivative** step size for variable of interest. Can be provided as a ``float`` value that applies
|
|
538
|
+
to all variables or an array, one for each parameter being evaluated in the information matrix.
|
|
539
|
+
waveform_model: Callable function to the waveform generator with signature ``(*params, **waveform_kwargs)``.
|
|
540
|
+
params: Source parameters.
|
|
541
|
+
deriv_inds: Subset of parameters of interest for which to calculate the information matrix, by index.
|
|
542
|
+
If ``None``, it will be ``np.arange(len(params))``.
|
|
543
|
+
inner_product_kwargs: Keyword arguments for the inner product function.
|
|
544
|
+
return_derivs: If ``True``, also returns computed numerical derivatives.
|
|
545
|
+
**kwargs: Keyword arguments passed to :func:`dh_dlambda`.
|
|
546
|
+
|
|
547
|
+
Returns:
|
|
548
|
+
If ``not return_derivs``, this will be the information matrix as a numpy array. If ``return_derivs is True``,
|
|
549
|
+
it will be a tuple with the first entry as the information matrix and the second entry as the partial derivatives.
|
|
550
|
+
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
# setup initial information
|
|
554
|
+
num_params = len(params)
|
|
555
|
+
|
|
556
|
+
if deriv_inds is None:
|
|
557
|
+
deriv_inds = np.arange(num_params)
|
|
558
|
+
|
|
559
|
+
num_info_params = len(deriv_inds)
|
|
560
|
+
|
|
561
|
+
if isinstance(eps, float):
|
|
562
|
+
eps = np.full_like(params, eps)
|
|
563
|
+
|
|
564
|
+
# collect derivatives over the variables of interest
|
|
565
|
+
dh = []
|
|
566
|
+
for i, eps_i in zip(deriv_inds, eps):
|
|
567
|
+
# derivative up
|
|
568
|
+
temp = dh_dlambda(eps_i, waveform_model, params, i, **kwargs)
|
|
569
|
+
dh.append(temp)
|
|
570
|
+
|
|
571
|
+
# calculate the components of the symmetric matrix
|
|
572
|
+
info = np.zeros((num_info_params, num_info_params))
|
|
573
|
+
for i in range(num_info_params):
|
|
574
|
+
for j in range(i, num_info_params):
|
|
575
|
+
info[i][j] = inner_product(
|
|
576
|
+
[dh[i][k] for k in range(len(dh[i]))],
|
|
577
|
+
[dh[j][k] for k in range(len(dh[i]))],
|
|
578
|
+
**inner_product_kwargs,
|
|
579
|
+
)
|
|
580
|
+
info[j][i] = info[i][j]
|
|
581
|
+
|
|
582
|
+
if return_derivs:
|
|
583
|
+
return info, dh
|
|
584
|
+
else:
|
|
585
|
+
return info
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def covariance(
|
|
589
|
+
*info_mat_args: tuple,
|
|
590
|
+
info_mat: Optional[np.ndarray] = None,
|
|
591
|
+
diagonalize: Optional[bool] = False,
|
|
592
|
+
return_info_mat: Optional[bool] = False,
|
|
593
|
+
precision: Optional[bool] = False,
|
|
594
|
+
**info_mat_kwargs: dict,
|
|
595
|
+
) -> np.ndarray | list:
|
|
596
|
+
"""Calculate covariance matrix for a set of EMRI parameters, computing the information matrix if not supplied.
|
|
597
|
+
|
|
598
|
+
Args:
|
|
599
|
+
*info_mat_args: Set of arguments to pass to :func:`info_matrix`. Not required if ``info_mat`` is not ``None``.
|
|
600
|
+
info_mat: Pre-computed information matrix. If supplied, this matrix will be inverted.
|
|
601
|
+
diagonalize: If ``True``, diagonalizes the covariance matrix.
|
|
602
|
+
return_info_mat: If ``True``, also returns the computed information matrix.
|
|
603
|
+
precision: If ``True``, uses 500-dps precision to compute the information matrix inverse (requires `mpmath <https://mpmath.org>`_). This is typically a good idea as the information matrix can be highly ill-conditioned.
|
|
604
|
+
**info_mat_kwargs: Keyword arguments to pass to :func:`info_matrix`.
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
Covariance matrix. If ``return_info_mat is True``. A list will be returned with the covariance as the first
|
|
608
|
+
entry and the information matrix as the second entry. If ``return_derivs is True`` (keyword argument to :func:`info_matrix`),
|
|
609
|
+
then another entry will be added to the list for the derivatives.
|
|
610
|
+
|
|
611
|
+
"""
|
|
612
|
+
|
|
613
|
+
if info_mat is None:
|
|
614
|
+
info_mat = info_matrix(*info_mat_args, **info_mat_kwargs)
|
|
615
|
+
|
|
616
|
+
# parse output properly
|
|
617
|
+
if "return_derivs" in info_mat_kwargs and info_mat_kwargs["return_derivs"]:
|
|
618
|
+
return_derivs = True
|
|
619
|
+
info_mat, dh = info_mat
|
|
620
|
+
|
|
621
|
+
else:
|
|
622
|
+
return_derivs = False
|
|
623
|
+
|
|
624
|
+
# attempt to import and setup mpmath if precision required
|
|
625
|
+
if precision:
|
|
626
|
+
try:
|
|
627
|
+
import mpmath as mp
|
|
628
|
+
|
|
629
|
+
mp.mp.dps = 500
|
|
630
|
+
except ModuleNotFoundError:
|
|
631
|
+
print("mpmath module not installed. Defaulting to low precision...")
|
|
632
|
+
precision = False
|
|
633
|
+
|
|
634
|
+
if precision:
|
|
635
|
+
hp_info_mat = mp.matrix(info_mat.tolist())
|
|
636
|
+
U, S, V = mp.svd_r(hp_info_mat) # singular value decomposition
|
|
637
|
+
temp = mp.diag([val ** (-1) for val in S]) # get S**-1
|
|
638
|
+
temp2 = V.T * temp * U.T # construct pseudo-inverse
|
|
639
|
+
cov = np.array(temp2.tolist(), dtype=np.float64)
|
|
640
|
+
else:
|
|
641
|
+
cov = np.linalg.pinv(info_mat)
|
|
642
|
+
|
|
643
|
+
if diagonalize:
|
|
644
|
+
# get eigeninformation
|
|
645
|
+
eig_vals, eig_vecs = get_eigeninfo(cov, high_precision=precision)
|
|
646
|
+
|
|
647
|
+
# diagonal cov now
|
|
648
|
+
cov = np.dot(np.dot(eig_vecs.T, cov), eig_vecs)
|
|
649
|
+
|
|
650
|
+
# just requesting covariance
|
|
651
|
+
if True not in [return_info_mat, return_derivs]:
|
|
652
|
+
return cov
|
|
653
|
+
|
|
654
|
+
# if desiring more information, create a list capable of variable size
|
|
655
|
+
returns = [cov]
|
|
656
|
+
|
|
657
|
+
# add information matrix
|
|
658
|
+
if return_info_mat:
|
|
659
|
+
returns.append(info_mat)
|
|
660
|
+
|
|
661
|
+
# add derivatives
|
|
662
|
+
if return_derivs:
|
|
663
|
+
returns.append(dh)
|
|
664
|
+
|
|
665
|
+
return returns
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def plot_covariance_corner(
|
|
669
|
+
params: np.ndarray,
|
|
670
|
+
cov: np.ndarray,
|
|
671
|
+
nsamp: Optional[int] = 25000,
|
|
672
|
+
fig: Optional[plt.Figure] = None,
|
|
673
|
+
**kwargs: dict,
|
|
674
|
+
) -> plt.Figure:
|
|
675
|
+
"""Construct a corner plot for a given covariance matrix.
|
|
676
|
+
|
|
677
|
+
The `corner <https://corner.readthedocs.io/en/latest/>`_ module is required for this.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
params: The set of parameters used for the event (the mean vector of the covariance matrix).
|
|
681
|
+
cov: Covariance matrix from which to construct the corner plot.
|
|
682
|
+
nsamp: Number of samples to draw from the multivariate distribution.
|
|
683
|
+
fig: Matplotlib :class:`plt.Figure` object. Use this if passing an existing corner plot figure.
|
|
684
|
+
**kwargs: Keyword arguments for the corner plot - see the module documentation for more info.
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
The corner plot figure.
|
|
688
|
+
|
|
689
|
+
"""
|
|
690
|
+
|
|
691
|
+
# TODO: add capability for ChainConsumer?
|
|
692
|
+
try:
|
|
693
|
+
import corner
|
|
694
|
+
except ModuleNotFoundError:
|
|
695
|
+
raise ValueError(
|
|
696
|
+
"Attempting to plot using the corner module, but it is not installed."
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# generate fake samples from the covariance distribution
|
|
700
|
+
samp = np.random.multivariate_normal(params, cov, size=nsamp)
|
|
701
|
+
|
|
702
|
+
# make corner plot
|
|
703
|
+
fig = corner.corner(samp, **kwargs)
|
|
704
|
+
return fig
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def plot_covariance_contour(
|
|
708
|
+
params: np.ndarray,
|
|
709
|
+
cov: np.ndarray,
|
|
710
|
+
horizontal_index: int,
|
|
711
|
+
vertical_index: int,
|
|
712
|
+
nsamp: Optional[int] = 25000,
|
|
713
|
+
ax: Optional[plt.Axes] = None,
|
|
714
|
+
**kwargs: dict,
|
|
715
|
+
) -> plt.Axes | Tuple[plt.Figure, plt.Axes]:
|
|
716
|
+
"""Construct a contour plot for a given covariance matrix on a single axis object.
|
|
717
|
+
|
|
718
|
+
The `corner <https://corner.readthedocs.io/en/latest/>`_ module is required for this.
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
params: The set of parameters used for the event (the mean vector of the covariance matrix).
|
|
722
|
+
cov: Covariance matrix from which to construct the corner plot.
|
|
723
|
+
horizontal_index: Parameter index to plot along the horizontal axis of the contour plot.
|
|
724
|
+
vertical_index: Parameter index to plot along the vertical axis of the contour plot.
|
|
725
|
+
nsamp: Number of samples to draw from the multivariate distribution.
|
|
726
|
+
fig: Matplotlib :class:`plt.Figure` object. Use this if passing an existing corner plot figure.
|
|
727
|
+
**kwargs: Keyword arguments for the corner plot - see the module documentation for more info.
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
If ``ax`` is provided, the return will be that ax object. If it is not provided, then a
|
|
731
|
+
Matplotlib Figure and Axes obejct is created and returned as a tuple: ``(plt.Figure, plt.Axes)``.
|
|
732
|
+
|
|
733
|
+
"""
|
|
734
|
+
|
|
735
|
+
# TODO: add capability for ChainConsumer?
|
|
736
|
+
try:
|
|
737
|
+
import corner
|
|
738
|
+
except ModuleNotFoundError:
|
|
739
|
+
raise ModuleNotFoundError(
|
|
740
|
+
"Attempting to plot using the corner module, but it is not installed."
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
if ax is None:
|
|
744
|
+
fig, ax = plt.subplots(1, 1)
|
|
745
|
+
else:
|
|
746
|
+
fig = None
|
|
747
|
+
|
|
748
|
+
# generate fake samples from the covariance distribution
|
|
749
|
+
samp = np.random.multivariate_normal(params, cov, size=nsamp)
|
|
750
|
+
|
|
751
|
+
x = samp[:, horizontal_index]
|
|
752
|
+
y = samp[:, vertical_index]
|
|
753
|
+
|
|
754
|
+
# make corner plot
|
|
755
|
+
corner.hist2d(x, y, ax=ax, **kwargs)
|
|
756
|
+
|
|
757
|
+
if fig is None:
|
|
758
|
+
return ax
|
|
759
|
+
else:
|
|
760
|
+
return (fig, ax)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def get_eigeninfo(
|
|
764
|
+
arr: np.ndarray, high_precision: Optional[bool] = False
|
|
765
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
766
|
+
"""Performs eigenvalue decomposition and returns the eigenvalues and right-eigenvectors for the supplied fisher/covariance matrix.
|
|
767
|
+
|
|
768
|
+
Args:
|
|
769
|
+
arr: Input matrix for which to perform eigenvalue decomposition.
|
|
770
|
+
high_precision: If ``True``, use 500-dps precision to ensure accurate eigenvalue decomposition
|
|
771
|
+
(requires `mpmath <https://mpmath.org>`_ to be installed). Defaults to False.
|
|
772
|
+
|
|
773
|
+
Returns:
|
|
774
|
+
Tuple containing Eigenvalues and right-Eigenvectors for the supplied array, constructed such that evects[:,k] corresponds to evals[k].
|
|
775
|
+
|
|
776
|
+
"""
|
|
777
|
+
|
|
778
|
+
if high_precision:
|
|
779
|
+
try:
|
|
780
|
+
import mpmath as mp
|
|
781
|
+
|
|
782
|
+
mp.mp.dps = 500
|
|
783
|
+
except ModuleNotFoundError:
|
|
784
|
+
print("mpmath is not installed - using low-precision eigen decomposition.")
|
|
785
|
+
high_precision = False
|
|
786
|
+
|
|
787
|
+
if high_precision:
|
|
788
|
+
hp_arr = mp.matrix(arr.tolist())
|
|
789
|
+
# get eigenvectors
|
|
790
|
+
E, EL, ER = mp.eig(hp_arr, left=True, right=True)
|
|
791
|
+
|
|
792
|
+
# convert back
|
|
793
|
+
evals = np.array(E, dtype=np.float64)
|
|
794
|
+
evects = np.array(ER.tolist(), dtype=np.float64)
|
|
795
|
+
|
|
796
|
+
return evals, evects
|
|
797
|
+
|
|
798
|
+
else:
|
|
799
|
+
evals, evects = np.linalg.eig(arr)
|
|
800
|
+
return evals, evects
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def cutler_vallisneri_bias(
|
|
804
|
+
waveform_model_true: callable,
|
|
805
|
+
waveform_model_approx: callable,
|
|
806
|
+
params: np.ndarray,
|
|
807
|
+
eps: float | np.ndarray,
|
|
808
|
+
input_diagnostics: Optional[dict] = None,
|
|
809
|
+
info_mat: Optional[np.ndarray] = None,
|
|
810
|
+
deriv_inds: Optional[np.ndarray | list] = None,
|
|
811
|
+
return_derivs: Optional[bool] = False,
|
|
812
|
+
return_cov: Optional[bool] = False,
|
|
813
|
+
parameter_transforms: Optional[TransformContainer] = None,
|
|
814
|
+
waveform_true_args: Optional[tuple] = (),
|
|
815
|
+
waveform_true_kwargs: Optional[dict] = {},
|
|
816
|
+
waveform_approx_args: Optional[tuple] = (),
|
|
817
|
+
waveform_approx_kwargs: Optional[dict] = {},
|
|
818
|
+
inner_product_kwargs: Optional[dict] = {},
|
|
819
|
+
) -> list:
|
|
820
|
+
"""Calculate the Cutler-Vallisneri bias.
|
|
821
|
+
|
|
822
|
+
# TODO: add basic math
|
|
823
|
+
|
|
824
|
+
Args:
|
|
825
|
+
waveform_model_true: Callable function to the **true** waveform generator with signature ``(*params, **waveform_kwargs)``.
|
|
826
|
+
waveform_model_approx: Callable function to the **approximate** waveform generator with signature ``(*params, **waveform_kwargs)``.
|
|
827
|
+
params: Source parameters.
|
|
828
|
+
eps: Absolute **derivative** step size. See :func:`info_matrix`.
|
|
829
|
+
input_diagnostics: Dictionary including the diagnostic information if it is precomputed. Dictionary must include
|
|
830
|
+
keys ``"cov"`` (covariance matrix, output of :func:`covariance`), ``"h_true"`` (the **true** waveform),
|
|
831
|
+
and ``"dh"`` (derivatives of the waveforms, list of outputs from :func:`dh_dlambda`).
|
|
832
|
+
info_mat: Pre-computed information matrix. If supplied, this matrix will be inverted to find the covariance.
|
|
833
|
+
deriv_inds: Subset of parameters of interest. See :func:`info_matrix`.
|
|
834
|
+
return_derivs: If ``True``, also returns computed numerical derivatives.
|
|
835
|
+
return_cov: If ``True``, also returns computed covariance matrix.
|
|
836
|
+
parameter_transforms: `TransformContainer <https://mikekatz04.github.io/Eryn/user/utils.html#eryn.utils.TransformContainer>`_ object. See :func:`info_matrix`.
|
|
837
|
+
waveform_true_args: Arguments for the **true** waveform generator.
|
|
838
|
+
waveform_true_kwargs: Keyword arguments for the **true** waveform generator.
|
|
839
|
+
waveform_approx_args: Arguments for the **approximate** waveform generator.
|
|
840
|
+
waveform_approx_kwargs: Keyword arguments for the **approximate** waveform generator.
|
|
841
|
+
inner_product_kwargs: Keyword arguments for the inner product function.
|
|
842
|
+
|
|
843
|
+
Returns:
|
|
844
|
+
List of return information. By default, it is ``[systematic error, bias]``.
|
|
845
|
+
If ``return_derivs`` or ``return_cov`` are ``True``, they will be added to the list with derivs added before covs.
|
|
846
|
+
|
|
847
|
+
"""
|
|
848
|
+
|
|
849
|
+
if deriv_inds is None:
|
|
850
|
+
deriv_inds = np.arange(len(params))
|
|
851
|
+
|
|
852
|
+
if info_mat is not None and input_diagnostics is not None:
|
|
853
|
+
warnings.warn(
|
|
854
|
+
"Provided info_mat and input_diagnostics kwargs. Ignoring info_mat."
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
# adjust parameters to waveform basis
|
|
858
|
+
params_in = parameter_transforms.transform_base_parameters(params.copy())
|
|
859
|
+
|
|
860
|
+
if input_diagnostics is None:
|
|
861
|
+
# get true waveform
|
|
862
|
+
h_true = waveform_model_true(
|
|
863
|
+
*(tuple(params_in) + tuple(waveform_true_args)), **waveform_true_kwargs
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
# get covariance info and waveform derivatives
|
|
867
|
+
cov, dh = covariance(
|
|
868
|
+
eps,
|
|
869
|
+
waveform_model_true,
|
|
870
|
+
params,
|
|
871
|
+
return_derivs=True,
|
|
872
|
+
deriv_inds=deriv_inds,
|
|
873
|
+
info_mat=info_mat,
|
|
874
|
+
parameter_transforms=parameter_transforms,
|
|
875
|
+
waveform_args=waveform_true_args,
|
|
876
|
+
waveform_kwargs=waveform_true_kwargs,
|
|
877
|
+
inner_product_kwargs=inner_product_kwargs,
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
else:
|
|
881
|
+
# pre-computed info
|
|
882
|
+
cov = input_diagnostics["cov"]
|
|
883
|
+
h_true = input_diagnostics["h_true"]
|
|
884
|
+
dh = input_diagnostics["dh"]
|
|
885
|
+
|
|
886
|
+
# get approximate waveform
|
|
887
|
+
h_approx = waveform_model_approx(
|
|
888
|
+
*(tuple(params_in) + tuple(waveform_approx_args)), **waveform_approx_kwargs
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
# adjust/check waveform outputs
|
|
892
|
+
if isinstance(h_true, np.ndarray) and h_true.ndim == 1:
|
|
893
|
+
h_true = [h_true]
|
|
894
|
+
elif isinstance(h_true, np.ndarray) and h_true.ndim == 2:
|
|
895
|
+
h_true = list(h_true)
|
|
896
|
+
|
|
897
|
+
if isinstance(h_approx, np.ndarray) and h_approx.ndim == 1:
|
|
898
|
+
h_approx = [h_approx]
|
|
899
|
+
elif isinstance(h_approx, np.ndarray) and h_approx.ndim == 2:
|
|
900
|
+
h_approx = list(h_approx)
|
|
901
|
+
|
|
902
|
+
assert len(h_approx) == len(h_true)
|
|
903
|
+
assert np.all(
|
|
904
|
+
np.asarray([len(h_approx[i]) == len(h_true[i]) for i in range(len(h_true))])
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
# difference in the waveforms
|
|
908
|
+
diff = [h_true[i] - h_approx[i] for i in range(len(h_approx))]
|
|
909
|
+
|
|
910
|
+
# systematic err
|
|
911
|
+
syst_vec = np.array(
|
|
912
|
+
[
|
|
913
|
+
inner_product(
|
|
914
|
+
dh[k],
|
|
915
|
+
diff,
|
|
916
|
+
**inner_product_kwargs,
|
|
917
|
+
)
|
|
918
|
+
for k in range(len(deriv_inds))
|
|
919
|
+
]
|
|
920
|
+
)
|
|
921
|
+
|
|
922
|
+
bias = np.dot(cov, syst_vec)
|
|
923
|
+
|
|
924
|
+
# return list
|
|
925
|
+
returns = [syst_vec, bias]
|
|
926
|
+
|
|
927
|
+
# add anything requested
|
|
928
|
+
if return_cov:
|
|
929
|
+
returns.append(cov)
|
|
930
|
+
|
|
931
|
+
if return_derivs:
|
|
932
|
+
returns.append(dh)
|
|
933
|
+
|
|
934
|
+
return returns
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def scale_to_snr(
|
|
938
|
+
target_snr: float,
|
|
939
|
+
sig: np.ndarray | list,
|
|
940
|
+
*snr_args: tuple,
|
|
941
|
+
return_orig_snr: Optional[bool] = False,
|
|
942
|
+
**snr_kwargs: dict,
|
|
943
|
+
) -> np.ndarray | list | Tuple[np.ndarray | list, float]:
|
|
944
|
+
"""Calculate the SNR and scale a signal.
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
target_snr: Desired SNR value for the injected signal.
|
|
948
|
+
sig: Signal to adjust. A copy will be made.
|
|
949
|
+
Can be time-domain or frequency-domain.
|
|
950
|
+
Must be 1D ``np.ndarray``, list of 1D ``np.ndarray``s, or 2D ``np.ndarray``
|
|
951
|
+
across channels with shape ``(nchannels, data length)``.
|
|
952
|
+
*snr_args: Arguments to pass to :func:`snr`.
|
|
953
|
+
return_orig_snr: If ``True``, return the original SNR in addition to the adjusted data.
|
|
954
|
+
**snr_kwargs: Keyword arguments to pass to :func:`snr`.
|
|
955
|
+
|
|
956
|
+
Returns:
|
|
957
|
+
Returns the copied input signal adjusted to the target SNR. If ``return_orig_snr is True``, the original
|
|
958
|
+
SNR is added as the second entry of a tuple with the adjusted signal (as the first entry in the tuple).
|
|
959
|
+
|
|
960
|
+
"""
|
|
961
|
+
# get the snr and adjustment factor
|
|
962
|
+
snr_out = snr(sig, *snr_args, **snr_kwargs)
|
|
963
|
+
factor = target_snr / snr_out
|
|
964
|
+
|
|
965
|
+
# any changes back to the original signal type
|
|
966
|
+
back_single = False
|
|
967
|
+
back_2d_array = False
|
|
968
|
+
|
|
969
|
+
if isinstance(sig, list) is False and sig.ndim == 1:
|
|
970
|
+
sig = [sig]
|
|
971
|
+
back_single = True
|
|
972
|
+
|
|
973
|
+
elif isinstance(sig, list) is False and sig.ndim == 2:
|
|
974
|
+
sig = list(sig)
|
|
975
|
+
back_2d_array = True
|
|
976
|
+
|
|
977
|
+
# adjust
|
|
978
|
+
out = [sig_i * factor for sig_i in sig]
|
|
979
|
+
|
|
980
|
+
# adjust type back to the input type
|
|
981
|
+
if back_2d_array:
|
|
982
|
+
out = np.asarray(out)
|
|
983
|
+
|
|
984
|
+
if back_single:
|
|
985
|
+
out = out[0]
|
|
986
|
+
|
|
987
|
+
if return_orig_snr:
|
|
988
|
+
return (out, snr_out)
|
|
989
|
+
|
|
990
|
+
return out
|