lisaanalysistools 1.0.0__cp312-cp312-macosx_10_9_x86_64.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.

Potentially problematic release.


This version of lisaanalysistools might be problematic. Click here for more details.

Files changed (37) hide show
  1. lisaanalysistools-1.0.0.dist-info/LICENSE +201 -0
  2. lisaanalysistools-1.0.0.dist-info/METADATA +80 -0
  3. lisaanalysistools-1.0.0.dist-info/RECORD +37 -0
  4. lisaanalysistools-1.0.0.dist-info/WHEEL +5 -0
  5. lisaanalysistools-1.0.0.dist-info/top_level.txt +2 -0
  6. lisatools/__init__.py +0 -0
  7. lisatools/_version.py +4 -0
  8. lisatools/analysiscontainer.py +438 -0
  9. lisatools/cutils/detector.cpython-312-darwin.so +0 -0
  10. lisatools/datacontainer.py +292 -0
  11. lisatools/detector.py +410 -0
  12. lisatools/diagnostic.py +976 -0
  13. lisatools/glitch.py +193 -0
  14. lisatools/sampling/__init__.py +0 -0
  15. lisatools/sampling/likelihood.py +882 -0
  16. lisatools/sampling/moves/__init__.py +0 -0
  17. lisatools/sampling/moves/gbgroupstretch.py +53 -0
  18. lisatools/sampling/moves/gbmultipletryrj.py +1287 -0
  19. lisatools/sampling/moves/gbspecialgroupstretch.py +671 -0
  20. lisatools/sampling/moves/gbspecialstretch.py +1836 -0
  21. lisatools/sampling/moves/mbhspecialmove.py +286 -0
  22. lisatools/sampling/moves/placeholder.py +16 -0
  23. lisatools/sampling/moves/skymodehop.py +110 -0
  24. lisatools/sampling/moves/specialforegroundmove.py +564 -0
  25. lisatools/sampling/prior.py +508 -0
  26. lisatools/sampling/stopping.py +320 -0
  27. lisatools/sampling/utility.py +324 -0
  28. lisatools/sensitivity.py +888 -0
  29. lisatools/sources/__init__.py +0 -0
  30. lisatools/sources/emri/__init__.py +1 -0
  31. lisatools/sources/emri/tdiwaveform.py +72 -0
  32. lisatools/stochastic.py +291 -0
  33. lisatools/utils/__init__.py +0 -0
  34. lisatools/utils/constants.py +40 -0
  35. lisatools/utils/multigpudataholder.py +730 -0
  36. lisatools/utils/pointeradjust.py +106 -0
  37. lisatools/utils/utility.py +240 -0
