lisaanalysistools 1.0.14__cp312-cp312-macosx_11_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.

Potentially problematic release.


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

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