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.
Files changed (48) hide show
  1. lisaanalysistools/git_version.py +7 -0
  2. lisaanalysistools-1.1.20.dist-info/METADATA +281 -0
  3. lisaanalysistools-1.1.20.dist-info/RECORD +48 -0
  4. lisaanalysistools-1.1.20.dist-info/WHEEL +5 -0
  5. lisaanalysistools-1.1.20.dist-info/licenses/LICENSE +201 -0
  6. lisatools/.dylibs/libgcc_s.1.1.dylib +0 -0
  7. lisatools/.dylibs/libstdc++.6.dylib +0 -0
  8. lisatools/__init__.py +90 -0
  9. lisatools/_version.py +34 -0
  10. lisatools/analysiscontainer.py +474 -0
  11. lisatools/cutils/Detector.cu +307 -0
  12. lisatools/cutils/Detector.hpp +84 -0
  13. lisatools/cutils/__init__.py +129 -0
  14. lisatools/cutils/global.hpp +28 -0
  15. lisatools/cutils/pycppdetector.pyx +256 -0
  16. lisatools/datacontainer.py +312 -0
  17. lisatools/detector.py +867 -0
  18. lisatools/diagnostic.py +990 -0
  19. lisatools/git_version.py.in +7 -0
  20. lisatools/orbit_files/equalarmlength-orbits-best-fit-to-esa.h5 +0 -0
  21. lisatools/orbit_files/equalarmlength-orbits.h5 +0 -0
  22. lisatools/orbit_files/esa-trailing-orbits.h5 +0 -0
  23. lisatools/sampling/__init__.py +0 -0
  24. lisatools/sampling/likelihood.py +882 -0
  25. lisatools/sampling/moves/__init__.py +0 -0
  26. lisatools/sampling/moves/skymodehop.py +110 -0
  27. lisatools/sampling/prior.py +646 -0
  28. lisatools/sampling/stopping.py +320 -0
  29. lisatools/sampling/utility.py +411 -0
  30. lisatools/sensitivity.py +1554 -0
  31. lisatools/sources/__init__.py +6 -0
  32. lisatools/sources/bbh/__init__.py +1 -0
  33. lisatools/sources/bbh/waveform.py +106 -0
  34. lisatools/sources/defaultresponse.py +37 -0
  35. lisatools/sources/emri/__init__.py +1 -0
  36. lisatools/sources/emri/waveform.py +79 -0
  37. lisatools/sources/gb/__init__.py +1 -0
  38. lisatools/sources/gb/waveform.py +69 -0
  39. lisatools/sources/utils.py +459 -0
  40. lisatools/sources/waveformbase.py +41 -0
  41. lisatools/stochastic.py +327 -0
  42. lisatools/utils/__init__.py +0 -0
  43. lisatools/utils/constants.py +54 -0
  44. lisatools/utils/exceptions.py +95 -0
  45. lisatools/utils/parallelbase.py +11 -0
  46. lisatools/utils/utility.py +122 -0
  47. lisatools_backend_cpu/git_version.py +7 -0
  48. lisatools_backend_cpu/pycppdetector.cpython-39-darwin.so +0 -0