@@ -0,0 +1,888 @@
1
+ import warnings
2
+ from abc import ABC
3
+ from typing import Any, Tuple, Optional, List
4
+ from copy import deepcopy
5
+
6
+ import math
7
+ import numpy as np
8
+ from numpy.typing import ArrayLike
9
+ from scipy import interpolate
10
+ import matplotlib.pyplot as plt
11
+
12
+ try:
13
+ import cupy as cp
14
+
15
+ except (ModuleNotFoundError, ImportError):
16
+ import numpy as cp
17
+
18
+ from . import detector as lisa_models
19
+ from .utils.utility import AET
20
+ from .utils.constants import *
21
+ from .stochastic import (
22
+ StochasticContribution,
23
+ FittedHyperbolicTangentGalacticForeground,
24
+ )
25
+
26
+ """
27
+ The sensitivity code is heavily based on an original code by Stas Babak, Antoine Petiteau for the LDC team.
28
+
29
+ """
30
+
31
+
32
+ class Sensitivity(ABC):
33
+ """Base Class for PSD information."""
34
+
35
+ @staticmethod
36
+ def transform(
37
+ f: float | np.ndarray,
38
+ Spm: float | np.ndarray,
39
+ Sop: float | np.ndarray,
40
+ **kwargs: dict,
41
+ ) -> float | np.ndarray:
42
+ """Transform from the base sensitivity functions to the TDI PSDs.
43
+
44
+ Args:
45
+ f: Frequency array.
46
+ Spm: Acceleration term.
47
+ Sop: OMS term.
48
+ **kwargs: For interoperability.
49
+
50
+ Returns:
51
+ Transformed TDI PSD values.
52
+
53
+ """
54
+ raise NotImplementedError
55
+
56
+ @staticmethod
57
+ def lisanoises(
58
+ f: float | np.ndarray,
59
+ model: Optional[lisa_models.LISAModel | str] = lisa_models.scirdv1,
60
+ unit: Optional[str] = "relative_frequency",
61
+ ) -> Tuple[float, float]:
62
+ """Calculate both LISA noise terms based on input model.
63
+
64
+ Args:
65
+ f: Frequency array.
66
+ model: Noise model. Object of type :class:`lisa_models.LISAModel`. It can also be a string corresponding to one of the stock models.
67
+ unit: Either ``"relative_frequency"`` or ``"displacement"``.
68
+
69
+ Returns:
70
+ Tuple with acceleration term as first value and oms term as second value.
71
+
72
+ """
73
+
74
+ if isinstance(model, str):
75
+ model = lisa_models.check_lisa_model(model)
76
+
77
+ # TODO: fix this up
78
+ Soms_d_in = model.Soms_d
79
+ Sa_a_in = model.Sa_a
80
+
81
+ frq = f
82
+ ### Acceleration noise
83
+ ## In acceleration
84
+ Sa_a = Sa_a_in * (1.0 + (0.4e-3 / frq) ** 2) * (1.0 + (frq / 8e-3) ** 4)
85
+ ## In displacement
86
+ Sa_d = Sa_a * (2.0 * np.pi * frq) ** (-4.0)
87
+ ## In relative frequency unit
88
+ Sa_nu = Sa_d * (2.0 * np.pi * frq / C_SI) ** 2
89
+ Spm = Sa_nu
90
+
91
+ ### Optical Metrology System
92
+ ## In displacement
93
+ Soms_d = Soms_d_in * (1.0 + (2.0e-3 / f) ** 4)
94
+ ## In relative frequency unit
95
+ Soms_nu = Soms_d * (2.0 * np.pi * frq / C_SI) ** 2
96
+ Sop = Soms_nu
97
+
98
+ if unit == "displacement":
99
+ return Sa_d, Soms_d
100
+ elif unit == "relative_frequency":
101
+ return Spm, Sop
102
+
103
+ @classmethod
104
+ def get_Sn(
105
+ cls,
106
+ f: float | np.ndarray,
107
+ model: Optional[lisa_models.LISAModel | str] = lisa_models.scirdv1,
108
+ **kwargs: dict,
109
+ ) -> float | np.ndarray:
110
+ """Calculate the PSD
111
+
112
+ Args:
113
+ f: Frequency array.
114
+ model: Noise model. Object of type :class:`lisa_models.LISAModel`. It can also be a string corresponding to one of the stock models.
115
+ **kwargs: For interoperability.
116
+
117
+ Returns:
118
+ PSD values.
119
+
120
+ """
121
+ x = 2.0 * np.pi * lisaLT * f
122
+
123
+ # get noise values
124
+ Spm, Sop = cls.lisanoises(f, model)
125
+
126
+ # transform as desired for TDI combination
127
+ Sout = cls.transform(f, Spm, Sop, **kwargs)
128
+
129
+ # will add zero if ignored
130
+ Sout += cls.get_stochastic_contribution(f, **kwargs)
131
+ return Sout
132
+
133
+ @classmethod
134
+ def get_stochastic_contribution(
135
+ cls,
136
+ f: float | np.ndarray,
137
+ stochastic_params: Optional[tuple] = (),
138
+ stochastic_kwargs: Optional[dict] = {},
139
+ stochastic_function: Optional[StochasticContribution | str] = None,
140
+ ) -> float | np.ndarray:
141
+ """Calculate contribution from stochastic signal.
142
+
143
+ This function directs and wraps the calculation of and returns
144
+ the stochastic signal. The ``stochastic_function`` calculates the
145
+ sensitivity contribution. The ``transform_factor`` can transform that
146
+ output to the correct TDI contribution.
147
+
148
+ Args:
149
+ f: Frequency array.
150
+ stochastic_params: Parameters (arguments) to feed to ``stochastic_function``.
151
+ stochastic_kwargs: Keyword arguments to feeed to ``stochastic_function``.
152
+ stochastic_function: Stochastic class or string name of stochastic class. Takes ``stochastic_args`` and ``stochastic_kwargs``.
153
+ If ``None``, it uses :class:`FittedHyperbolicTangentGalacticForeground`.
154
+
155
+ Returns:
156
+ Contribution from stochastic signal.
157
+
158
+
159
+ """
160
+
161
+ if isinstance(f, float):
162
+ f = np.ndarray([f])
163
+ squeeze = True
164
+ else:
165
+ squeeze = False
166
+
167
+ sgal = np.zeros_like(f)
168
+
169
+ if (
170
+ (stochastic_params != () and stochastic_params is not None)
171
+ or (stochastic_kwargs != {} and stochastic_kwargs is not None)
172
+ or stochastic_function is not None
173
+ ):
174
+ if stochastic_function is None:
175
+ stochastic_function = FittedHyperbolicTangentGalacticForeground
176
+
177
+ try:
178
+ check = stochastic_function.get_Sh(
179
+ f, *stochastic_params, **stochastic_kwargs
180
+ )
181
+ except:
182
+ breakpoint()
183
+ sgal[:] = check
184
+
185
+ if squeeze:
186
+ sgal = sgal.squeeze()
187
+ return sgal
188
+
189
+ @staticmethod
190
+ def stochastic_transform(
191
+ f: float | np.ndarray, Sh: float | np.ndarray, **kwargs: dict
192
+ ) -> float | np.ndarray:
193
+ """Transform from the base stochastic functions to the TDI PSDs.
194
+
195
+ **Note**: If not implemented, the transform will return the input.
196
+
197
+ Args:
198
+ f: Frequency array.
199
+ Sh: Power spectral density in stochastic term.
200
+ **kwargs: For interoperability.
201
+
202
+ Returns:
203
+ Transformed TDI PSD values.
204
+
205
+ """
206
+ return Sh
207
+
208
+
209
+ class X1TDISens(Sensitivity):
210
+ @staticmethod
211
+ def transform(
212
+ f: float | np.ndarray,
213
+ Spm: float | np.ndarray,
214
+ Sop: float | np.ndarray,
215
+ **kwargs: dict,
216
+ ) -> float | np.ndarray:
217
+ __doc__ = (
218
+ "Transform from the base sensitivity functions to the XYZ TDI PSDs.\n\n"
219
+ + Sensitivity.transform.__doc__.split("PSDs.\n\n")[-1]
220
+ )
221
+
222
+ x = 2.0 * np.pi * lisaLT * f
223
+ return 16.0 * np.sin(x) ** 2 * (2.0 * (1.0 + np.cos(x) ** 2) * Spm + Sop)
224
+
225
+ @staticmethod
226
+ def stochastic_transform(
227
+ f: float | np.ndarray, Sh: float | np.ndarray, **kwargs: dict
228
+ ) -> float | np.ndarray:
229
+ __doc__ = (
230
+ "Transform from the base stochastic functions to the XYZ stochastic TDI information.\n\n"
231
+ + Sensitivity.stochastic_transform.__doc__.split("PSDs.\n\n")[-1]
232
+ )
233
+ x = 2.0 * np.pi * lisaLT * f
234
+ t = 4.0 * x**2 * np.sin(x) ** 2
235
+ return Sh * t
236
+
237
+
238
+ class Y1TDISens(X1TDISens):
239
+ __doc__ = X1TDISens.__doc__
240
+ pass
241
+
242
+
243
+ class Z1TDISens(X1TDISens):
244
+ __doc__ = X1TDISens.__doc__
245
+ pass
246
+
247
+
248
+ class XY1TDISens(Sensitivity):
249
+ @staticmethod
250
+ def transform(
251
+ f: float | np.ndarray,
252
+ Spm: float | np.ndarray,
253
+ Sop: float | np.ndarray,
254
+ **kwargs: dict,
255
+ ) -> float | np.ndarray:
256
+ __doc__ = (
257
+ "Transform from the base sensitivity functions to the XYZ TDI PSDs.\n\n"
258
+ + Sensitivity.transform.__doc__.split("PSDs.\n\n")[-1]
259
+ )
260
+
261
+ x = 2.0 * np.pi * lisaLT * f
262
+ ## TODO Check the acceleration noise term
263
+ return -4.0 * np.sin(2 * x) * np.sin(x) * (Sop + 4.0 * Spm)
264
+
265
+ @staticmethod
266
+ def stochastic_transform(
267
+ f: float | np.ndarray, Sh: float | np.ndarray, **kwargs: dict
268
+ ) -> float | np.ndarray:
269
+ __doc__ = (
270
+ "Transform from the base stochastic functions to the XYZ stochastic TDI information.\n\n"
271
+ + Sensitivity.stochastic_transform.__doc__.split("PSDs.\n\n")[-1]
272
+ )
273
+ x = 2.0 * np.pi * lisaLT * f
274
+ # TODO: check these functions
275
+ # GB = -0.5 of X
276
+ t = -0.5 * (4.0 * x**2 * np.sin(x) ** 2)
277
+ return Sh * t
278
+
279
+
280
+ class ZX1TDISens(XY1TDISens):
281
+ __doc__ = XY1TDISens.__doc__
282
+ pass
283
+
284
+
285
+ class YZ1TDISens(XY1TDISens):
286
+ __doc__ = XY1TDISens.__doc__
287
+ pass
288
+
289
+
290
+ class X2TDISens(Sensitivity):
291
+ @staticmethod
292
+ def transform(
293
+ f: float | np.ndarray,
294
+ Spm: float | np.ndarray,
295
+ Sop: float | np.ndarray,
296
+ **kwargs: dict,
297
+ ) -> float | np.ndarray:
298
+ __doc__ = (
299
+ "Transform from the base sensitivity functions to the XYZ TDI PSDs.\n\n"
300
+ + Sensitivity.transform.__doc__.split("PSDs.\n\n")[-1]
301
+ )
302
+
303
+ x = 2.0 * np.pi * lisaLT * f
304
+ ## TODO Check the acceleration noise term
305
+ return (64.0 * np.sin(x) ** 2 * np.sin(2 * x) ** 2 * Sop) + (
306
+ 256.0 * (3 + np.cos(2 * x)) * np.cos(x) ** 2 * np.sin(x) ** 4 * Spm
307
+ )
308
+
309
+ @staticmethod
310
+ def stochastic_transform(
311
+ f: float | np.ndarray, Sh: float | np.ndarray, **kwargs: dict
312
+ ) -> float | np.ndarray:
313
+ __doc__ = (
314
+ "Transform from the base stochastic functions to the XYZ stochastic TDI information.\n\n"
315
+ + Sensitivity.stochastic_transform.__doc__.split("PSDs.\n\n")[-1]
316
+ )
317
+ x = 2.0 * np.pi * lisaLT * f
318
+ # TODO: check these functions for TDI2
319
+ t = 4.0 * x**2 * np.sin(x) ** 2
320
+ return Sh * t
321
+
322
+
323
+ class Y2TDISens(X2TDISens):
324
+ __doc__ = X2TDISens.__doc__
325
+ pass
326
+
327
+
328
+ class Z2TDISens(X2TDISens):
329
+ __doc__ = X2TDISens.__doc__
330
+ pass
331
+
332
+
333
+ class A1TDISens(Sensitivity):
334
+ @staticmethod
335
+ def transform(
336
+ f: float | np.ndarray,
337
+ Spm: float | np.ndarray,
338
+ Sop: float | np.ndarray,
339
+ **kwargs: dict,
340
+ ) -> float | np.ndarray:
341
+ __doc__ = (
342
+ "Transform from the base sensitivity functions to the A,E TDI PSDs.\n\n"
343
+ + Sensitivity.transform.__doc__.split("PSDs.\n\n")[-1]
344
+ )
345
+
346
+ x = 2.0 * np.pi * lisaLT * f
347
+ return (
348
+ 8.0
349
+ * np.sin(x) ** 2
350
+ * (
351
+ 2.0 * Spm * (3.0 + 2.0 * np.cos(x) + np.cos(2 * x))
352
+ + Sop * (2.0 + np.cos(x))
353
+ )
354
+ )
355
+
356
+ @staticmethod
357
+ def stochastic_transform(
358
+ f: float | np.ndarray, Sh: float | np.ndarray, **kwargs: dict
359
+ ) -> float | np.ndarray:
360
+ __doc__ = (
361
+ "Transform from the base stochastic functions to the XYZ stochastic TDI information.\n\n"
362
+ + Sensitivity.stochastic_transform.__doc__.split("PSDs.\n\n")[-1]
363
+ )
364
+ x = 2.0 * np.pi * lisaLT * f
365
+ t = 4.0 * x**2 * np.sin(x) ** 2
366
+ return 1.5 * (Sh * t)
367
+
368
+
369
+ class E1TDISens(A1TDISens):
370
+ __doc__ = A1TDISens.__doc__
371
+ pass
372
+
373
+
374
+ class T1TDISens(Sensitivity):
375
+ @staticmethod
376
+ def transform(
377
+ f: float | np.ndarray,
378
+ Spm: float | np.ndarray,
379
+ Sop: float | np.ndarray,
380
+ **kwargs: dict,
381
+ ) -> float | np.ndarray:
382
+ __doc__ = (
383
+ "Transform from the base sensitivity functions to the T TDI PSDs.\n\n"
384
+ + Sensitivity.transform.__doc__.split("PSDs.\n\n")[-1]
385
+ )
386
+
387
+ x = 2.0 * np.pi * lisaLT * f
388
+ return (
389
+ 16.0 * Sop * (1.0 - np.cos(x)) * np.sin(x) ** 2
390
+ + 128.0 * Spm * np.sin(x) ** 2 * np.sin(0.5 * x) ** 4
391
+ )
392
+
393
+ @staticmethod
394
+ def stochastic_transform(
395
+ f: float | np.ndarray, Sh: float | np.ndarray, **kwargs: dict
396
+ ) -> float | np.ndarray:
397
+ __doc__ = (
398
+ "Transform from the base stochastic functions to the XYZ stochastic TDI information.\n\n"
399
+ + Sensitivity.stochastic_transform.__doc__.split("PSDs.\n\n")[-1]
400
+ )
401
+ x = 2.0 * np.pi * lisaLT * f
402
+ t = 4.0 * x**2 * np.sin(x) ** 2
403
+ return 0.0 * (Sh * t)
404
+
405
+
406
+ class LISASens(Sensitivity):
407
+ @classmethod
408
+ def get_Sn(
409
+ cls,
410
+ f: float | np.ndarray,
411
+ model: Optional[lisa_models.LISAModel | str] = lisa_models.scirdv1,
412
+ average: bool = True,
413
+ **kwargs: dict,
414
+ ) -> float | np.ndarray:
415
+ """Compute the base LISA sensitivity function.
416
+
417
+ Args:
418
+ f: Frequency array.
419
+ model: Noise model. Object of type :class:`lisa_models.LISAModel`. It can also be a string corresponding to one of the stock models.
420
+ average: Whether to apply averaging factors to sensitivity curve.
421
+ Antenna response: ``av_resp = np.sqrt(5) if average else 1.0``
422
+ Projection effect: ``Proj = 2.0 / np.sqrt(3) if average else 1.0``
423
+ **kwargs: Keyword arguments to pass to :func:`get_stochastic_contribution`. # TODO: fix
424
+
425
+ Returns:
426
+ Sensitivity array.
427
+
428
+ """
429
+
430
+ # get noise values
431
+ Sa_d, Sop = cls.lisanoises(f, model, unit="displacement")
432
+
433
+ all_m = np.sqrt(4.0 * Sa_d + Sop)
434
+ ## Average the antenna response
435
+ av_resp = np.sqrt(5) if average else 1.0
436
+
437
+ ## Projection effect
438
+ Proj = 2.0 / np.sqrt(3) if average else 1.0
439
+
440
+ ## Approximative transfer function
441
+ f0 = 1.0 / (2.0 * lisaLT)
442
+ a = 0.41
443
+ T = np.sqrt(1 + (f / (a * f0)) ** 2)
444
+ sens = (av_resp * Proj * T * all_m / lisaL) ** 2
445
+
446
+ # will add zero if ignored
447
+ sens += cls.get_stochastic_contribution(f, **kwargs)
448
+ return sens
449
+
450
+
451
+ class CornishLISASens(LISASens):
452
+ """PSD from https://arxiv.org/pdf/1803.01944.pdf
453
+
454
+ Power Spectral Density for the LISA detector assuming it has been active for a year.
455
+ I found an analytic version in one of Niel Cornish's paper which he submitted to the arXiv in
456
+ 2018. I evaluate the PSD at the frequency bins found in the signal FFT.
457
+
458
+ PSD obtained from: https://arxiv.org/pdf/1803.01944.pdf
459
+
460
+ """
461
+
462
+ @staticmethod
463
+ def get_Sn(
464
+ f: float | np.ndarray, average: bool = True, **kwargs: dict
465
+ ) -> float | np.ndarray:
466
+ # TODO: documentation here
467
+
468
+ sky_averaging_constant = 20.0 / 3.0 if average else 1.0
469
+
470
+ L = 2.5 * 10**9 # Length of LISA arm
471
+ f0 = 19.09 * 10 ** (-3) # transfer frequency
472
+
473
+ # Optical Metrology Sensor
474
+ Poms = ((1.5e-11) * (1.5e-11)) * (1 + np.power((2e-3) / f, 4))
475
+
476
+ # Acceleration Noise
477
+ Pacc = (
478
+ (3e-15)
479
+ * (3e-15)
480
+ * (1 + (4e-4 / f) * (4e-4 / f))
481
+ * (1 + np.power(f / (8e-3), 4))
482
+ )
483
+
484
+ # constants for Galactic background after 1 year of observation
485
+ alpha = 0.171
486
+ beta = 292
487
+ k = 1020
488
+ gamma = 1680
489
+ f_k = 0.00215
490
+
491
+ # Galactic background contribution
492
+ Sc = (
493
+ 9e-45
494
+ * np.power(f, -7 / 3)
495
+ * np.exp(-np.power(f, alpha) + beta * f * np.sin(k * f))
496
+ * (1 + np.tanh(gamma * (f_k - f)))
497
+ )
498
+
499
+ # PSD
500
+ PSD = (sky_averaging_constant) * (
501
+ (10 / (3 * L * L))
502
+ * (Poms + (4 * Pacc) / (np.power(2 * np.pi * f, 4)))
503
+ * (1 + 0.6 * (f / f0) * (f / f0))
504
+ + Sc
505
+ )
506
+
507
+ return PSD
508
+
509
+
510
+ class FlatPSDFunction(LISASens):
511
+ """White Noise PSD function."""
512
+
513
+ @staticmethod
514
+ def get_Sn(f: float | np.ndarray, val: float, **kwargs: dict) -> float | np.ndarray:
515
+ # TODO: documentation here
516
+ out = np.full_like(f, val)
517
+ if isinstance(f, float):
518
+ out = out.item()
519
+ return out
520
+
521
+
522
+ class SensitivityMatrix:
523
+ """Container to hold sensitivity information.
524
+
525
+ Args:
526
+ f: Frequency array.
527
+ sens_mat: Input sensitivity list. The shape of the nested lists should represent the shape of the
528
+ desired matrix. Each entry in the list must be an array, :class:`Sensitivity`-derived object,
529
+ or a string corresponding to the :class:`Sensitivity` object.
530
+ **sens_kwargs: Keyword arguments to pass to :method:`Sensitivity.get_Sn`.
531
+
532
+ """
533
+
534
+ def __init__(
535
+ self,
536
+ f: np.ndarray,
537
+ sens_mat: List[List[np.ndarray | Sensitivity]]
538
+ | List[np.ndarray | Sensitivity]
539
+ | np.ndarray
540
+ | Sensitivity,
541
+ *sens_args: tuple,
542
+ **sens_kwargs: dict,
543
+ ) -> None:
544
+ self.frequency_arr = f
545
+ self.data_length = len(self.frequency_arr)
546
+ self.sens_args = sens_args
547
+ self.sens_kwargs = sens_kwargs
548
+ self.sens_mat = sens_mat
549
+
550
+ @property
551
+ def frequency_arr(self) -> np.ndarray:
552
+ return self._frequency_arr
553
+
554
+ @frequency_arr.setter
555
+ def frequency_arr(self, frequency_arr: np.ndarray) -> None:
556
+ assert frequency_arr.dtype == np.float64 or frequency_arr.dtype == float
557
+ assert frequency_arr.ndim == 1
558
+ self._frequency_arr = frequency_arr
559
+
560
+ def update_frequency_arr(self, frequency_arr: np.ndarray) -> None:
561
+ """Update class with new frequency array.
562
+
563
+ Args:
564
+ frequency_arr: Frequency array.
565
+
566
+ """
567
+ self.frequency_arr = frequency_arr
568
+ self.sens_mat = self.sens_mat_input
569
+
570
+ def update_model(self, model: lisa_models.LISAModel) -> None:
571
+ """Update class with new sensitivity model.
572
+
573
+ Args:
574
+ model: Noise model. Object of type :class:`lisa_models.LISAModel`. It can also be a string corresponding to one of the stock models.
575
+
576
+ """
577
+ self.sens_kwargs["model"] = model
578
+ self.sens_mat = self.sens_mat_input
579
+
580
+ def update_stochastic(self, **kwargs: dict) -> None:
581
+ """Update class with new stochastic function.
582
+
583
+ Args:
584
+ **kwargs: Keyword arguments update for :func:`lisatools.sensitivity.Sensitivity.get_stochastic_contribution`.
585
+ This operation will combine the new and old kwarg dictionaries, updating any
586
+ old information with any added corresponding new information. **Note**: any old information
587
+ that is not updated will remain in place.
588
+
589
+ """
590
+ self.sens_kwargs = {**self.sens_kwargs, **kwargs}
591
+ self.sens_mat = self.sens_mat_input
592
+
593
+ @property
594
+ def sens_mat(self) -> np.ndarray:
595
+ """Get sensitivity matrix."""
596
+ return self._sens_mat
597
+
598
+ @sens_mat.setter
599
+ def sens_mat(
600
+ self,
601
+ sens_mat: List[List[np.ndarray | Sensitivity]]
602
+ | List[np.ndarray | Sensitivity]
603
+ | np.ndarray
604
+ | Sensitivity,
605
+ ) -> None:
606
+ """Set sensitivity matrix."""
607
+ self.sens_mat_input = deepcopy(sens_mat)
608
+ self._sens_mat = np.asarray(sens_mat, dtype=object)
609
+
610
+ # not an
611
+ new_out = np.full(len(self._sens_mat.flatten()), None, dtype=object)
612
+ self.return_shape = self._sens_mat.shape
613
+ for i in range(len(self._sens_mat.flatten())):
614
+ current_sens = self._sens_mat.flatten()[i]
615
+ if hasattr(current_sens, "get_Sn") or isinstance(current_sens, str):
616
+ new_out[i] = get_sensitivity(
617
+ self.frequency_arr,
618
+ *self.sens_args,
619
+ sens_fn=current_sens,
620
+ **self.sens_kwargs,
621
+ )
622
+
623
+ elif isinstance(current_sens, np.ndarray) or isinstance(
624
+ current_sens, cp.ndarray
625
+ ):
626
+ new_out[i] = current_sens
627
+ else:
628
+ raise ValueError
629
+
630
+ # setup in array form
631
+ self._sens_mat = np.asarray(list(new_out), dtype=float).reshape(
632
+ self.return_shape + (-1,)
633
+ )
634
+
635
+ def __getitem__(self, index: Any) -> np.ndarray:
636
+ """Indexing the class indexes the array."""
637
+ return self.sens_mat[index]
638
+
639
+ @property
640
+ def ndim(self) -> int:
641
+ """Dimensionality of sens mat array."""
642
+ return self.sens_mat.ndim
643
+
644
+ def flatten(self) -> np.ndarray:
645
+ """Flatten sens mat array."""
646
+ return self.sens_mat.reshape(-1, self.sens_mat.shape[-1])
647
+
648
+ @property
649
+ def shape(self) -> tuple:
650
+ """Shape of sens mat array."""
651
+ return self.sens_mat.shape
652
+
653
+ def loglog(
654
+ self,
655
+ ax: Optional[plt.Axes] = None,
656
+ fig: Optional[plt.Figure] = None,
657
+ inds: Optional[int | tuple] = None,
658
+ char_strain: Optional[bool] = False,
659
+ **kwargs: dict,
660
+ ) -> Tuple[plt.Figure, plt.Axes]:
661
+ """Produce a log-log plot of the sensitivity.
662
+
663
+ Args:
664
+ ax: Matplotlib Axes objects to add plots. Either a list of Axes objects or a single Axes object.
665
+ fig: Matplotlib figure object.
666
+ inds: Integer index to select out which data to add to a single access.
667
+ A list can be provided if ax is a list. They must be the same length.
668
+ char_strain: If ``True``, plot in characteristic strain representation. **Note**: assumes the sensitivity
669
+ is input as power spectral density.
670
+ **kwargs: Keyword arguments to be passed to ``loglog`` function in matplotlib.
671
+
672
+ Returns:
673
+ Matplotlib figure and axes objects in a 2-tuple.
674
+
675
+
676
+ """
677
+ if (ax is None and fig is None) or (
678
+ ax is not None and (isinstance(ax, list) or isinstance(ax, np.ndarray))
679
+ ):
680
+ if ax is None and fig is None:
681
+ outer_shape = self.shape[:-1]
682
+ if len(outer_shape) == 2:
683
+ nrows = outer_shape[0]
684
+ ncols = outer_shape[1]
685
+ elif len(outer_shape) == 1:
686
+ nrows = 1
687
+ ncols = outer_shape[0]
688
+
689
+ fig, ax = plt.subplots(nrows, ncols, sharex=True, sharey=True)
690
+ try:
691
+ ax = ax.ravel()
692
+ except AttributeError:
693
+ ax = [ax] # just one axis object, no list
694
+
695
+ else:
696
+ assert len(ax) == np.prod(self.shape[:-1])
697
+
698
+ for i in range(np.prod(self.shape[:-1])):
699
+ plot_in = self.flatten()[i]
700
+ if char_strain:
701
+ plot_in = np.sqrt(self.frequency_arr * plot_in)
702
+ ax[i].loglog(self.frequency_arr, plot_in, **kwargs)
703
+
704
+ elif fig is not None:
705
+ raise NotImplementedError
706
+
707
+ elif isinstance(ax, plt.axes):
708
+ if inds is None:
709
+ raise ValueError(
710
+ "When passing a single axes object for `ax`, but also pass `inds` kwarg."
711
+ )
712
+ plot_in = self.sens_mat[inds]
713
+ if char_strain:
714
+ plot_in = np.sqrt(self.frequency_arr * plot_in)
715
+ ax.loglog(self.frequency_arr, plot_in, **kwargs)
716
+
717
+ else:
718
+ raise ValueError(
719
+ "ax must be a list of axes objects or a single axes object."
720
+ )
721
+
722
+ return (fig, ax)
723
+
724
+
725
+ class XYZ1SensitivityMatrix(SensitivityMatrix):
726
+ """Default sensitivity matrix for XYZ (TDI 1)
727
+
728
+ This is 3x3 symmetric matrix.
729
+
730
+ Args:
731
+ f: Frequency array.
732
+ **sens_kwargs: Keyword arguments to pass to :method:`Sensitivity.get_Sn`.
733
+
734
+ """
735
+
736
+ def __init__(self, f: np.ndarray, **sens_kwargs: dict) -> None:
737
+ sens_mat = [
738
+ [X1TDISens, XY1TDISens, ZX1TDISens],
739
+ [XY1TDISens, Y1TDISens, YZ1TDISens],
740
+ [ZX1TDISens, YZ1TDISens, Z1TDISens],
741
+ ]
742
+ super().__init__(f, sens_mat, **sens_kwargs)
743
+
744
+
745
+ class AET1SensitivityMatrix(SensitivityMatrix):
746
+ """Default sensitivity matrix for AET (TDI 1)
747
+
748
+ This is just an array because no cross-terms.
749
+
750
+ Args:
751
+ f: Frequency array.
752
+ **sens_kwargs: Keyword arguments to pass to :method:`Sensitivity.get_Sn`.
753
+
754
+ """
755
+
756
+ def __init__(self, f: np.ndarray, **sens_kwargs: dict) -> None:
757
+ sens_mat = [A1TDISens, E1TDISens, T1TDISens]
758
+ super().__init__(f, sens_mat, **sens_kwargs)
759
+
760
+
761
+ class AE1SensitivityMatrix(SensitivityMatrix):
762
+ """Default sensitivity matrix for AE (no T) (TDI 1)
763
+
764
+ Args:
765
+ f: Frequency array.
766
+ **sens_kwargs: Keyword arguments to pass to :method:`Sensitivity.get_Sn`.
767
+
768
+ """
769
+
770
+ def __init__(self, f: np.ndarray, **sens_kwargs: dict) -> None:
771
+ sens_mat = [A1TDISens, E1TDISens]
772
+ super().__init__(f, sens_mat, **sens_kwargs)
773
+
774
+
775
+ class LISASensSensitivityMatrix(SensitivityMatrix):
776
+ """Default sensitivity matrix adding :class:`LISASens` for the specified number of channels.
777
+
778
+ Args:
779
+ f: Frequency array.
780
+ nchannels: Number of channels.
781
+ **sens_kwargs: Keyword arguments to pass to :method:`Sensitivity.get_Sn`.
782
+
783
+ """
784
+
785
+ def __init__(self, f: np.ndarray, nchannels: int, **sens_kwargs: dict) -> None:
786
+ sens_mat = [LISASens for _ in range(nchannels)]
787
+ super().__init__(f, sens_mat, **sens_kwargs)
788
+
789
+
790
+ def get_sensitivity(
791
+ f: float | np.ndarray,
792
+ *args: tuple,
793
+ sens_fn: Optional[Sensitivity | str] = LISASens,
794
+ return_type="PSD",
795
+ **kwargs,
796
+ ) -> float | np.ndarray:
797
+ """Generic sensitivity generator
798
+
799
+ Same interface to many sensitivity curves.
800
+
801
+ Args:
802
+ f: Frequency array.
803
+ *args: Any additional arguments for the sensitivity function ``get_Sn`` method.
804
+ sens_fn: String or class that represents the name of the desired PSD function.
805
+ return_type: Described the desired output. Choices are ASD,
806
+ PSD, or char_strain (characteristic strain). Default is ASD.
807
+ **kwargs: Keyword arguments to pass to sensitivity function ``get_Sn`` method.
808
+
809
+ Return:
810
+ Sensitivity values.
811
+
812
+ """
813
+
814
+ if isinstance(sens_fn, str):
815
+ try:
816
+ sensitivity = globals()[sens_fn]
817
+ except KeyError:
818
+ raise ValueError(
819
+ f"{sens_fn} sensitivity is not available. Available stock sensitivities are"
820
+ )
821
+
822
+ elif hasattr(sens_fn, "get_Sn"):
823
+ sensitivity = sens_fn
824
+
825
+ else:
826
+ raise ValueError(
827
+ "sens_fn must be a string for a stock option or a class with a get_Sn method."
828
+ )
829
+
830
+ PSD = sensitivity.get_Sn(f, *args, **kwargs)
831
+
832
+ if return_type == "PSD":
833
+ return PSD
834
+
835
+ elif return_type == "ASD":
836
+ return PSD ** (1 / 2)
837
+
838
+ elif return_type == "char_strain":
839
+ return (f * PSD) ** (1 / 2)
840
+
841
+ else:
842
+ raise ValueError("return_type must be PSD, ASD, or char_strain.")
843
+
844
+
845
+ __stock_sens_options__ = [
846
+ "X1TDISens",
847
+ "Y1TDISens",
848
+ "Z1TDISens",
849
+ "XY1TDISens",
850
+ "YZ1TDISens",
851
+ "ZX1TDISens",
852
+ "A1TDISens",
853
+ "E1TDISens",
854
+ "T1TDISens",
855
+ "X2TDISens",
856
+ "Y2TDISens",
857
+ "Z2TDISens",
858
+ "LISASens",
859
+ "CornishLISASens",
860
+ "FlatPSDFunction",
861
+ ]
862
+
863
+
864
+ def get_stock_sensitivity_options() -> List[Sensitivity]:
865
+ """Get stock options for sensitivity curves.
866
+
867
+ Returns:
868
+ List of stock sensitivity options.
869
+
870
+ """
871
+ return __stock_sens_options__
872
+
873
+
874
+ __stock_sensitivity_mat_options__ = [
875
+ "XYZ1SensitivityMatrix",
876
+ "AET1SensitivityMatrix",
877
+ "AE1SensitivityMatrix",
878
+ ]
879
+
880
+
881
+ def get_stock_sensitivity_matrix_options() -> List[SensitivityMatrix]:
882
+ """Get stock options for sensitivity matrix.
883
+
884
+ Returns:
885
+ List of stock sensitivity matrix options.
886
+
887
+ """
888
+ return __stock_sensitivity_mat_options__