fastlisaresponse 1.0.2__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 fastlisaresponse might be problematic. Click here for more details.

@@ -0,0 +1 @@
1
+ from .response import pyResponseTDI, ResponseWrapper
@@ -0,0 +1 @@
1
+ __version__ = '1.0.2'
File without changes
@@ -0,0 +1,33 @@
1
+ import numpy as np
2
+
3
+ try:
4
+ import cupy as cp
5
+
6
+ gpu = True
7
+
8
+ except (ImportError, ModuleNotFoundError) as e:
9
+ gpu = False
10
+
11
+
12
+ def pointer_adjust(func):
13
+ def func_wrapper(*args, **kwargs):
14
+ targs = []
15
+ for arg in args:
16
+ if gpu:
17
+ if isinstance(arg, cp.ndarray):
18
+ targs.append(arg.data.mem.ptr)
19
+ continue
20
+
21
+ if isinstance(arg, np.ndarray):
22
+ targs.append(arg.__array_interface__["data"][0])
23
+ continue
24
+
25
+ try:
26
+ targs.append(arg.ptr)
27
+ continue
28
+ except AttributeError:
29
+ targs.append(arg)
30
+
31
+ return func(*targs, **kwargs)
32
+
33
+ return func_wrapper
@@ -0,0 +1,796 @@
1
+ from multiprocessing.sharedctypes import Value
2
+ import numpy as np
3
+ from typing import Optional, List
4
+ import warnings
5
+ from typing import Tuple
6
+ from copy import deepcopy
7
+
8
+
9
+ try:
10
+ import cupy as cp
11
+ from pyresponse import get_response_wrap as get_response_wrap_gpu
12
+ from pyresponse import get_tdi_delays_wrap as get_tdi_delays_wrap_gpu
13
+
14
+ gpu = True
15
+
16
+ except (ImportError, ModuleNotFoundError) as e:
17
+ import numpy as xp
18
+
19
+ gpu = False
20
+
21
+ from .cutils.detector import pycppdetector as pycppdetector_here
22
+ from pyresponse_cpu import get_response_wrap as get_response_wrap_cpu
23
+ from pyresponse_cpu import get_tdi_delays_wrap as get_tdi_delays_wrap_cpu
24
+ import time
25
+ import h5py
26
+
27
+ from scipy.interpolate import CubicSpline
28
+
29
+ from lisatools.detector import EqualArmlengthOrbits, Orbits
30
+ from lisatools.utils.utility import AET
31
+ from lisatools.utils.pointeradjust import pointer_adjust
32
+
33
+ YRSID_SI = 31558149.763545603
34
+
35
+
36
+ def get_factorial(n):
37
+ fact = 1
38
+
39
+ for i in range(1, n + 1):
40
+ fact = fact * i
41
+
42
+ return fact
43
+
44
+
45
+ from math import factorial
46
+
47
+ factorials = np.array([factorial(i) for i in range(30)])
48
+
49
+ C_inv = 3.3356409519815204e-09
50
+
51
+
52
+ class pyResponseTDI(object):
53
+ """Class container for fast LISA response function generation.
54
+
55
+ The class computes the generic time-domain response function for LISA.
56
+ It takes LISA constellation orbital information as input and properly determines
57
+ the response for these orbits numerically. This includes both the projection
58
+ of the gravitational waves onto the LISA constellation arms and combinations \
59
+ of projections into TDI observables. The methods and maths used can be found
60
+ in # TODO: add url for paper.
61
+
62
+ This class is also GPU-accelerated, which is very helpful for Bayesian inference
63
+ methods.
64
+
65
+ Args:
66
+ sampling_frequency (double): The sampling rate in Hz.
67
+ num_pts (int): Number of points to produce for the final output template.
68
+ orbit_kwargs (dict): Dictionary containing orbital information. The kwargs and defaults
69
+ are: :code:`orbit_module=None, order=0, max_t_orbits=3.15576e7, orbit_file=None`.
70
+ :code:`orbit_module` is an orbit module from the LDC package. :code:`max_t_orbits` is
71
+ the maximum time desired for the orbital information. `orbit_file` is
72
+ an h5 file of the form used `here <https://gitlab.in2p3.fr/lisa-simulation/orbits>`_.
73
+ :code:`order` is the order of interpolation used in the orbit modules.
74
+ order (int, optional): Order of Lagrangian interpolation technique. Lower orders
75
+ will be faster. The user must make sure the order is sufficient for the
76
+ waveform being used. (default: 25)
77
+ tdi (str or list, optional): TDI setup. Currently, the stock options are
78
+ :code:`'1st generation'` and :code:`'2nd generation'`. Or the user can provide
79
+ a list of tdi_combinations of the form
80
+ :code:`{"link": 12, "links_for_delay": [21, 13, 31], "sign": 1, "type": "delay"}`.
81
+ :code:`'link'` (`int`) the link index (12, 21, 13, 31, 23, 32) for the projection (:math:`y_{ij}`).
82
+ :code:`'links_for_delay'` (`list`) are the link indexes as a list used for delays
83
+ applied to the link projections.
84
+ ``'sign'`` is the sign in front of the contribution to the TDI observable. It takes the value of `+1` or `-1`.
85
+ ``type`` is either ``"delay"`` or ``"advance"``. It is optional and defaults to ``"delay"``.
86
+ (default: ``"1st generation"``)
87
+ tdi_orbit_kwargs (dict, optional): Same as :code:`orbit_kwargs`, but specifically for the TDI
88
+ portion of the response computation. This allows the user to use two different orbits
89
+ for the projections and TDI. For example, this can be used to examine the efficacy of
90
+ frequency domain TDI codes that can handle generic orbits for the projections, but
91
+ assume equal armlength orbits to reduce and simplify the expression for TDI
92
+ computations. (default: :code:`None`, this means the orbits for the projections
93
+ and TDI will be the same and will be built from :code:`orbit_kwargs`)
94
+ tdi_chan (str, optional): Which TDI channel combination to return. Choices are :code:`'XYZ'`,
95
+ :code:`AET`, or :code:`AE`. (default: :code:`'XYZ'`)
96
+ use_gpu (bool, optional): If True, run code on the GPU. (default: :code:`False`)
97
+
98
+ Attributes:
99
+ A_in (xp.ndarray): Array containing y values for linear spline of A
100
+ during Lagrangian interpolation.
101
+ buffer_integer (int): Self-determined buffer necesary for the given
102
+ value for :code:`order`.
103
+ channels_no_delays (2D np.ndarray): Carrier of link index and sign information
104
+ for arms that do not get delayed during TDI computation.
105
+ deps (double): The spacing between Epsilon values in the interpolant
106
+ for the A quantity in Lagrangian interpolation. Hard coded to
107
+ 1/(:code:`num_A` - 1).
108
+ dt (double): Inverse of the sampling_frequency.
109
+ E_in (xp.ndarray): Array containing y values for linear spline of E
110
+ during Lagrangian interpolation.
111
+ half_order (int): Half of :code:`order` adjusted to be :code:`int`.
112
+ link_inds (xp.ndarray): Link indexes for delays in TDI.
113
+ link_space_craft_0_in (xp.ndarray): Link indexes for receiver on each
114
+ arm of the LISA constellation.
115
+ link_space_craft_1_in (xp.ndarray): Link indexes for emitter on each
116
+ arm of the LISA constellation.
117
+ nlinks (int): The number of links in the constellation. Typically 6.
118
+ num_A (int): Number of points to use for A spline values used in the Lagrangian
119
+ interpolation. This is hard coded to 1001.
120
+ num_channels (int): 3.
121
+ num_pts (int): Number of points to produce for the final output template.
122
+ num_tdi_combinations (int): Number of independent arm computations.
123
+ num_tdi_delay_comps (int): Number of independent arm computations that require delays.
124
+ orbits_store (dict): Contains orbital information for the projection and TDI
125
+ steps.
126
+ order (int): Order of Lagrangian interpolation technique.
127
+ response_gen (func): Projection generator function.
128
+ sampling_frequency (double): The sampling rate in Hz.
129
+ tdi (str or list): TDI setup.
130
+ tdi_buffer (int): The buffer necessary for all information needed at early times
131
+ for the TDI computation. This is set to 200.
132
+ tdi_chan (str): Which TDI channel combination to return.
133
+ tdi_delays (xp.ndarray): TDI delays.
134
+ tdi_gen (func): TDI generating function.
135
+ tdi_signs (xp.ndarray): Signs applied to the addition of a delayed link. (+1 or -1)
136
+ use_gpu (bool): If True, run on GPU.
137
+ xp (obj): Either Numpy or Cupy.
138
+
139
+ """
140
+
141
+ def __init__(
142
+ self,
143
+ sampling_frequency,
144
+ num_pts,
145
+ order=25,
146
+ tdi="1st generation",
147
+ orbits: Optional[Orbits] = EqualArmlengthOrbits,
148
+ tdi_orbits: Optional[Orbits] = None,
149
+ tdi_chan="XYZ",
150
+ use_gpu=False,
151
+ ):
152
+
153
+ # setup all quantities
154
+ self.sampling_frequency = sampling_frequency
155
+ self.dt = 1 / sampling_frequency
156
+ self.tdi_buffer = 200
157
+
158
+ self.num_pts = num_pts
159
+
160
+ # Lagrangian interpolation setup
161
+ self.order = order
162
+ self.buffer_integer = self.order * 2 + 1
163
+ self.half_order = int((order + 1) / 2)
164
+
165
+ # setup TDI information
166
+ self.tdi = tdi
167
+ self.tdi_chan = tdi_chan
168
+
169
+ # setup functions for GPU or CPU
170
+ self.use_gpu = use_gpu
171
+ if use_gpu:
172
+ self.response_gen = get_response_wrap_gpu
173
+ self.tdi_gen = get_tdi_delays_wrap_gpu
174
+
175
+ else:
176
+ self.response_gen = get_response_wrap_cpu
177
+ self.tdi_gen = get_tdi_delays_wrap_cpu
178
+
179
+ # prepare the interpolation of A and E in the Lagrangian interpolation
180
+ self._fill_A_E()
181
+
182
+ # setup orbits
183
+ self.response_orbits = orbits
184
+
185
+ if tdi_orbits is None:
186
+ tdi_orbits = self.response_orbits
187
+
188
+ self.tdi_orbits = tdi_orbits
189
+
190
+ if self.num_pts * self.dt > self.response_orbits.t_base.max():
191
+ warnings.warn(
192
+ "Input number of points is longer in time than available orbital information. Trimming to fit orbital information."
193
+ )
194
+ self.num_pts = int(self.response_orbits.t_base.max() / self.dt)
195
+
196
+ # setup spacecraft links indexes
197
+
198
+ # setup TDI info
199
+ self._init_TDI_delays()
200
+
201
+ @property
202
+ def xp(self) -> object:
203
+ return np if not self.use_gpu else cp
204
+
205
+ @property
206
+ def response_orbits(self) -> Orbits:
207
+ """Response function orbits."""
208
+ return self._response_orbits
209
+
210
+ @response_orbits.setter
211
+ def response_orbits(self, orbits: Orbits) -> None:
212
+ """Set response orbits."""
213
+
214
+ if orbits is None:
215
+ orbits = EqualArmlengthOrbits()
216
+
217
+ assert isinstance(orbits, Orbits)
218
+
219
+ orbits.pycppdetector_base = pycppdetector_here
220
+
221
+ self._response_orbits = deepcopy(orbits)
222
+
223
+ if not self._response_orbits.configured:
224
+ self._response_orbits.configure(linear_interp_setup=True)
225
+
226
+ @property
227
+ def tdi_orbits(self) -> Orbits:
228
+ """TDI function orbits."""
229
+ return self._tdi_orbits
230
+
231
+ @tdi_orbits.setter
232
+ def tdi_orbits(self, orbits: Orbits) -> None:
233
+ """Set TDI orbits."""
234
+
235
+ if orbits is None:
236
+ orbits = EqualArmlengthOrbits()
237
+
238
+ assert isinstance(orbits, Orbits)
239
+
240
+ orbits.pycppdetector_base = pycppdetector_here
241
+ self._tdi_orbits = deepcopy(orbits)
242
+
243
+ if not self._tdi_orbits.configured:
244
+ self._tdi_orbits.configure(linear_interp_setup=True)
245
+
246
+ @property
247
+ def citation(self):
248
+ """Get citations for use of this code"""
249
+
250
+ return """
251
+ # TODO add
252
+ """
253
+
254
+ def _fill_A_E(self):
255
+ """Set up A and E terms inside the Lagrangian interpolant"""
256
+
257
+ factorials = np.asarray([float(get_factorial(n)) for n in range(40)])
258
+
259
+ # base quantities for linear interpolant over A
260
+ self.num_A = 1001
261
+ self.deps = 1.0 / (self.num_A - 1)
262
+
263
+ eps = np.arange(self.num_A) * self.deps
264
+
265
+ h = self.half_order
266
+
267
+ denominator = factorials[h - 1] * factorials[h]
268
+
269
+ # prepare A
270
+ A_in = np.zeros_like(eps)
271
+ for j, eps_i in enumerate(eps):
272
+ A = 1.0
273
+ for i in range(1, h):
274
+ A *= (i + eps_i) * (i + 1 - eps_i)
275
+
276
+ A /= denominator
277
+ A_in[j] = A
278
+
279
+ self.A_in = self.xp.asarray(A_in)
280
+
281
+ # prepare E
282
+ E_in = self.xp.zeros((self.half_order,))
283
+
284
+ for j in range(1, self.half_order):
285
+ first_term = factorials[h - 1] / factorials[h - 1 - j]
286
+ second_term = factorials[h] / factorials[h + j]
287
+ value = first_term * second_term
288
+ value = value * (-1.0) ** j
289
+ E_in[j - 1] = value
290
+
291
+ self.E_in = self.xp.asarray(E_in)
292
+
293
+ def _init_TDI_delays(self):
294
+ """Initialize TDI specific information"""
295
+
296
+ # setup the actual TDI combination
297
+ if self.tdi in ["1st generation", "2nd generation"]:
298
+ # tdi 1.0
299
+ tdi_combinations = [
300
+ {"link": 13, "links_for_delay": [], "sign": +1},
301
+ {"link": 31, "links_for_delay": [13], "sign": +1},
302
+ {"link": 12, "links_for_delay": [13, 31], "sign": +1},
303
+ {"link": 21, "links_for_delay": [13, 31, 12], "sign": +1},
304
+ {"link": 12, "links_for_delay": [], "sign": -1},
305
+ {"link": 21, "links_for_delay": [12], "sign": -1},
306
+ {"link": 13, "links_for_delay": [12, 21], "sign": -1},
307
+ {"link": 31, "links_for_delay": [12, 21, 13], "sign": -1},
308
+ ]
309
+
310
+ if self.tdi == "2nd generation":
311
+ # tdi 2.0 is tdi 1.0 + additional terms
312
+ tdi_combinations += [
313
+ {"link": 12, "links_for_delay": [13, 31, 12, 21], "sign": +1},
314
+ {"link": 21, "links_for_delay": [13, 31, 12, 21, 12], "sign": +1},
315
+ {
316
+ "link": 13,
317
+ "links_for_delay": [13, 31, 12, 21, 12, 21],
318
+ "sign": +1,
319
+ },
320
+ {
321
+ "link": 31,
322
+ "links_for_delay": [13, 31, 12, 21, 12, 21, 13],
323
+ "sign": +1,
324
+ },
325
+ {"link": 13, "links_for_delay": [12, 21, 13, 31], "sign": -1},
326
+ {"link": 31, "links_for_delay": [12, 21, 13, 31, 13], "sign": -1},
327
+ {
328
+ "link": 12,
329
+ "links_for_delay": [12, 21, 13, 31, 13, 31],
330
+ "sign": -1,
331
+ },
332
+ {
333
+ "link": 21,
334
+ "links_for_delay": [12, 21, 13, 31, 13, 31, 12],
335
+ "sign": -1,
336
+ },
337
+ ]
338
+
339
+ elif isinstance(self.tdi, list):
340
+ tdi_combinations = self.tdi
341
+
342
+ else:
343
+ raise ValueError(
344
+ "tdi kwarg should be '1st generation', '2nd generation', or a list with a specific tdi combination."
345
+ )
346
+ self.tdi_combinations = tdi_combinations
347
+
348
+ @property
349
+ def tdi_combinations(self) -> List:
350
+ """TDI Combination setup"""
351
+ return self._tdi_combinations
352
+
353
+ @tdi_combinations.setter
354
+ def tdi_combinations(self, tdi_combinations: List) -> None:
355
+ """Set TDI combinations and fill out setup."""
356
+ tdi_base_links = []
357
+ tdi_link_combinations = []
358
+ tdi_signs = []
359
+ channels = []
360
+
361
+ for permutation_number in range(3):
362
+ for tmp in tdi_combinations:
363
+ if len(tmp["links_for_delay"]) == 0:
364
+ tdi_base_links.append(
365
+ self._cyclic_permutation(tmp["link"], permutation_number)
366
+ )
367
+ tdi_link_combinations.append(-11)
368
+ tdi_signs.append(tmp["sign"])
369
+ channels.append(permutation_number)
370
+ continue
371
+
372
+ for link_delay in tmp["links_for_delay"]:
373
+ tdi_base_links.append(
374
+ self._cyclic_permutation(tmp["link"], permutation_number)
375
+ )
376
+ tdi_link_combinations.append(
377
+ self._cyclic_permutation(link_delay, permutation_number)
378
+ )
379
+ tdi_signs.append(tmp["sign"])
380
+ channels.append(permutation_number)
381
+
382
+ self.tdi_base_links = self.xp.asarray(tdi_base_links).astype(self.xp.int32)
383
+ self.tdi_link_combinations = self.xp.asarray(tdi_link_combinations).astype(
384
+ self.xp.int32
385
+ )
386
+ self.tdi_signs = self.xp.asarray(tdi_signs).astype(self.xp.int32)
387
+ self.channels = self.xp.asarray(channels).astype(self.xp.int32)
388
+ assert (
389
+ len(self.tdi_base_links)
390
+ == len(self.tdi_link_combinations)
391
+ == len(self.tdi_signs)
392
+ == len(self.channels)
393
+ )
394
+
395
+ def _cyclic_permutation(self, link, permutation):
396
+ """permute indexes by cyclic permutation"""
397
+ link_str = str(link)
398
+
399
+ out = ""
400
+ for i in range(2):
401
+ sc = int(link_str[i])
402
+ temp = sc + permutation
403
+ if temp > 3:
404
+ temp = temp % 3
405
+ out += str(temp)
406
+
407
+ return int(out)
408
+
409
+ @property
410
+ def y_gw(self):
411
+ """Projections along the arms"""
412
+ return self.y_gw_flat.reshape(self.nlinks, -1)
413
+
414
+ def _data_time_check(
415
+ self, t_data: np.ndarray, input_in: np.ndarray
416
+ ) -> Tuple[np.ndarray, np.ndarray]:
417
+ # remove input data that goes beyond orbital information
418
+ if t_data.max() > self.response_orbits.t.max():
419
+ warnings.warn(
420
+ "Input waveform is longer than available orbital information. Trimming to fit orbital information."
421
+ )
422
+
423
+ max_ind = np.where(t_data <= self.response_orbits.t.max())[0][-1]
424
+
425
+ t_data = t_data[:max_ind]
426
+ input_in = input_in[:max_ind]
427
+ return (t_data, input_in)
428
+
429
+ def get_projections(self, input_in, lam, beta, t0=10000.0):
430
+ """Compute projections of GW signal on to LISA constellation
431
+
432
+ Args:
433
+ input_in (xp.ndarray): Input complex time-domain signal. It should be of the form:
434
+ :math:`h_+ + ih_x`. If using the GPU for the response, this should be a CuPy array.
435
+ lam (double): Ecliptic Longitude in radians.
436
+ beta (double): Ecliptic Latitude in radians.
437
+ t0 (double, optional): Time at which to the waveform. Because of the delays
438
+ and interpolation towards earlier times, the beginning of the waveform
439
+ is garbage. ``t0`` tells the waveform generator where to start the waveform
440
+ compraed to ``t=0``.
441
+
442
+ Raises:
443
+ ValueError: If ``t0`` is not large enough.
444
+
445
+
446
+ """
447
+ self.tdi_start_ind = int(t0 / self.dt)
448
+ # get necessary buffer for TDI
449
+ self.check_tdi_buffer = int(100.0 * self.sampling_frequency) + 4 * self.order
450
+
451
+ from copy import deepcopy
452
+
453
+ tmp_orbits = deepcopy(self.response_orbits.x_base)
454
+ self.projection_buffer = (
455
+ int(
456
+ (
457
+ np.sum(
458
+ tmp_orbits.copy() * tmp_orbits.copy(),
459
+ axis=-1,
460
+ )
461
+ ** (1 / 2)
462
+ ).max()
463
+ * C_inv
464
+ )
465
+ + 4 * self.order
466
+ )
467
+ self.projections_start_ind = self.tdi_start_ind - 2 * self.check_tdi_buffer
468
+
469
+ if self.projections_start_ind < self.projection_buffer:
470
+ raise ValueError(
471
+ "Need to increase t0. The initial buffer is not large enough."
472
+ )
473
+
474
+ # determine sky vectors
475
+ k = np.zeros(3, dtype=np.float64)
476
+ u = np.zeros(3, dtype=np.float64)
477
+ v = np.zeros(3, dtype=np.float64)
478
+
479
+ self.num_total_points = len(input_in)
480
+
481
+ cosbeta = np.cos(beta)
482
+ sinbeta = np.sin(beta)
483
+
484
+ coslam = np.cos(lam)
485
+ sinlam = np.sin(lam)
486
+
487
+ v[0] = -sinbeta * coslam
488
+ v[1] = -sinbeta * sinlam
489
+ v[2] = cosbeta
490
+ u[0] = sinlam
491
+ u[1] = -coslam
492
+ u[2] = 0.0
493
+ k[0] = -cosbeta * coslam
494
+ k[1] = -cosbeta * sinlam
495
+ k[2] = -sinbeta
496
+
497
+ self.nlinks = 6
498
+ k_in = self.xp.asarray(k)
499
+ u_in = self.xp.asarray(u)
500
+ v_in = self.xp.asarray(v)
501
+
502
+ input_in = self.xp.asarray(input_in)
503
+
504
+ t_data = self.xp.arange(len(input_in)) * self.dt
505
+
506
+ t_data, input_in = self._data_time_check(t_data, input_in)
507
+
508
+ assert len(input_in) >= self.num_pts
509
+ y_gw = self.xp.zeros((self.nlinks * self.num_pts,), dtype=self.xp.float64)
510
+
511
+ self.response_gen(
512
+ y_gw,
513
+ t_data,
514
+ k_in,
515
+ u_in,
516
+ v_in,
517
+ self.dt,
518
+ len(input_in),
519
+ input_in,
520
+ len(input_in),
521
+ self.order,
522
+ self.sampling_frequency,
523
+ self.buffer_integer,
524
+ self.A_in,
525
+ self.deps,
526
+ len(self.A_in),
527
+ self.E_in,
528
+ self.projections_start_ind,
529
+ self.response_orbits,
530
+ )
531
+
532
+ self.y_gw_flat = y_gw
533
+ self.y_gw_length = self.num_pts
534
+
535
+ @property
536
+ def XYZ(self):
537
+ """Return links as an array"""
538
+ return self.delayed_links_flat.reshape(3, -1)
539
+
540
+ def get_tdi_delays(self, y_gw=None):
541
+ """Get TDI combinations from projections.
542
+
543
+ This functions generates the TDI combinations from the projections
544
+ computed with ``get_projections``. It can return XYZ, AET, or AE depending
545
+ on what was input for ``tdi_chan`` into ``__init__``.
546
+
547
+ Args:
548
+ y_gw (xp.ndarray, optional): Projections along each link. Must be
549
+ a 2D ``numpy`` or ``cupy`` array with shape: ``(nlinks, num_pts)``.
550
+ The links must be entered in the proper order in the code:
551
+ 21, 12, 31, 13, 32, 23. (Default: None)
552
+
553
+ Returns:
554
+ tuple: (X,Y,Z) or (A,E,T) or (A,E)
555
+
556
+ Raises:
557
+ ValueError: If ``tdi_chan`` is not one of the options.
558
+
559
+
560
+ """
561
+ self.delayed_links_flat = self.xp.zeros(
562
+ (3, self.num_pts), dtype=self.xp.float64
563
+ )
564
+
565
+ # y_gw entered directly
566
+ if y_gw is not None:
567
+ assert y_gw.shape == (len(self.link_space_craft_0_in), self.num_pts)
568
+ self.y_gw_flat = y_gw.flatten().copy()
569
+ self.y_gw_length = self.num_pts
570
+
571
+ elif self.y_gw_flat is None:
572
+ raise ValueError(
573
+ "Need to either enter projection array or have this code determine projections."
574
+ )
575
+
576
+ self.delayed_links_flat = self.delayed_links_flat.flatten()
577
+
578
+ t_data = self.xp.arange(self.y_gw_length) * self.dt
579
+
580
+ self.tdi_gen(
581
+ self.delayed_links_flat,
582
+ self.y_gw_flat,
583
+ self.y_gw_length,
584
+ self.num_pts,
585
+ t_data,
586
+ self.tdi_base_links,
587
+ self.tdi_link_combinations,
588
+ self.tdi_signs,
589
+ self.channels,
590
+ len(self.tdi_base_links), # num_units
591
+ 3, # num channels
592
+ self.order,
593
+ self.sampling_frequency,
594
+ self.buffer_integer,
595
+ self.A_in,
596
+ self.deps,
597
+ len(self.A_in),
598
+ self.E_in,
599
+ self.tdi_start_ind,
600
+ self.tdi_orbits,
601
+ )
602
+
603
+ if self.tdi_chan == "XYZ":
604
+ X, Y, Z = self.XYZ
605
+ return X, Y, Z
606
+
607
+ elif self.tdi_chan == "AET" or self.tdi_chan == "AE":
608
+ X, Y, Z = self.XYZ
609
+ A, E, T = AET(X, Y, Z)
610
+ if self.tdi_chan == "AET":
611
+ return A, E, T
612
+
613
+ else:
614
+ return A, E
615
+
616
+ else:
617
+ raise ValueError("tdi_chan must be 'XYZ', 'AET' or 'AE'.")
618
+
619
+
620
+ class ResponseWrapper:
621
+ """Wrapper to produce LISA TDI from TD waveforms
622
+
623
+ This class takes a waveform generator that produces :math:`h_+ \pm ih_x`.
624
+ (:code:`flip_hx` is used if the waveform produces :math:`h_+ - ih_x`).
625
+ It takes the complex waveform in the SSB frame and produces the TDI channels
626
+ according to settings chosen for :class:`pyResponseTDI`.
627
+
628
+ The waveform generator must have :code:`kwargs` with :code:`T` for the observation
629
+ time in years and :code:`dt` for the time step in seconds.
630
+
631
+ Args:
632
+ waveform_gen (obj): Function or class (with a :code:`__call__` function) that takes parameters and produces
633
+ :math:`h_+ \pm h_x`.
634
+ Tobs (double): Observation time in years.
635
+ dt (double): Time between time samples in seconds. The inverse of the sampling frequency.
636
+ index_lambda (int): The user will input parameters. The code will read these in
637
+ with the :code:`*args` formalism producing a list. :code:`index_lambda`
638
+ tells the class the index of the ecliptic longitude within this list of
639
+ parameters.
640
+ index_beta (int): The user will input parameters. The code will read these in
641
+ with the :code:`*args` formalism producing a list. :code:`index_beta`
642
+ tells the class the index of the ecliptic latitude (or ecliptic polar angle)
643
+ within this list of parameters.
644
+ t0 (double, optional): Start of returned waveform in seconds leaving ample time for garbage at
645
+ the beginning of the waveform. It also removed the same amount from the end. (Default: 10000.0)
646
+ flip_hx (bool, optional): If True, :code:`waveform_gen` produces :math:`h_+ - ih_x`.
647
+ :class:`pyResponseTDI` takes :math:`h_+ + ih_x`, so this setting will
648
+ multiply the cross polarization term out of the waveform generator by -1.
649
+ (Default: :code:`False`)
650
+ remove_sky_coords (bool, optional): If True, remove the sky coordinates from
651
+ the :code:`*args` list. This should be set to True if the waveform
652
+ generator does not take in the sky information. (Default: :code:`False`)
653
+ is_ecliptic_latitude (bool, optional): If True, the latitudinal sky
654
+ coordinate is the ecliptic latitude. If False, thes latitudinal sky
655
+ coordinate is the polar angle. In this case, the code will
656
+ convert it with :math:`\beta=\pi / 2 - \Theta`. (Default: :code:`True`)
657
+ use_gpu (bool, optional): If True, use GPU. (Default: :code:`False`)
658
+ remove_garbage (bool or str, optional): If True, it removes everything before ``t0``
659
+ and after the end time - ``t0``. If ``str``, it must be ``"zero"``. If ``"zero"``,
660
+ it will not remove the points, but set them to zero. This is ideal for PE. (Default: ``True``)
661
+ n_overide (int, optional): If not ``None``, this will override the determination of
662
+ the number of points, ``n``, from ``int(T/dt)`` to the ``n_overide``. This is used
663
+ if there is an issue matching points between the waveform generator and the response
664
+ model.
665
+ **kwargs (dict, optional): Keyword arguments passed to :class:`pyResponseTDI`.
666
+
667
+ """
668
+
669
+ def __init__(
670
+ self,
671
+ waveform_gen,
672
+ Tobs,
673
+ dt,
674
+ index_lambda,
675
+ index_beta,
676
+ t0=10000.0,
677
+ flip_hx=False,
678
+ remove_sky_coords=False,
679
+ is_ecliptic_latitude=True,
680
+ use_gpu=False,
681
+ remove_garbage=True,
682
+ n_overide=None,
683
+ orbits: Optional[Orbits] = EqualArmlengthOrbits,
684
+ **kwargs,
685
+ ):
686
+
687
+ # store all necessary information
688
+ self.waveform_gen = waveform_gen
689
+ self.index_lambda = index_lambda
690
+ self.index_beta = index_beta
691
+ self.dt = dt
692
+ self.t0 = t0
693
+ self.sampling_frequency = 1.0 / dt
694
+
695
+ if orbits is None:
696
+ orbits = EqualArmlengthOrbits()
697
+
698
+ assert isinstance(orbits, Orbits)
699
+
700
+ if Tobs * YRSID_SI > orbits.t_base.max():
701
+ warnings.warn(
702
+ f"Tobs is larger than available orbital information time array. Reducing Tobs to {orbits.t_base.max()}"
703
+ )
704
+ Tobs = orbits.t_base.max() / YRSID_SI
705
+
706
+ if n_overide is not None:
707
+ if not isinstance(n_overide, int):
708
+ raise ValueError("n_overide must be an integer if not None.")
709
+ self.n = n_overide
710
+
711
+ else:
712
+ self.n = int(Tobs * YRSID_SI / dt)
713
+
714
+ self.Tobs = self.n * dt
715
+ self.is_ecliptic_latitude = is_ecliptic_latitude
716
+ self.remove_sky_coords = remove_sky_coords
717
+ self.flip_hx = flip_hx
718
+ self.remove_garbage = remove_garbage
719
+
720
+ # initialize response function class
721
+ self.response_model = pyResponseTDI(
722
+ self.sampling_frequency, self.n, orbits=orbits, use_gpu=use_gpu, **kwargs
723
+ )
724
+
725
+ self.use_gpu = use_gpu
726
+
727
+ self.Tobs = (self.n * self.response_model.dt) / YRSID_SI
728
+
729
+ @property
730
+ def xp(self) -> object:
731
+ return np if not self.use_gpu else cp
732
+
733
+ @property
734
+ def citation(self):
735
+ """Get citations for use of this code"""
736
+
737
+ return """
738
+ # TODO add
739
+ """
740
+
741
+ def __call__(self, *args, **kwargs):
742
+ """Run the waveform and response generation
743
+
744
+ Args:
745
+ *args (list): Arguments to the waveform generator. This must include
746
+ the sky coordinates.
747
+ **kwargs (dict): kwargs necessary for the waveform generator.
748
+
749
+ Return:
750
+ list: TDI Channels.
751
+
752
+ """
753
+
754
+ args = list(args)
755
+
756
+ # get sky coords
757
+ beta = args[self.index_beta]
758
+ lam = args[self.index_lambda]
759
+
760
+ # remove them from the list if waveform generator does not take them
761
+ if self.remove_sky_coords:
762
+ args.pop(self.index_beta)
763
+ args.pop(self.index_lambda)
764
+
765
+ # transform polar angle
766
+ if not self.is_ecliptic_latitude:
767
+ beta = np.pi / 2.0 - beta
768
+
769
+ # add the new Tobs and dt info to the waveform generator kwargs
770
+ kwargs["T"] = self.Tobs
771
+ kwargs["dt"] = self.dt
772
+
773
+ # get the waveform
774
+ h = self.waveform_gen(*args, **kwargs)
775
+
776
+ if self.flip_hx:
777
+ h = h.real - 1j * h.imag
778
+
779
+ self.response_model.get_projections(h, lam, beta, t0=self.t0)
780
+ tdi_out = self.response_model.get_tdi_delays()
781
+
782
+ out = list(tdi_out)
783
+ if self.remove_garbage is True: # bool
784
+ for i in range(len(out)):
785
+ out[i] = out[i][
786
+ self.response_model.tdi_start_ind : -self.response_model.tdi_start_ind
787
+ ]
788
+
789
+ elif isinstance(self.remove_garbage, str): # bool
790
+ if self.remove_garbage != "zero":
791
+ raise ValueError("remove_garbage must be True, False, or 'zero'.")
792
+ for i in range(len(out)):
793
+ out[i][: self.response_model.tdi_start_ind] = 0.0
794
+ out[i][-self.response_model.tdi_start_ind :] = 0.0
795
+
796
+ return out
@@ -0,0 +1 @@
1
+ from .utility import get_overlap
@@ -0,0 +1,81 @@
1
+ import numpy as np
2
+
3
+ try:
4
+ import cupy as cp
5
+ from pyresponse import get_response_wrap as get_response_wrap_gpu
6
+ from pyresponse import get_tdi_delays_wrap as get_tdi_delays_wrap_gpu
7
+
8
+ gpu = True
9
+
10
+ except (ImportError, ModuleNotFoundError) as e:
11
+ import numpy as xp
12
+
13
+ gpu = False
14
+
15
+
16
+ def get_overlap(sig1, sig2, phase_maximize=False, use_gpu=False):
17
+ """Calculate the mismatch across TDI channels
18
+
19
+ Calculates the overlap between two sets of TDI observables in the time
20
+ domain. The overlap is complex allowing for the addition of overlap
21
+ over all channels. It can be phase maximized as well.
22
+
23
+ This function has GPU capabilities.
24
+
25
+ Args:
26
+ sig1 (list or xp.ndarray): TDI observables for first signal. Must be ``list`` of
27
+ ``xp.ndarray`` or a single ``xp.ndarray``. Must have same length as ``sig2`` in terms
28
+ of number of channels and length of the indivudal channels.
29
+ sig2 (list or xp.ndarray): TDI observables for second signal. Must be ``list`` of
30
+ ``xp.ndarray`` or a single ``xp.ndarray``. Must have same length as ``sig1`` in terms
31
+ of number of channels and length of the individual channels.
32
+ phase_maximize (bool, optional): If ``True``, maximize over the phase in the overlap.
33
+ This is equivalent to getting the magnitude of the phasor that is the complex
34
+ overlap. (Defaut: ``False``)
35
+ use_gpu (bool, optional): If ``True``, use the GPU. This sets ``xp=cupy``. If ``False,
36
+ use the CPU and set ``xp=numpy``.
37
+
38
+ Returns:
39
+ double: Overlap as a real value.
40
+
41
+ """
42
+
43
+ # choose right array library
44
+ if use_gpu:
45
+ xp = cp
46
+ else:
47
+ xp = np
48
+
49
+ # check inputs
50
+ if not isinstance(sig1, list):
51
+ if not isinstance(sig1, xp.ndarray):
52
+ raise ValueError("sig1 must be list of or single xp.ndarray.")
53
+
54
+ elif sig1.ndim < 2:
55
+ sig1 = [sig1]
56
+
57
+ if not isinstance(sig2, list):
58
+ if not isinstance(sig2, xp.ndarray):
59
+ raise ValueError("sig1 must be list of or single xp.ndarray.")
60
+
61
+ elif sig1.ndim < 2:
62
+ sig2 = [sig2]
63
+
64
+ assert len(sig1) == len(sig2)
65
+ assert len(sig1[0]) == len(sig2[0])
66
+
67
+ # complex overlap
68
+ overlap = 0.0 + 1j * 0.0
69
+ for sig1_i, sig2_i in zip(sig1, sig2):
70
+ overlap_i = np.dot(np.fft.rfft(sig1_i).conj(), np.fft.rfft(sig2_i)) / np.sqrt(
71
+ np.dot(np.fft.rfft(sig1_i).conj(), np.fft.rfft(sig1_i))
72
+ * np.dot(np.fft.rfft(sig2_i).conj(), np.fft.rfft(sig2_i))
73
+ )
74
+
75
+ overlap += overlap_i
76
+
77
+ if phase_maximize:
78
+ return np.abs(overlap)
79
+
80
+ else:
81
+ return overlap.real
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.1
2
+ Name: fastlisaresponse
3
+ Version: 1.0.2
4
+ Home-page: https://github.com/mikekatz04/lisa-on-gpu
5
+ Author: Michael Katz
6
+ Author-email: mikekatz04@gmail.com
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: GNU General Public License (GPL)
9
+ Classifier: Environment :: GPU :: NVIDIA CUDA
10
+ Classifier: Natural Language :: English
11
+ Classifier: Programming Language :: C++
12
+ Classifier: Programming Language :: Cython
13
+ Classifier: Programming Language :: Python :: 3.7
14
+ Requires-Python: >=3.6
15
+ Description-Content-Type: text/markdown
16
+
17
+ # fastlisaresponse: Generic LISA response function for GPUs
18
+
19
+ This code base provides a GPU-accelerated version of the generic time-domain LISA response function. The GPU-acceleration allows this code to be used directly in Parameter Estimation.
20
+
21
+ Please see the [documentation](https://mikekatz04.github.io/lisa-on-gpu/) for further information on these modules. The code can be found on Github [here](https://github.com/mikekatz04/lisa-on-gpu). It can be found on # TODO fix [Zenodo](https://zenodo.org/record/3981654#.XzS_KRNKjlw).
22
+
23
+ If you use all or any parts of this code, please cite (TODO: fill in ). See the [documentation](https://mikekatz04.github.io/lisa-on-gpu/) to properly cite specific modules.
24
+
25
+ ## Getting Started
26
+
27
+ Below is a quick set of instructions to get you started with `fastlisaresponse`.
28
+
29
+ 0) [Install Anaconda](https://docs.anaconda.com/anaconda/install/) if you do not have it.
30
+
31
+ 1) Create a virtual environment. **Note**: There is no available `conda` compiler for Windows. If you want to install for Windows, you will probably need to add libraries and include paths to the `setup.py` file.
32
+
33
+ ```
34
+ conda create -n lisa_env -c conda-forge gcc_linux-64 gxx_linux-64 numpy Cython scipy jupyter ipython h5py matplotlib python=3.9
35
+ conda activate lisa_env
36
+ ```
37
+
38
+ If on MACOSX, substitute `gcc_linux-64` and `gxx_linus-64` with `clang_osx-64` and `clangxx_osx-64`.
39
+
40
+ 2) Clone the repository.
41
+
42
+ ```
43
+ git clone https://github.com/mikekatz04/lisa-on-gpu.git
44
+ cd lisa-on-gpu
45
+ ```
46
+
47
+ 3) Run install.
48
+
49
+ ```
50
+ python setup.py install
51
+ ```
52
+
53
+ 4) To import fastlisaresponse:
54
+
55
+ ```
56
+ from fastlisaresponse import ResponseWrapper
57
+ ```
58
+
59
+ See [examples notebook](https://github.com/mikekatz04/lisa-on-gpu/blob/master/examples/fast_LISA_response_tutorial.ipynb).
60
+
61
+
62
+ ### Prerequisites
63
+
64
+ To install this software for CPU usage, you need Python >3.4 and NumPy. To run the examples, you will also need jupyter and matplotlib. We generally recommend installing everything, including gcc and g++ compilers, in the conda environment as is shown in the examples here. This generally helps avoid compilation and linking issues. If you use your own chosen compiler, you will need to make sure all necessary information is passed to the setup command (see below). You also may need to add information to the `setup.py` file.
65
+
66
+ To install this software for use with NVIDIA GPUs (compute capability >2.0), you need the [CUDA toolkit](https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html) and [CuPy](https://cupy.chainer.org/). The CUDA toolkit must have cuda version >8.0. Be sure to properly install CuPy within the correct CUDA toolkit version. Make sure the nvcc binary is on `$PATH` or set it as the `CUDAHOME` environment variable.
67
+
68
+
69
+ ### Installing
70
+
71
+
72
+ 0) [Install Anaconda](https://docs.anaconda.com/anaconda/install/) if you do not have it.
73
+
74
+ 1) Create a virtual environment.
75
+
76
+ ```
77
+ conda create -n lisa_env -c conda-forge gcc_linux-64 gxx_linux-64 numpy Cython scipy jupyter ipython h5py matplotlib python=3.9
78
+ conda activate few_env
79
+ ```
80
+
81
+ If on MACOSX, substitute `gcc_linux-64` and `gxx_linus-64` with `clang_osx-64` and `clangxx_osx-64`.
82
+
83
+ If you want a faster install, you can install the python packages (numpy, Cython, scipy, tqdm, jupyter, ipython, h5py, requests, matplotlib) with pip.
84
+
85
+ 2) Clone the repository.
86
+
87
+ ```
88
+ git clone https://github.com/BlackHolePerturbationToolkit/FastEMRIWaveforms.git
89
+ cd FastEMRIWaveforms
90
+ ```
91
+
92
+ 3) If using GPUs, use pip to [install cupy](https://docs-cupy.chainer.org/en/stable/install.html). If you have cuda version 9.2, for example:
93
+
94
+ ```
95
+ pip install cupy-cuda92
96
+ ```
97
+
98
+ 4) Run install. Make sure CUDA is on your PATH.
99
+
100
+ ```
101
+ python setup.py install
102
+ ```
103
+
104
+ ## Running the Tests
105
+
106
+ Since the code package in minimal in size, the example notebook should be run to verify it is running correctly.
107
+
108
+
109
+ ## Contributing
110
+
111
+ Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
112
+
113
+ ## Versioning
114
+
115
+ We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/mikekatz04/lisa-on-gpu/tags).
116
+
117
+ Current Version: 1.0.2
118
+
119
+ ## Authors
120
+
121
+ * **Michael Katz**
122
+ * Jean-Baptiste Bayle
123
+ * Alvin J. K. Chua
124
+ * Michele Vallisneri
125
+
126
+ ### Contibutors
127
+
128
+ * Maybe you!
129
+
130
+ ## License
131
+
132
+ This project is licensed under the GNU License - see the [LICENSE.md](LICENSE.md) file for details.
133
+
134
+ ## Acknowledgments
135
+
136
+ * It was also supported in part through the computational resources and staff contributions provided for the Quest/Grail high performance computing facility at Northwestern University.
@@ -0,0 +1,13 @@
1
+ pyresponse_cpu.cpython-312-darwin.so,sha256=HzhXgvA5Pv9syEyDoQmFz3J2Cj_8U14n7vO7THMUjfo,91632
2
+ fastlisaresponse/__init__.py,sha256=wwmiIBy9IuFwoc4jQyJVkJBhjB8B1XZjerTe_E8FkI8,53
3
+ fastlisaresponse/_version.py,sha256=C8nyPP5-54GgYCcP38Lbel_pRimOW-Ra4bw6Vzp2lmE,21
4
+ fastlisaresponse/pointer_adjust.py,sha256=TjcSehyffLxwgJnrAmcFlPvxXb3XPElMoHXLBOQN-PI,736
5
+ fastlisaresponse/response.py,sha256=5czptGR1lYmQtdzD1nuvnCXPl7tAgCwBbUiVg1YMOnc,29507
6
+ fastlisaresponse/cutils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ fastlisaresponse/cutils/detector.cpython-312-darwin.so,sha256=06Bqus2cisiftaDe4eAvNXjBbYVJ7WsbkYz8b7xg0xw,122000
8
+ fastlisaresponse/utils/__init__.py,sha256=pf2NmWKs_uQNzlyA5iNO1gTRDISKNmIIsvOcKqQ3hgw,33
9
+ fastlisaresponse/utils/utility.py,sha256=uhu827ZNwMHfccD9aLRNG_yIwcfK5a3aaAJ7RSRAun4,2684
10
+ fastlisaresponse-1.0.2.dist-info/METADATA,sha256=Ix8TMUO6Ia8-RmIFAkUAcJucb1mtrcSPgmKiUjpL39E,5191
11
+ fastlisaresponse-1.0.2.dist-info/WHEEL,sha256=KYtn_mzb_QwZSHwPlosUO3fDl70znfUFngLlrLVHeBY,111
12
+ fastlisaresponse-1.0.2.dist-info/top_level.txt,sha256=xbAh3KhbfqEkGUbhVISA5j-Qx62Bwy7szKCZGg5WZWA,32
13
+ fastlisaresponse-1.0.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp312-cp312-macosx_10_9_x86_64
5
+
@@ -0,0 +1,2 @@
1
+ fastlisaresponse
2
+ pyresponse_cpu
Binary file