lisatools/detector.py ADDED
@@ -0,0 +1,867 @@
1
+ from __future__ import annotations
2
+ import os
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, List, Tuple, Optional
5
+ from dataclasses import dataclass
6
+ import requests
7
+ from copy import deepcopy
8
+ import h5py
9
+ from scipy import interpolate
10
+
11
+ from .utils.constants import *
12
+ from .utils.utility import get_array_module
13
+
14
+ import numpy as np
15
+
16
+ from .utils.parallelbase import LISAToolsParallelModule
17
+
18
+
19
+ SC = [1, 2, 3]
20
+ LINKS = [12, 23, 31, 13, 32, 21]
21
+
22
+ LINEAR_INTERP_TIMESTEP = 600.00 # sec (0.25 hr)
23
+
24
+
25
+ class Orbits(LISAToolsParallelModule, ABC):
26
+ """LISA Orbit Base Class
27
+
28
+ Args:
29
+ filename: File name. File should be in the style of LISAOrbits
30
+ armlength: Armlength of detector.
31
+ force_backend: If ``gpu`` or ``cuda``, use a gpu.
32
+
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ filename: str,
38
+ armlength: Optional[float] = 2.5e9,
39
+ force_backend: Optional[str] = None,
40
+ **kwargs
41
+ ) -> None:
42
+
43
+ # TODO: should we make it compute armlength.
44
+ self.filename = filename
45
+ self.armlength = armlength
46
+ self._setup()
47
+ self.configured = False
48
+ LISAToolsParallelModule.__init__(self, force_backend=force_backend)
49
+
50
+ @property
51
+ def xp(self):
52
+ """numpy or cupy based on self.use_gpu"""
53
+ return self.backend.xp
54
+
55
+ @property
56
+ def armlength(self) -> float:
57
+ """Armlength parameter."""
58
+ return self._armlength
59
+
60
+ @armlength.setter
61
+ def armlength(self, armlength: float) -> None:
62
+ """armlength setter."""
63
+
64
+ if isinstance(armlength, float):
65
+ # TODO: put error check that it is close
66
+ self._armlength = armlength
67
+
68
+ else:
69
+ raise ValueError("armlength must be float.")
70
+
71
+ @property
72
+ def LINKS(self) -> List[int]:
73
+ """Link order."""
74
+ return LINKS
75
+
76
+ @property
77
+ def SC(self) -> List[int]:
78
+ """Spacecraft order."""
79
+ return SC
80
+
81
+ @property
82
+ def link_space_craft_r(self) -> List[int]:
83
+ """Receiver (first) spacecraft"""
84
+ return [int(str(link_i)[0]) for link_i in self.LINKS]
85
+
86
+ @property
87
+ def link_space_craft_e(self) -> List[int]:
88
+ """Sender (second) spacecraft"""
89
+ return [int(str(link_i)[1]) for link_i in self.LINKS]
90
+
91
+ def _setup(self) -> None:
92
+ """Read in orbital data from file and store."""
93
+ with self.open() as f:
94
+ for key in f.attrs.keys():
95
+ setattr(self, key + "_base", f.attrs[key])
96
+
97
+ @property
98
+ def filename(self) -> str:
99
+ """Orbit file name."""
100
+ return self._filename
101
+
102
+ @filename.setter
103
+ def filename(self, filename: str) -> None:
104
+ """Set file name."""
105
+
106
+ assert isinstance(filename, str)
107
+
108
+ if os.path.exists(filename):
109
+ self._filename = filename
110
+
111
+ else:
112
+ # get path
113
+ path_to_this_file = __file__.split("detector.py")[0]
114
+
115
+ # make sure orbit_files directory exists in the right place
116
+ if not os.path.exists(path_to_this_file + "orbit_files/"):
117
+ os.mkdir(path_to_this_file + "orbit_files/")
118
+ path_to_this_file = path_to_this_file + "orbit_files/"
119
+
120
+ if not os.path.exists(path_to_this_file + filename):
121
+ # download files from github if they are not there
122
+ github_file = f"https://github.com/mikekatz04/LISAanalysistools/blob/main/src/lisatools/orbit_files/{filename}"
123
+ r = requests.get(github_file)
124
+
125
+ # if not success
126
+ if r.status_code != 200:
127
+ raise ValueError(
128
+ f"Cannot find {filename} within default files located at github.com/mikekatz04/LISAanalysistools/lisatools/orbit_files/."
129
+ )
130
+ # write the contents to a local file
131
+ with open(path_to_this_file + filename, "wb") as f:
132
+ f.write(r.content)
133
+
134
+ # store
135
+ self._filename = path_to_this_file + filename
136
+
137
+ def open(self) -> h5py.File:
138
+ """Opens the h5 file in the proper mode.
139
+
140
+ Returns:
141
+ H5 file object: Opened file.
142
+
143
+ Raises:
144
+ RuntimeError: If backend is opened for writing when it is read-only.
145
+
146
+ """
147
+ f = h5py.File(self.filename, "r")
148
+ return f
149
+
150
+ @property
151
+ def t_base(self) -> np.ndarray:
152
+ """Time array from file."""
153
+ with self.open() as f:
154
+ t_base = np.arange(self.size_base) * self.dt_base
155
+ return t_base
156
+
157
+ @property
158
+ def ltt_base(self) -> np.ndarray:
159
+ """Light travel times along links from file."""
160
+ with self.open() as f:
161
+ ltt = f["tcb"]["ltt"][:]
162
+ return ltt
163
+
164
+ @property
165
+ def n_base(self) -> np.ndarray:
166
+ """Normal unit vectors towards receiver along links from file."""
167
+ with self.open() as f:
168
+ n = f["tcb"]["n"][:]
169
+ return n
170
+
171
+ @property
172
+ def x_base(self) -> np.ndarray:
173
+ """Spacecraft position from file."""
174
+ with self.open() as f:
175
+ x = f["tcb"]["x"][:]
176
+ return x
177
+
178
+ @property
179
+ def v_base(self) -> np.ndarray:
180
+ """Spacecraft velocities from file."""
181
+ with self.open() as f:
182
+ v = f["tcb"]["v"][:]
183
+ return v
184
+
185
+ @property
186
+ def t(self) -> np.ndarray:
187
+ """Configured time array."""
188
+ self._check_configured()
189
+ return self._t
190
+
191
+ @t.setter
192
+ def t(self, t: np.ndarray):
193
+ """Set configured time array."""
194
+ assert isinstance(t, np.ndarray) and t.ndim == 1
195
+ self._t = t
196
+
197
+ @property
198
+ def ltt(self) -> np.ndarray:
199
+ """Light travel time."""
200
+ self._check_configured()
201
+ return self._ltt
202
+
203
+ @ltt.setter
204
+ def ltt(self, ltt: np.ndarray) -> np.ndarray:
205
+ """Set light travel time."""
206
+ assert ltt.shape[0] == len(self.t)
207
+
208
+ @property
209
+ def n(self) -> np.ndarray:
210
+ """Normal vectors along links."""
211
+ self._check_configured()
212
+ return self._n
213
+
214
+ @n.setter
215
+ def n(self, n: np.ndarray) -> np.ndarray:
216
+ """Set Normal vectors along links."""
217
+ return self._n
218
+
219
+ @property
220
+ def x(self) -> np.ndarray:
221
+ """Spacecraft positions."""
222
+ self._check_configured()
223
+ return self._x
224
+
225
+ @x.setter
226
+ def x(self, x: np.ndarray) -> np.ndarray:
227
+ """Set Spacecraft positions."""
228
+ return self._x
229
+
230
+ @property
231
+ def v(self) -> np.ndarray:
232
+ """Spacecraft velocities."""
233
+ self._check_configured()
234
+ return self._v
235
+
236
+ @v.setter
237
+ def v(self, v: np.ndarray) -> np.ndarray:
238
+ """Set Spacecraft velocities."""
239
+ return self._v
240
+
241
+ def configure(
242
+ self,
243
+ t_arr: Optional[np.ndarray] = None,
244
+ dt: Optional[float] = None,
245
+ linear_interp_setup: Optional[bool] = False,
246
+ ) -> None:
247
+ """Configure the orbits to match the signal response generator time basis.
248
+
249
+ The base orbits will be scaled up or down as needed using Cubic Spline interpolation.
250
+ The higherarchy of consideration to each keyword argument if multiple are given:
251
+ ``linear_interp_setup``, ``t_arr``, ``dt``.
252
+
253
+ If nothing is provided, the base points are used.
254
+
255
+ Args:
256
+ t_arr: New time array.
257
+ dt: New time step. Will take the time duration to be that of the input data.
258
+ linear_interp_setup: If ``True``, it will create a dense grid designed for linear interpolation with a constant time step.
259
+
260
+ """
261
+
262
+ x_orig = self.t_base
263
+
264
+ # everything up base on input
265
+ if linear_interp_setup:
266
+ # setup spline
267
+ make_cpp = True
268
+ dt = LINEAR_INTERP_TIMESTEP
269
+ Tobs = self.t_base[-1]
270
+ Nobs = int(Tobs / dt)
271
+ t_arr = np.arange(Nobs) * dt
272
+ if t_arr[-1] < self.t_base[-1]:
273
+ t_arr = np.concatenate([t_arr, self.t_base[-1:]])
274
+ elif t_arr is not None:
275
+ # check array inputs and fill dt
276
+ assert np.all(t_arr >= self.t_base[0]) and np.all(t_arr <= self.t_base[-1])
277
+ make_cpp = True
278
+ dt = abs(t_arr[1] - t_arr[0])
279
+
280
+ elif dt is not None:
281
+ # fill array based on dt and base t
282
+ make_cpp = True
283
+ Tobs = self.t_base[-1]
284
+ Nobs = int(Tobs / dt)
285
+ t_arr = np.arange(Nobs) * dt
286
+ if t_arr[-1] < self.t_base[-1]:
287
+ t_arr = np.concatenate([t_arr, self.t_base[-1:]])
288
+
289
+ else:
290
+ make_cpp = False
291
+ t_arr = self.t_base
292
+
293
+ x_new = t_arr.copy()
294
+ self.t = t_arr.copy()
295
+
296
+ # use base quantities, and interpolate to prepare new arrays accordingly
297
+ for which in ["ltt", "x", "n", "v"]:
298
+ arr = getattr(self, which + "_base")
299
+ arr_tmp = arr.reshape(self.size_base, -1)
300
+ arr_out_tmp = np.zeros((len(x_new), arr_tmp.shape[-1]))
301
+ for i in range(arr_tmp.shape[-1]):
302
+ arr_out_tmp[:, i] = interpolate.CubicSpline(x_orig, arr_tmp[:, i])(
303
+ x_new
304
+ )
305
+ arr_out = arr_out_tmp.reshape((len(x_new),) + arr.shape[1:])
306
+ setattr(self, "_" + which, arr_out)
307
+
308
+ # make sure base spacecraft and link inormation is ready
309
+ lsr = np.asarray(self.link_space_craft_r).copy().astype(np.int32)
310
+ lse = np.asarray(self.link_space_craft_e).copy().astype(np.int32)
311
+ ll = np.asarray(self.LINKS).copy().astype(np.int32)
312
+
313
+ # indicate this class instance has been configured
314
+ self.configured = True
315
+
316
+ # prepare cpp class args to load when needed
317
+ if make_cpp:
318
+ self.pycppdetector_args = [
319
+ dt,
320
+ len(self.t),
321
+ self.xp.asarray(self.n.flatten().copy()),
322
+ self.xp.asarray(self.ltt.flatten().copy()),
323
+ self.xp.asarray(self.x.flatten().copy()),
324
+ self.xp.asarray(ll),
325
+ self.xp.asarray(lsr),
326
+ self.xp.asarray(lse),
327
+ self.armlength,
328
+ ]
329
+ self.dt = dt
330
+ else:
331
+ self.pycppdetector_args = None
332
+ self.dt = dt
333
+
334
+ @property
335
+ def dt(self) -> float:
336
+ """new time step if it exists"""
337
+ if self._dt is None:
338
+ raise ValueError("dt not available for t_arr only.")
339
+ return self._dt
340
+
341
+ @dt.setter
342
+ def dt(self, dt: float) -> None:
343
+ self._dt = dt
344
+
345
+ @property
346
+ def pycppdetector(self) -> object:
347
+ """C++ class"""
348
+ if self._pycppdetector_args is None:
349
+ raise ValueError(
350
+ "Asking for c++ class. Need to set linear_interp_setup = True when configuring."
351
+ )
352
+ self._pycppdetector = self.backend.pycppDetector(*self._pycppdetector_args)
353
+ return self._pycppdetector
354
+
355
+ @property
356
+ def pycppdetector_args(self) -> tuple:
357
+ """args for the c++ class."""
358
+ return self._pycppdetector_args
359
+
360
+ @pycppdetector_args.setter
361
+ def pycppdetector_args(self, pycppdetector_args: tuple) -> None:
362
+ self._pycppdetector_args = pycppdetector_args
363
+
364
+ @property
365
+ def size(self) -> int:
366
+ """Number of time points."""
367
+ self._check_configured()
368
+ return len(self.t)
369
+
370
+ def _check_configured(self) -> None:
371
+ if not self.configured:
372
+ raise ValueError(
373
+ "Cannot request property. Need to use configure() method first."
374
+ )
375
+
376
+ def get_light_travel_times(
377
+ self, t: float | np.ndarray, link: int | np.ndarray
378
+ ) -> float | np.ndarray:
379
+ """Compute light travel time as a function of time.
380
+
381
+ Computes with the c++ backend.
382
+
383
+ Args:
384
+ t: Time array in seconds.
385
+ link: which link. Must be ``in self.LINKS``.
386
+
387
+ Returns:
388
+ Light travel times.
389
+
390
+ """
391
+ # test and prepare inputs
392
+ if isinstance(t, float) and isinstance(link, int):
393
+ squeeze = True
394
+ t = self.xp.atleast_1d(t)
395
+ link = self.xp.atleast_1d(link).astype(np.int32)
396
+
397
+ elif isinstance(t, self.xp.ndarray) and isinstance(link, int):
398
+ squeeze = False
399
+ t = self.xp.atleast_1d(t)
400
+ link = self.xp.full_like(t, link, dtype=np.int32)
401
+
402
+ elif isinstance(t, self.xp.ndarray) and isinstance(link, self.xp.ndarray):
403
+ squeeze = False
404
+ t = self.xp.asarray(t)
405
+ link = self.xp.asarray(link).astype(np.int32)
406
+ else:
407
+ raise ValueError(
408
+ "(t, link) can be (float, int), (np.ndarray, int), (np.ndarray, np.ndarray)."
409
+ )
410
+
411
+ # buffer array and c computation
412
+ ltt_out = self.xp.zeros_like(t)
413
+ self.pycppdetector.get_light_travel_time_arr_wrap(
414
+ ltt_out, t, link, len(ltt_out)
415
+ )
416
+
417
+ # prepare output
418
+ if squeeze:
419
+ return ltt_out[0]
420
+ return ltt_out
421
+
422
+ def get_pos(self, t: float | np.ndarray, sc: int | np.ndarray) -> np.ndarray:
423
+ """Compute light travel time as a function of time.
424
+
425
+ Computes with the c++ backend.
426
+
427
+ Args:
428
+ t: Time array in seconds.
429
+ sc: which spacecraft. Must be ``in self.SC``.
430
+
431
+ Returns:
432
+ Position of spacecraft.
433
+
434
+ """
435
+ # test and setup inputs accordingly
436
+ if isinstance(t, float) and isinstance(sc, int):
437
+ squeeze = True
438
+ t = self.xp.atleast_1d(t)
439
+ sc = self.xp.atleast_1d(sc).astype(np.int32)
440
+
441
+ elif isinstance(t, self.xp.ndarray) and isinstance(sc, int):
442
+ squeeze = False
443
+ t = self.xp.atleast_1d(t)
444
+ sc = self.xp.full_like(t, sc, dtype=np.int32)
445
+
446
+ elif isinstance(t, self.xp.ndarray) and isinstance(sc, self.xp.ndarray):
447
+ squeeze = False
448
+ t = self.xp.asarray(t)
449
+ sc = self.xp.asarray(sc).astype(np.int32)
450
+
451
+ else:
452
+ raise ValueError(
453
+ "(t, sc) can be (float, int), (np.ndarray, int), (np.ndarray, np.ndarray). If the inputs follow this, make sure the orbits class GPU setting matches the arrays coming in (GPU or CPU)."
454
+ )
455
+
456
+ # buffer arrays for input into c code
457
+ pos_x = self.xp.zeros_like(t)
458
+ pos_y = self.xp.zeros_like(t)
459
+ pos_z = self.xp.zeros_like(t)
460
+
461
+ # c code computation
462
+ self.pycppdetector.get_pos_arr_wrap(pos_x, pos_y, pos_z, t, sc, len(pos_x))
463
+
464
+ # prepare output
465
+ output = self.xp.array([pos_x, pos_y, pos_z]).T
466
+ if squeeze:
467
+ return output.squeeze()
468
+ return output
469
+
470
+ def get_normal_unit_vec(
471
+ self, t: float | np.ndarray, link: int | np.ndarray
472
+ ) -> np.ndarray:
473
+ """Compute link normal vector as a function of time.
474
+
475
+ Computes with the c++ backend.
476
+
477
+ Args:
478
+ t: Time array in seconds.
479
+ link: which link. Must be ``in self.LINKS``.
480
+
481
+ Returns:
482
+ Link normal vectors.
483
+
484
+ """
485
+ # test and prepare inputs
486
+ if isinstance(t, float) and isinstance(link, int):
487
+ squeeze = True
488
+ t = self.xp.atleast_1d(t)
489
+ link = self.xp.atleast_1d(link).astype(np.int32)
490
+
491
+ elif isinstance(t, self.xp.ndarray) and isinstance(link, int):
492
+ squeeze = False
493
+ t = self.xp.atleast_1d(t)
494
+ link = self.xp.full_like(t, link, dtype=np.int32)
495
+
496
+ elif isinstance(t, self.xp.ndarray) and isinstance(link, self.xp.ndarray):
497
+ squeeze = False
498
+ t = self.xp.asarray(t)
499
+ link = self.xp.asarray(link).astype(np.int32)
500
+ else:
501
+ raise ValueError(
502
+ "(t, link) can be (float, int), (np.ndarray, int), (np.ndarray, np.ndarray)."
503
+ )
504
+
505
+ # c code with buffers
506
+ normal_unit_vec_x = self.xp.zeros_like(t)
507
+ normal_unit_vec_y = self.xp.zeros_like(t)
508
+ normal_unit_vec_z = self.xp.zeros_like(t)
509
+
510
+ # c code
511
+ self.pycppdetector.get_normal_unit_vec_arr_wrap(
512
+ normal_unit_vec_x,
513
+ normal_unit_vec_y,
514
+ normal_unit_vec_z,
515
+ t,
516
+ link,
517
+ len(normal_unit_vec_x),
518
+ )
519
+
520
+ # prep outputs
521
+ output = self.xp.array(
522
+ [normal_unit_vec_x, normal_unit_vec_y, normal_unit_vec_z]
523
+ ).T
524
+ if squeeze:
525
+ return output.squeeze()
526
+ return output
527
+
528
+ @property
529
+ def ptr(self) -> int:
530
+ """pointer to c++ class"""
531
+ return self.pycppdetector.ptr
532
+
533
+
534
+ @classmethod
535
+ def supported_backends(cls):
536
+ return ["lisatools_" + _tmp for _tmp in cls.GPU_RECOMMENDED()]
537
+
538
+
539
+
540
+ class EqualArmlengthOrbits(Orbits):
541
+ """Equal Armlength Orbits
542
+
543
+ Orbit file: equalarmlength-orbits.h5
544
+
545
+ Args:
546
+ *args: Arguments for :class:`Orbits`.
547
+ **kwargs: Kwargs for :class:`Orbits`.
548
+
549
+ """
550
+
551
+ def __init__(self, *args: Any, **kwargs: Any):
552
+ super().__init__("equalarmlength-orbits.h5", *args, **kwargs)
553
+
554
+
555
+ class ESAOrbits(Orbits):
556
+ """ESA Orbits
557
+
558
+ Orbit file: esa-trailing-orbits.h5
559
+
560
+ Args:
561
+ *args: Arguments for :class:`Orbits`.
562
+ **kwargs: Kwargs for :class:`Orbits`.
563
+
564
+ """
565
+
566
+ def __init__(self, *args, **kwargs):
567
+ super().__init__("esa-trailing-orbits.h5", *args, **kwargs)
568
+
569
+
570
+ class DefaultOrbits(EqualArmlengthOrbits):
571
+ """Set default orbit class to Equal Armlength orbits for now."""
572
+
573
+ pass
574
+
575
+ @dataclass
576
+ class CurrentNoises:
577
+ """Noise values at a given frequency.
578
+
579
+ Args:
580
+ isi_oms_noise: Interspacecraft OMS noise value.
581
+ rfi_oms_noise: Reference interferometer OMS noise value.
582
+ tmi_oms_noise: Test-mass interferometer OMS noise value.
583
+ tm_noise: Test-mass acceleration noise value.
584
+ rfi_backlink_noise: Reference interferometer backlink noise value.
585
+ tmi_backlink_noise: Test-mass interferometer backlink noise value.
586
+ units: Either ``"relative_frequency"`` (AKA fractional frequency deviation [ffd]) or ``"displacement"``.
587
+
588
+ """
589
+ isi_oms_noise: float
590
+ rfi_oms_noise: float
591
+ tmi_oms_noise: float
592
+ tm_noise: float
593
+ rfi_backlink_noise: float
594
+ tmi_backlink_noise: float
595
+ units: str
596
+
597
+
598
+ @dataclass
599
+ class LISAModelSettings:
600
+ """Required LISA model settings:
601
+
602
+ Args:
603
+ Soms_d: OMS displacement noise.
604
+ Sa_a: Acceleration noise.
605
+ orbits: Orbital information.
606
+ name: Name of model.
607
+
608
+ """
609
+
610
+ Soms_d: float
611
+ Sa_a: float
612
+ orbits: Orbits
613
+ name: str
614
+
615
+
616
+ class LISAModel(LISAModelSettings, ABC):
617
+ """Model for the LISA Constellation
618
+
619
+ This includes sensitivity information computed in
620
+ :py:mod:`lisatools.sensitivity` and orbital information
621
+ contained in an :class:`Orbits` class object.
622
+
623
+ This class is used to house high-level methods useful
624
+ to various needed computations.
625
+
626
+ """
627
+
628
+ def __str__(self) -> str:
629
+ out = "LISA Constellation Configurations Settings:\n"
630
+ for key, item in self.__dict__.items():
631
+ out += f"{key}: {item}\n"
632
+ return out
633
+
634
+ def lisanoises(
635
+ self,
636
+ f: float | np.ndarray,
637
+ unit: Optional[str] = "relative_frequency",
638
+ ) -> CurrentNoises:
639
+ """Calculate both LISA noise terms based on input model.
640
+ Args:
641
+ f: Frequency array.
642
+ unit: Either ``"relative_frequency"`` or ``"displacement"``.
643
+ Returns:
644
+ Current noise values at ``f``.
645
+
646
+ """
647
+
648
+ # TODO: fix this up
649
+ Soms_d_in = self.Soms_d
650
+ Sa_a_in = self.Sa_a
651
+
652
+ frq = f
653
+ ### Acceleration noise
654
+ ## In acceleration
655
+ Sa_a = Sa_a_in * (1.0 + (0.4e-3 / frq) ** 2) * (1.0 + (frq / 8e-3) ** 4)
656
+ ## In displacement
657
+ Sa_d = Sa_a * (2.0 * np.pi * frq) ** (-4.0)
658
+ ## In relative frequency unit
659
+ Sa_nu = Sa_d * (2.0 * np.pi * frq / C_SI) ** 2
660
+ Spm = Sa_nu
661
+
662
+ ### Optical Metrology System
663
+ ## In displacement
664
+ Soms_d = Soms_d_in * (1.0 + (2.0e-3 / f) ** 4)
665
+ ## In relative frequency unit
666
+ Soms_nu = Soms_d * (2.0 * np.pi * frq / C_SI) ** 2
667
+ Sop = Soms_nu
668
+
669
+ # for mapping to more detailed noise setup
670
+ if unit == "displacement":
671
+ isi_oms_noise = Soms_d
672
+ tm_noise = Sa_d
673
+
674
+ elif unit == "relative_frequency":
675
+ isi_oms_noise = Sop
676
+ tm_noise = Spm
677
+
678
+ # for mapping to more detailed noise setup
679
+ rfi_oms_noise = 0.0
680
+ tmi_oms_noise = 0.0
681
+ rfi_backlink_noise = 0.0
682
+ tmi_backlink_noise = 0.0
683
+
684
+ return CurrentNoises(
685
+ isi_oms_noise,
686
+ rfi_oms_noise,
687
+ tmi_oms_noise,
688
+ tm_noise,
689
+ rfi_backlink_noise,
690
+ tmi_backlink_noise,
691
+ unit
692
+ )
693
+
694
+
695
+ # defaults
696
+ scirdv1 = LISAModel((15.0e-12) ** 2, (3.0e-15) ** 2, DefaultOrbits(), "scirdv1")
697
+ proposal = LISAModel((10.0e-12) ** 2, (3.0e-15) ** 2, DefaultOrbits(), "proposal")
698
+ mrdv1 = LISAModel((10.0e-12) ** 2, (2.4e-15) ** 2, DefaultOrbits(), "mrdv1")
699
+ sangria = LISAModel((7.9e-12) ** 2, (2.4e-15) ** 2, DefaultOrbits(), "sangria")
700
+
701
+
702
+ @dataclass
703
+ class ExtendedLISAModelSettings:
704
+ """Required Extended LISA model settings:
705
+
706
+ Args:
707
+ isi_oms_noise: Interspacecraft OMS noise level.
708
+ rfi_oms_noise: Reference interferometer OMS noise level.
709
+ tmi_oms_noise: Test-mass interferometer OMS noise level.
710
+ tm_noise: Test-mass acceleration noise level.
711
+ rfi_backlink_noise: Reference interferometer backlink noise level.
712
+ tmi_backlink_noise: Test-mass interferometer backlink noise level.
713
+ orbits: Orbital information.
714
+ name: Name of model.
715
+
716
+ """
717
+ isi_oms_level: float
718
+ rfi_oms_level: float
719
+ tmi_oms_level: float
720
+ tm_noise_level: float # formerly acceleration noise
721
+ rfi_backlink_noise_level: float
722
+ tmi_backlink_noise_level: float
723
+ orbits: Orbits
724
+ name: str
725
+
726
+ # TODO: verify this
727
+ # conversion factors into ffd units used in LDC
728
+ lamb = 1064.5e-9
729
+ nu0 = C_SI / lamb
730
+
731
+
732
+ class ExtendedLISAModel(ExtendedLISAModelSettings, ABC):
733
+ """Model for the LISA Constellation
734
+
735
+ This includes sensitivity information computed in
736
+ :py:mod:`lisatools.sensitivity` and orbital information
737
+ contained in an :class:`Orbits` class object.
738
+
739
+ This class is used to house high-level methods useful
740
+ to various needed computations.
741
+
742
+ """
743
+
744
+ def __str__(self) -> str:
745
+ out = "LISA Constellation Configurations Settings:\n"
746
+ for key, item in self.__dict__.items():
747
+ out += f"{key}: {item}\n"
748
+ return out
749
+
750
+ def disp_2_ffd(self, f: float | np.ndarray) -> float | np.ndarray:
751
+ return (2 * np.pi * f / lamb / nu0) ** 2
752
+
753
+ def acc_2_ffd(self, f: float | np.ndarray) -> float | np.ndarray:
754
+ return (1 / (lamb * 2 * np.pi * f ) / nu0) ** 2
755
+
756
+ def lisanoises(
757
+ self,
758
+ f: float | np.ndarray,
759
+ unit: Optional[str] = "relative_frequency",
760
+ method: Optional[str] ="modern",
761
+ ) -> CurrentNoises:
762
+ """Calculate both LISA noise terms based on input model.
763
+ Args:
764
+ f: Frequency array.
765
+ unit: Either ``"relative_frequency"`` or ``"displacement"``.
766
+ Returns:
767
+ Tuple with acceleration term as first value and oms term as second value.
768
+ """
769
+
770
+ # BASED on code from Olaf Hartwig
771
+ if method == "modern":
772
+ isi_oms_noise = self.isi_oms_level**2 * f**0
773
+ rfi_oms_noise = self.rfi_oms_level**2 * f**0
774
+ tmi_oms_noise = self.tmi_oms_level**2 * f**0
775
+
776
+ tm_noise = (self.tm_noise_level ** 2) * (1 + (0.4e-3 / f) ** 2)
777
+ rfi_backlink_noise = self.rfi_backlink_noise_level ** 2 * (1. + (2.e-3 / f) ** 4)
778
+ tmi_backlink_noise = self.tmi_backlink_noise_level ** 2 * (1. + (2.e-3 / f) ** 4)
779
+
780
+ elif method == "old":
781
+ isi_oms_noise = self.isi_oms_level**2 * f**0
782
+ rfi_oms_noise = self.rfi_oms_level**2 * f**0
783
+ tmi_oms_noise = self.tmi_oms_level**2 * f**0
784
+
785
+ tm_noise = (self.tm_noise_level ** 2) * (1 + (0.4e-3 / f) ** 2)
786
+ rfi_backlink_noise = self.rfi_backlink_noise_level ** 2 * (1. + (2.e-3 / f) ** 4)
787
+ tmi_backlink_noise = self.tmi_backlink_noise_level ** 2 * (1. + (2.e-3 / f) ** 4)
788
+
789
+ if unit == "displacement":
790
+ return CurrentNoises(
791
+ isi_oms_noise,
792
+ rfi_oms_noise,
793
+ tmi_oms_noise,
794
+ tm_noise,
795
+ rfi_backlink_noise,
796
+ tmi_backlink_noise,
797
+ unit
798
+ )
799
+ elif unit == "relative_frequency":
800
+ return CurrentNoises(
801
+ isi_oms_noise * self.disp_2_ffd(f),
802
+ rfi_oms_noise * self.disp_2_ffd(f),
803
+ tmi_oms_noise * self.disp_2_ffd(f),
804
+ tm_noise * self.acc_2_ffd(f),
805
+ rfi_backlink_noise * self.disp_2_ffd(f),
806
+ tmi_backlink_noise * self.disp_2_ffd(f),
807
+ unit
808
+ )
809
+ else:
810
+ raise ValueError("unit kwarg must be 'displacement' or 'relative_frequency'.")
811
+
812
+ # defaults
813
+
814
+ # HERE we simulate the old LDC way of generating the sensitivity by pretending
815
+ # the rfi_backlink_noise, which has the same functionality of the OMS noise in the
816
+ # LDC code, is the oms noise.
817
+ sangria_v2 = ExtendedLISAModel(6.35e-12, 3.32e-12, 1.42e-12, 2.4e-15, 3.0E-12, 3.0E-12, DefaultOrbits(), "sangria_v2")
818
+
819
+
820
+ __stock_list_models__ = [scirdv1, proposal, mrdv1, sangria, sangria_v2]
821
+ __stock_list_models_name__ = [tmp.name for tmp in __stock_list_models__]
822
+
823
+
824
+ def get_available_default_lisa_models() -> List[LISAModel]:
825
+ """Get list of default LISA models
826
+
827
+ Returns:
828
+ List of LISA models.
829
+
830
+ """
831
+ return __stock_list_models__
832
+
833
+
834
+ def get_default_lisa_model_from_str(model: str) -> LISAModel:
835
+ """Return a LISA model from a ``str`` input.
836
+
837
+ Args:
838
+ model: Model indicated with a ``str``.
839
+
840
+ Returns:
841
+ LISA model associated to that ``str``.
842
+
843
+ """
844
+ if model not in __stock_list_models_name__:
845
+ raise ValueError(
846
+ "Requested string model is not available. See lisatools.detector documentation."
847
+ )
848
+ return globals()[model]
849
+
850
+
851
+ def check_lisa_model(model: Any) -> LISAModel:
852
+ """Check input LISA model.
853
+
854
+ Args:
855
+ model: LISA model to check.
856
+
857
+ Returns:
858
+ LISA Model checked. Adjusted from ``str`` if ``str`` input.
859
+
860
+ """
861
+ if isinstance(model, str):
862
+ model = get_default_lisa_model_from_str(model)
863
+
864
+ if not isinstance(model, LISAModel) and not isinstance(model, ExtendedLISAModel):
865
+ raise ValueError("Model argument not given correctly.")
866
+
867
+ return model