gwsim 0.1.0__py3-none-any.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.
- gwsim/__init__.py +11 -0
- gwsim/__main__.py +8 -0
- gwsim/cli/__init__.py +0 -0
- gwsim/cli/config.py +88 -0
- gwsim/cli/default_config.py +56 -0
- gwsim/cli/main.py +101 -0
- gwsim/cli/merge.py +150 -0
- gwsim/cli/repository/__init__.py +0 -0
- gwsim/cli/repository/create.py +91 -0
- gwsim/cli/repository/delete.py +51 -0
- gwsim/cli/repository/download.py +54 -0
- gwsim/cli/repository/list_depositions.py +63 -0
- gwsim/cli/repository/main.py +38 -0
- gwsim/cli/repository/metadata/__init__.py +0 -0
- gwsim/cli/repository/metadata/main.py +24 -0
- gwsim/cli/repository/metadata/update.py +58 -0
- gwsim/cli/repository/publish.py +52 -0
- gwsim/cli/repository/upload.py +74 -0
- gwsim/cli/repository/utils.py +47 -0
- gwsim/cli/repository/verify.py +61 -0
- gwsim/cli/simulate.py +220 -0
- gwsim/cli/simulate_utils.py +596 -0
- gwsim/cli/utils/__init__.py +85 -0
- gwsim/cli/utils/checkpoint.py +178 -0
- gwsim/cli/utils/config.py +347 -0
- gwsim/cli/utils/hash.py +23 -0
- gwsim/cli/utils/retry.py +62 -0
- gwsim/cli/utils/simulation_plan.py +439 -0
- gwsim/cli/utils/template.py +56 -0
- gwsim/cli/utils/utils.py +149 -0
- gwsim/cli/validate.py +255 -0
- gwsim/data/__init__.py +8 -0
- gwsim/data/serialize/__init__.py +9 -0
- gwsim/data/serialize/decoder.py +59 -0
- gwsim/data/serialize/encoder.py +44 -0
- gwsim/data/serialize/serializable.py +33 -0
- gwsim/data/time_series/__init__.py +3 -0
- gwsim/data/time_series/inject.py +104 -0
- gwsim/data/time_series/time_series.py +355 -0
- gwsim/data/time_series/time_series_list.py +182 -0
- gwsim/detector/__init__.py +8 -0
- gwsim/detector/base.py +156 -0
- gwsim/detector/detectors/E1_2L_Aligned_Sardinia.interferometer +22 -0
- gwsim/detector/detectors/E1_2L_Misaligned_Sardinia.interferometer +22 -0
- gwsim/detector/detectors/E1_Triangle_EMR.interferometer +19 -0
- gwsim/detector/detectors/E1_Triangle_Sardinia.interferometer +19 -0
- gwsim/detector/detectors/E2_2L_Aligned_EMR.interferometer +22 -0
- gwsim/detector/detectors/E2_2L_Misaligned_EMR.interferometer +22 -0
- gwsim/detector/detectors/E2_Triangle_EMR.interferometer +19 -0
- gwsim/detector/detectors/E2_Triangle_Sardinia.interferometer +19 -0
- gwsim/detector/detectors/E3_Triangle_EMR.interferometer +19 -0
- gwsim/detector/detectors/E3_Triangle_Sardinia.interferometer +19 -0
- gwsim/detector/noise_curves/ET_10_HF_psd.txt +3000 -0
- gwsim/detector/noise_curves/ET_10_full_cryo_psd.txt +3000 -0
- gwsim/detector/noise_curves/ET_15_HF_psd.txt +3000 -0
- gwsim/detector/noise_curves/ET_15_full_cryo_psd.txt +3000 -0
- gwsim/detector/noise_curves/ET_20_HF_psd.txt +3000 -0
- gwsim/detector/noise_curves/ET_20_full_cryo_psd.txt +3000 -0
- gwsim/detector/noise_curves/ET_D_psd.txt +3000 -0
- gwsim/detector/utils.py +90 -0
- gwsim/glitch/__init__.py +7 -0
- gwsim/glitch/base.py +69 -0
- gwsim/mixin/__init__.py +8 -0
- gwsim/mixin/detector.py +203 -0
- gwsim/mixin/gwf.py +192 -0
- gwsim/mixin/population_reader.py +175 -0
- gwsim/mixin/randomness.py +107 -0
- gwsim/mixin/time_series.py +295 -0
- gwsim/mixin/waveform.py +47 -0
- gwsim/noise/__init__.py +19 -0
- gwsim/noise/base.py +134 -0
- gwsim/noise/bilby_stationary_gaussian.py +117 -0
- gwsim/noise/colored_noise.py +275 -0
- gwsim/noise/correlated_noise.py +257 -0
- gwsim/noise/pycbc_stationary_gaussian.py +112 -0
- gwsim/noise/stationary_gaussian.py +44 -0
- gwsim/noise/white_noise.py +51 -0
- gwsim/repository/__init__.py +0 -0
- gwsim/repository/zenodo.py +269 -0
- gwsim/signal/__init__.py +11 -0
- gwsim/signal/base.py +137 -0
- gwsim/signal/cbc.py +61 -0
- gwsim/simulator/__init__.py +7 -0
- gwsim/simulator/base.py +315 -0
- gwsim/simulator/state.py +85 -0
- gwsim/utils/__init__.py +11 -0
- gwsim/utils/datetime_parser.py +44 -0
- gwsim/utils/et_2l_geometry.py +165 -0
- gwsim/utils/io.py +167 -0
- gwsim/utils/log.py +145 -0
- gwsim/utils/population.py +48 -0
- gwsim/utils/random.py +69 -0
- gwsim/utils/retry.py +75 -0
- gwsim/utils/triangular_et_geometry.py +164 -0
- gwsim/version.py +7 -0
- gwsim/waveform/__init__.py +7 -0
- gwsim/waveform/factory.py +83 -0
- gwsim/waveform/pycbc_wrapper.py +37 -0
- gwsim-0.1.0.dist-info/METADATA +157 -0
- gwsim-0.1.0.dist-info/RECORD +103 -0
- gwsim-0.1.0.dist-info/WHEEL +4 -0
- gwsim-0.1.0.dist-info/entry_points.txt +2 -0
- gwsim-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Module for handling time series data for multiple channels."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from numbers import Number
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from astropy.units.quantity import Quantity
|
|
12
|
+
from gwpy.timeseries import TimeSeries as GWpyTimeSeries
|
|
13
|
+
from gwpy.types.index import Index
|
|
14
|
+
from scipy.interpolate import interp1d
|
|
15
|
+
|
|
16
|
+
from gwsim.data.serialize.serializable import JSONSerializable
|
|
17
|
+
from gwsim.data.time_series.inject import inject
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("gwsim")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from gwsim.data.time_series.time_series_list import TimeSeriesList
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TimeSeries(JSONSerializable):
|
|
27
|
+
"""Class representing a time series data for multiple channels."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, data: np.ndarray, start_time: int | float | Quantity, sampling_frequency: float | Quantity):
|
|
30
|
+
"""Initialize the TimeSeries with a list of GWPy TimeSeries objects.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
data: 2D numpy array of shape (num_of_channels, num_samples) containing the time series data.
|
|
34
|
+
start_time: Start time of the time series in GPS seconds.
|
|
35
|
+
sampling_frequency: Sampling frequency of the time series in Hz.
|
|
36
|
+
"""
|
|
37
|
+
if data.ndim != 2:
|
|
38
|
+
raise ValueError("Data must be a 2D numpy array with shape (num_of_channels, num_samples).")
|
|
39
|
+
|
|
40
|
+
if isinstance(start_time, Number):
|
|
41
|
+
start_time = Quantity(start_time, unit="s")
|
|
42
|
+
if isinstance(sampling_frequency, (int, float)):
|
|
43
|
+
sampling_frequency = Quantity(sampling_frequency, unit="Hz")
|
|
44
|
+
|
|
45
|
+
self._data: list[GWpyTimeSeries] = [
|
|
46
|
+
GWpyTimeSeries(
|
|
47
|
+
data=data[i],
|
|
48
|
+
t0=start_time,
|
|
49
|
+
sample_rate=sampling_frequency,
|
|
50
|
+
)
|
|
51
|
+
for i in range(data.shape[0])
|
|
52
|
+
]
|
|
53
|
+
self.num_of_channels = data.shape[0]
|
|
54
|
+
self.dtype = data.dtype
|
|
55
|
+
self.metadata = {}
|
|
56
|
+
|
|
57
|
+
def __len__(self) -> int:
|
|
58
|
+
"""Get the number of channels in the time series.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Number of channels in the time series.
|
|
62
|
+
"""
|
|
63
|
+
return self.num_of_channels
|
|
64
|
+
|
|
65
|
+
def __getitem__(self, index: int) -> GWpyTimeSeries:
|
|
66
|
+
"""Get the GWPy TimeSeries object for a specific channel.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
index: Index of the channel to retrieve.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
GWPy TimeSeries object for the specified channel.
|
|
73
|
+
"""
|
|
74
|
+
return self._data[index]
|
|
75
|
+
|
|
76
|
+
def __setitem__(self, index: int, value: GWpyTimeSeries) -> None:
|
|
77
|
+
"""Set the GWPy TimeSeries object for a specific channel.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
index: Index of the channel to set.
|
|
81
|
+
value: GWPy TimeSeries object to set for the specified channel.
|
|
82
|
+
"""
|
|
83
|
+
# First check whether the start time and sampling frequency match
|
|
84
|
+
if value.t0 != self.start_time:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
"Start time of the provided TimeSeries does not match."
|
|
87
|
+
f"The start time of this instance is {self.start_time}, "
|
|
88
|
+
f"while that of the provided TimeSeries is {value.t0}."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Debug: log the sampling frequencies
|
|
92
|
+
logger.debug(
|
|
93
|
+
"Assigning to channel %d: value.sample_rate=%.15f, self.sampling_frequency=%.15f",
|
|
94
|
+
index,
|
|
95
|
+
float(value.sample_rate.value),
|
|
96
|
+
float(self.sampling_frequency.value),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if value.sample_rate != self.sampling_frequency:
|
|
100
|
+
# Additional debug info
|
|
101
|
+
logger.warning(
|
|
102
|
+
"Sampling frequency mismatch on channel %d. "
|
|
103
|
+
"Difference: %.15e Hz. "
|
|
104
|
+
"Value times: %s to %s (%d samples, dt=%.15f). "
|
|
105
|
+
"Self times span should match.",
|
|
106
|
+
index,
|
|
107
|
+
float(value.sample_rate.value) - float(self.sampling_frequency.value),
|
|
108
|
+
value.times[0],
|
|
109
|
+
value.times[-1],
|
|
110
|
+
len(value),
|
|
111
|
+
float(value.dt.value),
|
|
112
|
+
)
|
|
113
|
+
raise ValueError(
|
|
114
|
+
"Sampling frequency of the provided TimeSeries does not match."
|
|
115
|
+
f"The sampling frequency of this instance is {self.sampling_frequency}, "
|
|
116
|
+
f"while that of the provided TimeSeries is {value.sample_rate}."
|
|
117
|
+
)
|
|
118
|
+
# Check the duration
|
|
119
|
+
if value.duration != self.duration:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"Duration of the provided TimeSeries does not match."
|
|
122
|
+
f"The duration of this instance is {self.duration}, "
|
|
123
|
+
f"while that of the provided TimeSeries is {value.duration}."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if not isinstance(value, GWpyTimeSeries):
|
|
127
|
+
raise TypeError(f"Value must be a GWpy TimeSeries instance, got {type(value)}")
|
|
128
|
+
|
|
129
|
+
self._data[index] = value
|
|
130
|
+
|
|
131
|
+
def __iter__(self):
|
|
132
|
+
"""Iterate over the channels in the time series.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Iterator over the GWPy TimeSeries objects in the time series.
|
|
136
|
+
"""
|
|
137
|
+
return iter(self._data)
|
|
138
|
+
|
|
139
|
+
def __eq__(self, other: object) -> bool:
|
|
140
|
+
"""Check equality with another TimeSeries object.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
other: Another TimeSeries object to compare with.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
True if the two TimeSeries objects are equal, False otherwise.
|
|
147
|
+
"""
|
|
148
|
+
if not isinstance(other, TimeSeries):
|
|
149
|
+
return False
|
|
150
|
+
if self.num_of_channels != other.num_of_channels:
|
|
151
|
+
return False
|
|
152
|
+
for i in range(self.num_of_channels):
|
|
153
|
+
if not np.array_equal(self[i].value, other[i].value):
|
|
154
|
+
return False
|
|
155
|
+
if self[i].t0 != other[i].t0:
|
|
156
|
+
return False
|
|
157
|
+
if self[i].sample_rate != other[i].sample_rate:
|
|
158
|
+
return False
|
|
159
|
+
return True
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def shape(self) -> tuple[int, int]:
|
|
163
|
+
"""Get the shape of the time series data.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Tuple representing the shape of the time series data (num_of_channels, num_samples).
|
|
167
|
+
"""
|
|
168
|
+
return (self.num_of_channels, self[0].size)
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def start_time(self) -> Quantity:
|
|
172
|
+
"""Get the start time of the time series.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Start time of the time series.
|
|
176
|
+
"""
|
|
177
|
+
return Quantity(self._data[0].t0)
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def duration(self) -> Quantity:
|
|
181
|
+
"""Get the duration of the time series.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Duration of the time series.
|
|
185
|
+
"""
|
|
186
|
+
return Quantity(self._data[0].duration)
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def end_time(self) -> Quantity:
|
|
190
|
+
"""Get the end time of the time series.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
End time of the time series.
|
|
194
|
+
"""
|
|
195
|
+
end_time: Quantity = self.start_time + self.duration
|
|
196
|
+
return end_time
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def sampling_frequency(self) -> Quantity:
|
|
200
|
+
"""Get the sampling frequency of the time series.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Sampling frequency of the time series.
|
|
204
|
+
"""
|
|
205
|
+
return Quantity(self._data[0].sample_rate)
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def time_array(self) -> Index:
|
|
209
|
+
"""Get the time array of the time series.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
Time array of the time series.
|
|
213
|
+
"""
|
|
214
|
+
return self[0].times
|
|
215
|
+
|
|
216
|
+
def crop(
|
|
217
|
+
self,
|
|
218
|
+
start_time: Quantity | None = None,
|
|
219
|
+
end_time: Quantity | None = None,
|
|
220
|
+
) -> TimeSeries:
|
|
221
|
+
"""Crop the time series to the specified start and end times.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
start_time: Start time of the cropped segment in GPS seconds. If None, use the
|
|
225
|
+
original start time.
|
|
226
|
+
end_time: End time of the cropped segment in GPS seconds. If None, use the
|
|
227
|
+
original end time.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Cropped TimeSeries instance.
|
|
231
|
+
"""
|
|
232
|
+
for i in range(self.num_of_channels):
|
|
233
|
+
self._data[i] = GWpyTimeSeries(self._data[i].crop(start=start_time, end=end_time, copy=True))
|
|
234
|
+
return self
|
|
235
|
+
|
|
236
|
+
def inject(self, other: TimeSeries) -> TimeSeries | None:
|
|
237
|
+
"""Inject another TimeSeries into the current TimeSeries.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
other: TimeSeries instance to inject.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Remaining TimeSeries instance if the injected TimeSeries extends beyond the current
|
|
244
|
+
TimeSeries end time, otherwise None.
|
|
245
|
+
"""
|
|
246
|
+
if len(other) != len(self):
|
|
247
|
+
raise ValueError("Number of channels in chunk must match number of channels in segment.")
|
|
248
|
+
|
|
249
|
+
# Enforce that other has the same sampling frequency as self
|
|
250
|
+
if not other.sampling_frequency == self.sampling_frequency:
|
|
251
|
+
raise ValueError(
|
|
252
|
+
f"Sampling frequency of chunk ({other.sampling_frequency}) must match "
|
|
253
|
+
f"sampling frequency of segment ({self.sampling_frequency}). "
|
|
254
|
+
"This ensures time grid alignment and avoids rounding errors."
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if other.end_time < self.start_time:
|
|
258
|
+
logger.warning(
|
|
259
|
+
"The time series to inject ends before the current time series starts. No injection performed."
|
|
260
|
+
"The start time of this segment is %s, while the end time of the other segment is %s",
|
|
261
|
+
self.start_time,
|
|
262
|
+
other.end_time,
|
|
263
|
+
)
|
|
264
|
+
return other
|
|
265
|
+
|
|
266
|
+
if other.start_time > self.end_time:
|
|
267
|
+
logger.warning(
|
|
268
|
+
"The time series to inject starts after the current time series ends. No injection performed."
|
|
269
|
+
"The end time of this segment is %s, while the start time of the other segment is %s",
|
|
270
|
+
self.end_time,
|
|
271
|
+
other.start_time,
|
|
272
|
+
)
|
|
273
|
+
return other
|
|
274
|
+
|
|
275
|
+
# Check whether there is any offset in times
|
|
276
|
+
other_start_time = other.start_time.to(self.start_time.unit)
|
|
277
|
+
idx = ((other_start_time - self.start_time) * self.sampling_frequency).value
|
|
278
|
+
if not np.isclose(idx, np.round(idx)):
|
|
279
|
+
logger.warning("Chunk time grid does not align with segment time grid.")
|
|
280
|
+
logger.warning("Interpolation will be used to align the chunk to the segment grid.")
|
|
281
|
+
|
|
282
|
+
other_end_time = other.end_time.to(self.start_time.unit)
|
|
283
|
+
other_new_times = self.time_array.value[
|
|
284
|
+
(self.time_array.value >= other_start_time.value) & (self.time_array.value <= other_end_time.value)
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
other = TimeSeries(
|
|
288
|
+
data=np.array(
|
|
289
|
+
[
|
|
290
|
+
interp1d(
|
|
291
|
+
other.time_array.value, other[i].value, kind="linear", bounds_error=False, fill_value=0.0
|
|
292
|
+
)(other_new_times)
|
|
293
|
+
for i in range(len(other))
|
|
294
|
+
]
|
|
295
|
+
),
|
|
296
|
+
start_time=Quantity(other_new_times[0], unit=self.start_time.unit),
|
|
297
|
+
sampling_frequency=self.sampling_frequency,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
for i in range(self.num_of_channels):
|
|
301
|
+
self[i] = inject(self[i], other[i])
|
|
302
|
+
|
|
303
|
+
if other.end_time > self.end_time:
|
|
304
|
+
return other.crop(start_time=self.end_time)
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
def inject_from_list(self, ts_iterable: Iterable[TimeSeries]) -> TimeSeriesList:
|
|
308
|
+
"""Inject multiple TimeSeries from an iterable into the current TimeSeries.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
ts_iterable: Iterable of TimeSeries instances to inject.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
TimeSeriesList of remaining TimeSeries instances that extend beyond the current TimeSeries end time.
|
|
315
|
+
"""
|
|
316
|
+
from gwsim.data.time_series.time_series_list import TimeSeriesList # pylint: disable=import-outside-toplevel
|
|
317
|
+
|
|
318
|
+
remaining_ts: list[TimeSeries] = []
|
|
319
|
+
for ts in ts_iterable:
|
|
320
|
+
remaining_chunk = self.inject(ts)
|
|
321
|
+
if remaining_chunk is not None:
|
|
322
|
+
remaining_ts.append(remaining_chunk)
|
|
323
|
+
return TimeSeriesList(remaining_ts)
|
|
324
|
+
|
|
325
|
+
def to_json_dict(self) -> dict:
|
|
326
|
+
"""Convert the TimeSeries to a JSON-serializable dictionary.
|
|
327
|
+
|
|
328
|
+
Assume the unit
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
JSON-serializable dictionary representation of the TimeSeries.
|
|
332
|
+
"""
|
|
333
|
+
return {
|
|
334
|
+
"__type__": "TimeSeries",
|
|
335
|
+
"data": [self[i].value.tolist() for i in range(self.num_of_channels)],
|
|
336
|
+
"start_time": self.start_time.value,
|
|
337
|
+
"start_time_unit": str(self.start_time.unit),
|
|
338
|
+
"sampling_frequency": self.sampling_frequency.value,
|
|
339
|
+
"sampling_frequency_unit": str(self.sampling_frequency.unit),
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@classmethod
|
|
343
|
+
def from_json_dict(cls, json_dict: dict) -> TimeSeries:
|
|
344
|
+
"""Create a TimeSeries object from a JSON-serializable dictionary.
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
json_dict: JSON-serializable dictionary representation of the TimeSeries.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
TimeSeries: An instance of the TimeSeries class created from the dictionary.
|
|
351
|
+
"""
|
|
352
|
+
data = np.array(json_dict["data"])
|
|
353
|
+
start_time = Quantity(json_dict["start_time"], unit=json_dict["start_time_unit"])
|
|
354
|
+
sampling_frequency = Quantity(json_dict["sampling_frequency"], unit=json_dict["sampling_frequency_unit"])
|
|
355
|
+
return cls(data=data, start_time=start_time, sampling_frequency=sampling_frequency)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Module defining TimeSeriesList, a list-like container for TimeSeries objects with validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Iterator
|
|
6
|
+
from typing import Any, cast, overload
|
|
7
|
+
|
|
8
|
+
from gwsim.data.time_series.time_series import TimeSeries
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TimeSeriesList(Iterable[TimeSeries]):
|
|
12
|
+
"""List of TimeSeries objects with validation."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, iterable: list[TimeSeries] | None = None):
|
|
15
|
+
"""List of TimeSeries objects with validation.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
iterable: Optional list of TimeSeries objects to initialize the list.
|
|
19
|
+
"""
|
|
20
|
+
self._data: list[TimeSeries] = []
|
|
21
|
+
|
|
22
|
+
if iterable is not None:
|
|
23
|
+
self._validate_items(iterable)
|
|
24
|
+
self._data.extend(iterable)
|
|
25
|
+
|
|
26
|
+
def _validate_items(self, items: Iterable[Any]) -> None:
|
|
27
|
+
"""Validate that all items are TimeSeries instances.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
items: Iterable of items to validate.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
TypeError: If any item is not a TimeSeries instance.
|
|
34
|
+
"""
|
|
35
|
+
for item in items:
|
|
36
|
+
if not isinstance(item, TimeSeries):
|
|
37
|
+
raise TypeError(f"All items must be TimeSeries instances, got {type(item)}")
|
|
38
|
+
|
|
39
|
+
@overload
|
|
40
|
+
def __setitem__(self, index: int, value: TimeSeries) -> None: ...
|
|
41
|
+
|
|
42
|
+
@overload
|
|
43
|
+
def __setitem__(self, index: slice, value: Iterable[TimeSeries]) -> None: ...
|
|
44
|
+
|
|
45
|
+
def __setitem__(self, index: int | slice, value: TimeSeries | Iterable[TimeSeries]) -> None:
|
|
46
|
+
"""Set item with validation.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
index: Index or slice to set.
|
|
50
|
+
value: TimeSeries object or list of TimeSeries objects to set.
|
|
51
|
+
"""
|
|
52
|
+
if isinstance(index, slice):
|
|
53
|
+
items = list(value) if not isinstance(value, list) else value
|
|
54
|
+
self._validate_items(items)
|
|
55
|
+
self._data[index] = cast(list[TimeSeries], items)
|
|
56
|
+
elif isinstance(index, int):
|
|
57
|
+
if not isinstance(value, TimeSeries):
|
|
58
|
+
raise TypeError(f"Value must be a TimeSeries instance, got {type(value)}")
|
|
59
|
+
self._data[index] = value
|
|
60
|
+
else:
|
|
61
|
+
raise TypeError("Index must be an int or slice.")
|
|
62
|
+
|
|
63
|
+
@overload
|
|
64
|
+
def __getitem__(self, index: int) -> TimeSeries: ...
|
|
65
|
+
|
|
66
|
+
@overload
|
|
67
|
+
def __getitem__(self, index: slice) -> list[TimeSeries]: ...
|
|
68
|
+
|
|
69
|
+
def __getitem__(self, index: int | slice) -> TimeSeries | list[TimeSeries]:
|
|
70
|
+
"""Get item.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
index: Index or slice to get.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
TimeSeries object or list of TimeSeries objects.
|
|
77
|
+
"""
|
|
78
|
+
return self._data[index]
|
|
79
|
+
|
|
80
|
+
def __len__(self) -> int:
|
|
81
|
+
"""Get the number of TimeSeries objects in the list.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Number of TimeSeries objects in the list.
|
|
85
|
+
"""
|
|
86
|
+
return len(self._data)
|
|
87
|
+
|
|
88
|
+
def __iter__(self) -> Iterator[TimeSeries]:
|
|
89
|
+
"""Iterate over the TimeSeries objects in the list.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Iterator over the TimeSeries objects in the list.
|
|
93
|
+
"""
|
|
94
|
+
return iter(self._data)
|
|
95
|
+
|
|
96
|
+
def append(self, value: TimeSeries) -> None:
|
|
97
|
+
"""Append a TimeSeries object to the list.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
value: TimeSeries object to append.
|
|
101
|
+
"""
|
|
102
|
+
if not isinstance(value, TimeSeries):
|
|
103
|
+
raise TypeError(f"Value must be a TimeSeries instance, got {type(value)}")
|
|
104
|
+
self._data.append(value)
|
|
105
|
+
|
|
106
|
+
def extend(self, iterable: Iterable[TimeSeries]) -> None:
|
|
107
|
+
"""Extend the list with TimeSeries objects from an iterable.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
iterable: Iterable of TimeSeries objects to extend the list.
|
|
111
|
+
"""
|
|
112
|
+
items = list(iterable)
|
|
113
|
+
self._validate_items(items)
|
|
114
|
+
self._data.extend(items)
|
|
115
|
+
|
|
116
|
+
def insert(self, index: int, value: TimeSeries) -> None:
|
|
117
|
+
"""Insert a TimeSeries object at a specific index.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
index: Index to insert at.
|
|
121
|
+
value: TimeSeries object to insert.
|
|
122
|
+
"""
|
|
123
|
+
if not isinstance(value, TimeSeries):
|
|
124
|
+
raise TypeError(f"Value must be a TimeSeries instance, got {type(value)}")
|
|
125
|
+
self._data.insert(index, value)
|
|
126
|
+
|
|
127
|
+
def pop(self, index: int = -1) -> TimeSeries:
|
|
128
|
+
"""Pop item at index.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
index: Index to pop. Defaults to -1 (last item).
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
TimeSeries object that was popped.
|
|
135
|
+
"""
|
|
136
|
+
return self._data.pop(index)
|
|
137
|
+
|
|
138
|
+
def to_json_dict(self) -> dict[str, Any]:
|
|
139
|
+
"""Convert the TimeSeriesList to a JSON-serializable dictionary.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
JSON-serializable dictionary representation of the TimeSeriesList.
|
|
143
|
+
"""
|
|
144
|
+
return {
|
|
145
|
+
"__type__": "TimeSeriesList",
|
|
146
|
+
"data": self._data,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def from_json_dict(cls, data: dict[str, Any]) -> TimeSeriesList:
|
|
151
|
+
"""Reconstruct a TimeSeriesList instance from a JSON dictionary.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
data: Dictionary with keys: 'data' containing list of TimeSeries dicts.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Reconstructed TimeSeriesList instance.
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
ValueError: If required keys are missing or data format is invalid.
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
items = data["data"]
|
|
164
|
+
except KeyError as e:
|
|
165
|
+
raise ValueError(f"Missing required key in JSON dict: {e}") from e
|
|
166
|
+
|
|
167
|
+
ts_list: list[TimeSeries] = []
|
|
168
|
+
for item in items:
|
|
169
|
+
if isinstance(item, TimeSeries):
|
|
170
|
+
ts_list.append(item)
|
|
171
|
+
else:
|
|
172
|
+
raise TypeError(f"Invalid item in TimeSeriesList JSON data: {type(item)}")
|
|
173
|
+
|
|
174
|
+
return cls(ts_list)
|
|
175
|
+
|
|
176
|
+
def __repr__(self) -> str:
|
|
177
|
+
"""Get the string representation of the TimeSeriesList.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
String representation of the TimeSeriesList.
|
|
181
|
+
"""
|
|
182
|
+
return f"TimeSeriesList({self._data!r})"
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Detector module for GWSim."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from gwsim.detector.base import Detector
|
|
6
|
+
from gwsim.detector.utils import DEFAULT_DETECTOR_BASE_PATH, load_interferometer_config
|
|
7
|
+
|
|
8
|
+
__all__ = ["DEFAULT_DETECTOR_BASE_PATH", "Detector", "load_interferometer_config"]
|
gwsim/detector/base.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""A module to handle gravitational wave detector configurations,"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
from pycbc.detector import Detector as PyCBCDetector
|
|
10
|
+
|
|
11
|
+
from gwsim.detector.utils import DEFAULT_DETECTOR_BASE_PATH, load_interferometer_config
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("gwsim")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load_all_detector_configuration_files(config_dir: Path = DEFAULT_DETECTOR_BASE_PATH) -> None:
|
|
17
|
+
# Glob all files in the config_dir with .interferometer extension
|
|
18
|
+
for config_file in config_dir.glob("*.interferometer"):
|
|
19
|
+
try:
|
|
20
|
+
load_interferometer_config(config_file)
|
|
21
|
+
logger.debug("Loaded detector configuration from %s", config_file)
|
|
22
|
+
except (OSError, ValueError) as e:
|
|
23
|
+
logger.warning("Failed to load detector configuration from %s: %s", config_file, e)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_load_all_detector_configuration_files(config_dir=DEFAULT_DETECTOR_BASE_PATH)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Detector:
|
|
30
|
+
"""A wrapper class around pycbc.detector.Detector that
|
|
31
|
+
handles custom detector configurations from .interferometer files
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, name: str | None = None, configuration_file: str | Path | None = None):
|
|
35
|
+
"""
|
|
36
|
+
Initialize Detector class.
|
|
37
|
+
If `detector_name` is a built-in PyCBC detector, use it directly.
|
|
38
|
+
Otherwise, load from the corresponding .interferometer config file.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
detector_name (str): The detector name or config name (e.g., 'V1' or 'E1_Triangle_Sardinia').
|
|
42
|
+
config_dir (str, optional): Directory where .interferometer files are stored (default: detectors_dir).
|
|
43
|
+
"""
|
|
44
|
+
self._metadata = {
|
|
45
|
+
"arguments": {
|
|
46
|
+
"name": name,
|
|
47
|
+
"configuration_file": configuration_file,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if name is not None and configuration_file is None:
|
|
51
|
+
try:
|
|
52
|
+
self._detector = PyCBCDetector(str(name))
|
|
53
|
+
self.name = str(name)
|
|
54
|
+
except ValueError as e:
|
|
55
|
+
logger.warning("Detector name '%s' not found in PyCBC: %s", name, e)
|
|
56
|
+
logger.warning("Setting up detector with no configuration.")
|
|
57
|
+
self._detector = None
|
|
58
|
+
self.name = str(name)
|
|
59
|
+
elif name is None and configuration_file is not None:
|
|
60
|
+
configuration_file = Path(configuration_file)
|
|
61
|
+
|
|
62
|
+
if configuration_file.is_file():
|
|
63
|
+
|
|
64
|
+
logger.debug("Loading detector from configuration file: %s", configuration_file)
|
|
65
|
+
|
|
66
|
+
prefix = load_interferometer_config(config_file=configuration_file)
|
|
67
|
+
|
|
68
|
+
elif (DEFAULT_DETECTOR_BASE_PATH / configuration_file).is_file():
|
|
69
|
+
|
|
70
|
+
logger.debug("Loading detector from default path: %s", configuration_file)
|
|
71
|
+
|
|
72
|
+
prefix = load_interferometer_config(config_file=DEFAULT_DETECTOR_BASE_PATH / configuration_file)
|
|
73
|
+
else:
|
|
74
|
+
raise FileNotFoundError(f"Configuration file '{configuration_file}' not found.")
|
|
75
|
+
self._detector = PyCBCDetector(prefix)
|
|
76
|
+
self.name = prefix
|
|
77
|
+
elif name is not None and configuration_file is not None:
|
|
78
|
+
raise ValueError("Specify either 'name' or 'configuration_file', not both.")
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError("Either 'name' or 'configuration_file' must be provided.")
|
|
81
|
+
|
|
82
|
+
self.configuration_file = configuration_file
|
|
83
|
+
|
|
84
|
+
def is_configured(self) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Check if the detector is properly configured.
|
|
87
|
+
"""
|
|
88
|
+
return self._detector is not None
|
|
89
|
+
|
|
90
|
+
def antenna_pattern(
|
|
91
|
+
self, right_ascension, declination, polarization, t_gps, frequency=0, polarization_type="tensor"
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
Return the antenna pattern for the detector.
|
|
95
|
+
"""
|
|
96
|
+
if not self.is_configured():
|
|
97
|
+
raise ValueError(f"Detector '{self.name}' is not configured.")
|
|
98
|
+
detector = cast(PyCBCDetector, self._detector)
|
|
99
|
+
return detector.antenna_pattern(right_ascension, declination, polarization, t_gps, frequency, polarization_type)
|
|
100
|
+
|
|
101
|
+
def time_delay_from_earth_center(self, right_ascension, declination, t_gps):
|
|
102
|
+
"""
|
|
103
|
+
Return the time delay from the Earth center for the detector.
|
|
104
|
+
"""
|
|
105
|
+
if not self.is_configured():
|
|
106
|
+
raise ValueError(f"Detector '{self.name}' is not configured.")
|
|
107
|
+
detector = cast(PyCBCDetector, self._detector)
|
|
108
|
+
return detector.time_delay_from_earth_center(right_ascension, declination, t_gps)
|
|
109
|
+
|
|
110
|
+
def __getattr__(self, attr):
|
|
111
|
+
"""
|
|
112
|
+
Delegate attributes to the underlying _detector.
|
|
113
|
+
"""
|
|
114
|
+
return getattr(self._detector, attr)
|
|
115
|
+
|
|
116
|
+
def __str__(self) -> str:
|
|
117
|
+
"""
|
|
118
|
+
Return a string representation of the detector name, stripped to the base part.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
str: The detector name.
|
|
122
|
+
"""
|
|
123
|
+
return self.name
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
"""
|
|
127
|
+
Return a detailed string representation of the Detector instance.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
str: A string representation of the Detector instance.
|
|
131
|
+
"""
|
|
132
|
+
return f"Detector(name={self.name}, configured={self.is_configured()})"
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def get_detector(name: str | Path) -> Detector:
|
|
136
|
+
"""A helper function to get a Detector instance or return the name string.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
name: Name of the detector (e.g., 'H1', 'L1') or configuration.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Detector instance if loading is successful, otherwise returns the name string.
|
|
143
|
+
"""
|
|
144
|
+
# First check if name corresponds to a configuration file
|
|
145
|
+
if Path(name).is_file() or (DEFAULT_DETECTOR_BASE_PATH / name).is_file():
|
|
146
|
+
return Detector(configuration_file=name)
|
|
147
|
+
return Detector(name=str(name))
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def metadata(self) -> dict:
|
|
151
|
+
"""Get a dictionary of metadata.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
dict: A dictionary of metadata.
|
|
155
|
+
"""
|
|
156
|
+
return self._metadata
|