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.
- lisaanalysistools-1.0.14.dist-info/LICENSE +201 -0
- lisaanalysistools-1.0.14.dist-info/METADATA +108 -0
- lisaanalysistools-1.0.14.dist-info/RECORD +43 -0
- lisaanalysistools-1.0.14.dist-info/WHEEL +5 -0
- lisaanalysistools-1.0.14.dist-info/top_level.txt +1 -0
- lisatools/__init__.py +0 -0
- lisatools/_version.py +4 -0
- lisatools/analysiscontainer.py +474 -0
- lisatools/cutils/__init__.py +0 -0
- lisatools/cutils/detector_cpu.cpython-312-darwin.so +0 -0
- lisatools/cutils/include/Detector.hpp +84 -0
- lisatools/cutils/include/__init__.py +0 -0
- lisatools/cutils/include/global.hpp +28 -0
- lisatools/cutils/src/Detector.cpp +307 -0
- lisatools/cutils/src/Detector.cu +307 -0
- lisatools/cutils/src/__init__.py +0 -0
- lisatools/cutils/src/pycppdetector.pyx +255 -0
- lisatools/datacontainer.py +312 -0
- lisatools/detector.py +706 -0
- lisatools/diagnostic.py +990 -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 +972 -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 +36 -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 +67 -0
- lisatools/sources/utils.py +456 -0
- lisatools/sources/waveformbase.py +41 -0
- lisatools/stochastic.py +327 -0
- lisatools/utils/__init__.py +0 -0
- lisatools/utils/constants.py +40 -0
- lisatools/utils/pointeradjust.py +106 -0
- 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
|