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
gwsim/detector/utils.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""A utility module for loading gravitational wave interferometer configurations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ast import literal_eval
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from pycbc.detector import add_detector_on_earth
|
|
10
|
+
|
|
11
|
+
# The default base path for detector configuration files
|
|
12
|
+
DEFAULT_DETECTOR_BASE_PATH = Path(__file__).parent / "detectors"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _bilby_to_pycbc_detector_parameters(bilby_params: dict) -> dict:
|
|
16
|
+
"""
|
|
17
|
+
Convert Bilby detector parameters to PyCBC-compatible parameters.
|
|
18
|
+
|
|
19
|
+
This function handles the conversion of units and conventions between Bilby and PyCBC,
|
|
20
|
+
including latitude/longitude to radians, length from km to meters, and azimuth adjustments
|
|
21
|
+
due to different reference conventions (Bilby: from East counterclockwise; PyCBC/LAL: from North clockwise).
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
bilby_params (dict): Dictionary of Bilby parameters (e.g., 'latitude', 'xarm_azimuth', etc.).
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
dict: Dictionary of converted PyCBC parameters.
|
|
28
|
+
"""
|
|
29
|
+
pycbc_params = {
|
|
30
|
+
"name": bilby_params["name"],
|
|
31
|
+
"latitude": np.deg2rad(bilby_params["latitude"]),
|
|
32
|
+
"longitude": np.deg2rad(bilby_params["longitude"]),
|
|
33
|
+
"height": bilby_params["elevation"],
|
|
34
|
+
"xangle": (np.pi / 2 - np.deg2rad(bilby_params["xarm_azimuth"])) % (2 * np.pi),
|
|
35
|
+
"yangle": (np.pi / 2 - np.deg2rad(bilby_params["yarm_azimuth"])) % (2 * np.pi),
|
|
36
|
+
"xaltitude": bilby_params["xarm_tilt"],
|
|
37
|
+
"yaltitude": bilby_params["yarm_tilt"],
|
|
38
|
+
"xlength": bilby_params["length"] * 1000,
|
|
39
|
+
"ylength": bilby_params["length"] * 1000,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return pycbc_params
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_interferometer_config(config_file: str | Path, encoding: str = "utf-8") -> str:
|
|
46
|
+
"""
|
|
47
|
+
Load a .interferometer config file and add its detector using pycbc.detector.add_detector_on_earth.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
config_file: The path to the config file.
|
|
51
|
+
encoding: The file encoding to use when reading the config file. Default is 'utf-8'.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
str: Added detector name (e.g., "E1").
|
|
55
|
+
"""
|
|
56
|
+
# Load the .interferometer config file
|
|
57
|
+
config_file = Path(config_file)
|
|
58
|
+
if not config_file.exists():
|
|
59
|
+
raise FileNotFoundError(f"Config file {config_file} not found.")
|
|
60
|
+
|
|
61
|
+
bilby_params = {}
|
|
62
|
+
with config_file.open(encoding=encoding) as f:
|
|
63
|
+
lines = f.readlines()
|
|
64
|
+
for line in lines:
|
|
65
|
+
if line[0] == "#" or line[0] == "\n":
|
|
66
|
+
continue
|
|
67
|
+
split_line = line.split("=")
|
|
68
|
+
key = split_line[0].strip()
|
|
69
|
+
if key == "power_spectral_density":
|
|
70
|
+
continue
|
|
71
|
+
value = literal_eval("=".join(split_line[1:]))
|
|
72
|
+
bilby_params[key] = value
|
|
73
|
+
|
|
74
|
+
params = _bilby_to_pycbc_detector_parameters(bilby_params)
|
|
75
|
+
det_name = params["name"]
|
|
76
|
+
|
|
77
|
+
add_detector_on_earth(
|
|
78
|
+
name=det_name,
|
|
79
|
+
latitude=params["latitude"],
|
|
80
|
+
longitude=params["longitude"],
|
|
81
|
+
height=params["height"],
|
|
82
|
+
xangle=params["xangle"],
|
|
83
|
+
yangle=params["yangle"],
|
|
84
|
+
xaltitude=params["xaltitude"],
|
|
85
|
+
yaltitude=params["yaltitude"],
|
|
86
|
+
xlength=params["xlength"],
|
|
87
|
+
ylength=params["ylength"],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return det_name
|
gwsim/glitch/__init__.py
ADDED
gwsim/glitch/base.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Base class for glitch simulators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from gwsim.mixin.detector import DetectorMixin
|
|
10
|
+
from gwsim.mixin.randomness import RandomnessMixin
|
|
11
|
+
from gwsim.mixin.time_series import TimeSeriesMixin
|
|
12
|
+
from gwsim.simulator.base import Simulator
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GlitchSimulator(TimeSeriesMixin, RandomnessMixin, DetectorMixin, Simulator):
|
|
16
|
+
"""Base class for glitch simulators."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
start_time: int = 0,
|
|
21
|
+
duration: float = 1024,
|
|
22
|
+
sampling_frequency: float = 4096,
|
|
23
|
+
max_samples: int | None = None,
|
|
24
|
+
dtype: type = np.float64,
|
|
25
|
+
seed: int | None = None,
|
|
26
|
+
detectors: list[str] | None = None,
|
|
27
|
+
**kwargs,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Initialize the base glitch simulator.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
start_time: Start time of the first glitch segment in GPS seconds. Default is 0.
|
|
33
|
+
duration: Duration of each glitch segment in seconds. Default is 1024.
|
|
34
|
+
sampling_frequency: Sampling frequency of the glitches in Hz. Default is 4096.
|
|
35
|
+
max_samples: Maximum number of samples to generate. None means infinite.
|
|
36
|
+
dtype: Data type for the time series data. Default is np.float64.
|
|
37
|
+
seed: Seed for the random number generator. If None, the RNG is not initialized.
|
|
38
|
+
detectors: List of detector names. Default is None.
|
|
39
|
+
**kwargs: Additional arguments absorbed by subclasses and mixins.
|
|
40
|
+
"""
|
|
41
|
+
super().__init__(
|
|
42
|
+
start_time=start_time,
|
|
43
|
+
duration=duration,
|
|
44
|
+
sampling_frequency=sampling_frequency,
|
|
45
|
+
max_samples=max_samples,
|
|
46
|
+
dtype=dtype,
|
|
47
|
+
seed=seed,
|
|
48
|
+
detectors=detectors,
|
|
49
|
+
**kwargs,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def metadata(self) -> dict:
|
|
54
|
+
"""Get the metadata of the simulator.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Metadata dictionary.
|
|
58
|
+
"""
|
|
59
|
+
meta = super().metadata
|
|
60
|
+
return meta
|
|
61
|
+
|
|
62
|
+
def update_state(self) -> None:
|
|
63
|
+
"""Update internal state after each sample generation.
|
|
64
|
+
|
|
65
|
+
This method can be overridden by subclasses to update any internal state
|
|
66
|
+
after generating a sample. The default implementation does nothing.
|
|
67
|
+
"""
|
|
68
|
+
self.counter = cast(int, self.counter) + 1
|
|
69
|
+
self.start_time += self.duration
|
gwsim/mixin/__init__.py
ADDED
gwsim/mixin/detector.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Detector mixin for simulators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from gwpy.timeseries import TimeSeries as GWpyTimeSeries
|
|
10
|
+
from scipy.interpolate import interp1d
|
|
11
|
+
|
|
12
|
+
from gwsim.data.time_series.time_series import TimeSeries
|
|
13
|
+
from gwsim.detector.base import Detector
|
|
14
|
+
from gwsim.detector.utils import DEFAULT_DETECTOR_BASE_PATH
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DetectorMixin: # pylint: disable=too-few-public-methods
|
|
18
|
+
"""Mixin class to add detector information to simulators."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, detectors: list[str] | None = None, **kwargs): # pylint: disable=unused-argument
|
|
21
|
+
"""Initialize the DetectorMixin.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
detectors (list[str] | None): List of detector names. If None, use all available detectors.
|
|
25
|
+
**kwargs: Additional arguments.
|
|
26
|
+
"""
|
|
27
|
+
super().__init__(**kwargs)
|
|
28
|
+
self._metadata = {"detector": {"arguments": {"detectors": detectors}}}
|
|
29
|
+
self.detectors = detectors
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def detectors(self) -> list[Detector]:
|
|
33
|
+
"""Get the list of detectors.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
List of detector names or Detector instances, or None if not set.
|
|
37
|
+
"""
|
|
38
|
+
return self._detectors
|
|
39
|
+
|
|
40
|
+
@detectors.setter
|
|
41
|
+
def detectors(self, value: list[str] | None) -> None:
|
|
42
|
+
"""Set the list of detectors.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
value (list[str | Path | Detector] | None):
|
|
46
|
+
List of detector names, config file paths, or Detector instances.
|
|
47
|
+
If None, no detectors are set.
|
|
48
|
+
"""
|
|
49
|
+
if value is None:
|
|
50
|
+
self._detectors = []
|
|
51
|
+
elif isinstance(value, list):
|
|
52
|
+
self._detectors = []
|
|
53
|
+
for det in value:
|
|
54
|
+
if Path(det).is_file() or (DEFAULT_DETECTOR_BASE_PATH / det).is_file():
|
|
55
|
+
self._detectors.append(Detector(configuration_file=det))
|
|
56
|
+
else:
|
|
57
|
+
self._detectors.append(Detector(name=str(det)))
|
|
58
|
+
else:
|
|
59
|
+
raise ValueError("detectors must be a list.")
|
|
60
|
+
|
|
61
|
+
def detectors_are_configured(self) -> bool:
|
|
62
|
+
"""Check if all detectors are configured.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if all detectors are configured, False otherwise.
|
|
66
|
+
"""
|
|
67
|
+
if all(det.is_configured() for det in self.detectors):
|
|
68
|
+
return True
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
def project_polarizations( # pylint: disable=too-many-locals,unused-argument
|
|
72
|
+
self,
|
|
73
|
+
polarizations: dict[str, GWpyTimeSeries],
|
|
74
|
+
right_ascension: float,
|
|
75
|
+
declination: float,
|
|
76
|
+
polarization_angle: float,
|
|
77
|
+
earth_rotation: bool = True,
|
|
78
|
+
**kwargs,
|
|
79
|
+
) -> TimeSeries:
|
|
80
|
+
"""Project waveform polarizations onto detectors using antenna patterns.
|
|
81
|
+
|
|
82
|
+
This method projects the plus and cross polarizations of a gravitational wave
|
|
83
|
+
onto each detector in the network, accounting for antenna response and
|
|
84
|
+
time delays.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
polarizations: Dictionary with 'plus' and 'cross' keys containing
|
|
88
|
+
TimeSeries objects of the waveform polarizations.
|
|
89
|
+
right_ascension: RA of source in radians.
|
|
90
|
+
declination: Declination of source in radians.
|
|
91
|
+
polarization_angle: Polarization angle in radians.
|
|
92
|
+
earth_rotation: If True, account for Earth's rotation by computing
|
|
93
|
+
antenna patterns at multiple times and interpolating.
|
|
94
|
+
Defaults to True.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Dictionary mapping detector names (str) to projected TimeSeries objects.
|
|
98
|
+
Keys are detector names, values are projected strain TimeSeries.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ValueError: If detectors are not configured.
|
|
102
|
+
ValueError: If polarizations dict doesn't contain 'plus' and 'cross' keys,
|
|
103
|
+
or if detector is not initialized.
|
|
104
|
+
TypeError: If polarizations values are not TimeSeries objects.
|
|
105
|
+
"""
|
|
106
|
+
# Validate the detector list
|
|
107
|
+
if not self.detectors_are_configured():
|
|
108
|
+
raise ValueError("Detectors are not configured in the simulator.")
|
|
109
|
+
|
|
110
|
+
# Validate inputs
|
|
111
|
+
if not isinstance(polarizations, dict):
|
|
112
|
+
raise TypeError("polarizations must be a dictionary")
|
|
113
|
+
if "plus" not in polarizations or "cross" not in polarizations:
|
|
114
|
+
raise ValueError("polarizations dict must contain 'plus' and 'cross' keys")
|
|
115
|
+
if not isinstance(polarizations["plus"], GWpyTimeSeries):
|
|
116
|
+
raise TypeError("polarizations['plus'] must be a GWpyTimeSeries")
|
|
117
|
+
if not isinstance(polarizations["cross"], GWpyTimeSeries):
|
|
118
|
+
raise TypeError("polarizations['cross'] must be a GWpyTimeSeries")
|
|
119
|
+
|
|
120
|
+
hp = polarizations["plus"]
|
|
121
|
+
hc = polarizations["cross"]
|
|
122
|
+
|
|
123
|
+
# Convert TimeSeries data to numpy arrays for computation
|
|
124
|
+
# Interpolate the hp and hc data to ensure smooth evaluation
|
|
125
|
+
time_array = cast(np.ndarray, hp.times.to_value())
|
|
126
|
+
reference_time = 0.5 * (time_array[0] + time_array[-1])
|
|
127
|
+
|
|
128
|
+
# Compute the time_array minus the reference_time to avoid the systematic large time values
|
|
129
|
+
time_array_wrt_reference = time_array - reference_time
|
|
130
|
+
|
|
131
|
+
hp_func = interp1d(time_array_wrt_reference, hp.to_value(), kind="cubic", bounds_error=False, fill_value=0.0)
|
|
132
|
+
hc_func = interp1d(time_array_wrt_reference, hc.to_value(), kind="cubic", bounds_error=False, fill_value=0.0)
|
|
133
|
+
|
|
134
|
+
if earth_rotation:
|
|
135
|
+
# Calculate the time delays first
|
|
136
|
+
time_delays = [
|
|
137
|
+
det.time_delay_from_earth_center(
|
|
138
|
+
right_ascension=right_ascension, declination=declination, t_gps=time_array
|
|
139
|
+
)
|
|
140
|
+
for det in self.detectors
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
# Calculate the antenna patterns at the reference time
|
|
145
|
+
reference_time = 0.5 * (time_array[0] + time_array[-1])
|
|
146
|
+
antenna_patterns = [
|
|
147
|
+
det.antenna_pattern(
|
|
148
|
+
right_ascension=right_ascension,
|
|
149
|
+
declination=declination,
|
|
150
|
+
polarization=polarization_angle,
|
|
151
|
+
t_gps=reference_time,
|
|
152
|
+
polarization_type="tensor",
|
|
153
|
+
)
|
|
154
|
+
for det in self.detectors
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
# Calculate the time delays at the reference time
|
|
158
|
+
time_delays = [
|
|
159
|
+
det.time_delay_from_earth_center(
|
|
160
|
+
right_ascension=right_ascension, declination=declination, t_gps=reference_time
|
|
161
|
+
)
|
|
162
|
+
for det in self.detectors
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
# Evaluate the detector responses
|
|
166
|
+
detector_responses = np.zeros((len(self.detectors), len(time_array)))
|
|
167
|
+
for i, det in enumerate(self.detectors):
|
|
168
|
+
time_delay = time_delays[i]
|
|
169
|
+
|
|
170
|
+
# Shift the waveform data according to time delays
|
|
171
|
+
shifted_times = time_array_wrt_reference + time_delay
|
|
172
|
+
|
|
173
|
+
if earth_rotation:
|
|
174
|
+
# Evaluate antenna patterns exactly at the delayed times
|
|
175
|
+
fp_vals, fc_vals = det.antenna_pattern(
|
|
176
|
+
right_ascension=right_ascension,
|
|
177
|
+
declination=declination,
|
|
178
|
+
polarization=polarization_angle,
|
|
179
|
+
t_gps=time_array + time_delay,
|
|
180
|
+
polarization_type="tensor",
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
# Use constant antenna patterns (from earlier calculation)
|
|
184
|
+
fp_vals, fc_vals = antenna_patterns[i]
|
|
185
|
+
|
|
186
|
+
hp_shifted = hp_func(shifted_times)
|
|
187
|
+
hc_shifted = hc_func(shifted_times)
|
|
188
|
+
|
|
189
|
+
detector_responses[i, :] = fp_vals * hp_shifted + fc_vals * hc_shifted
|
|
190
|
+
|
|
191
|
+
# Create TimeSeries for projected strain
|
|
192
|
+
start_time = cast(float, time_array[0])
|
|
193
|
+
projected_ts = TimeSeries(data=detector_responses, start_time=start_time, sampling_frequency=hp.sample_rate)
|
|
194
|
+
return projected_ts
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def metadata(self) -> dict:
|
|
198
|
+
"""Get metadata including detector information.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Dictionary containing the list of detectors.
|
|
202
|
+
"""
|
|
203
|
+
return self._metadata
|
gwsim/mixin/gwf.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""GWF (Gravitational Wave Frame) file output utilities using gwpy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from gwpy.io.gwf import write_frames
|
|
10
|
+
from gwpy.timeseries import TimeSeries
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def save_timeseries_to_gwf(
|
|
14
|
+
data: np.ndarray,
|
|
15
|
+
file_path: str | Path,
|
|
16
|
+
channel: str = "H1:STRAIN",
|
|
17
|
+
sampling_frequency: float = 4096,
|
|
18
|
+
start_time: float = 0,
|
|
19
|
+
overwrite: bool = False,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Save time series data to GWF frame file using gwpy.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
data: Time series data array.
|
|
25
|
+
file_path: Output GWF file path.
|
|
26
|
+
channel: Channel name (e.g., "H1:STRAIN"). Default is "H1:STRAIN".
|
|
27
|
+
sampling_frequency: Sampling rate in Hz. Default is 4096.
|
|
28
|
+
start_time: GPS start time. Default is 0.
|
|
29
|
+
overwrite: Whether to overwrite existing files. Default is False.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
ImportError: If gwpy is not available.
|
|
33
|
+
FileExistsError: If file exists and overwrite=False.
|
|
34
|
+
ValueError: If data is empty or parameters are invalid.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
if len(data) == 0:
|
|
38
|
+
raise ValueError("Data array cannot be empty")
|
|
39
|
+
|
|
40
|
+
if sampling_frequency <= 0:
|
|
41
|
+
raise ValueError("Sample rate must be positive")
|
|
42
|
+
|
|
43
|
+
# Convert to Path object
|
|
44
|
+
file_path = Path(file_path)
|
|
45
|
+
|
|
46
|
+
# Check if file exists
|
|
47
|
+
if file_path.exists() and not overwrite:
|
|
48
|
+
raise FileExistsError(f"File {file_path} already exists and overwrite=False")
|
|
49
|
+
|
|
50
|
+
# Create gwpy TimeSeries
|
|
51
|
+
timeseries = TimeSeries(data=data, t0=start_time, sample_rate=sampling_frequency, channel=channel, name=channel)
|
|
52
|
+
|
|
53
|
+
# Ensure output directory exists
|
|
54
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
|
|
56
|
+
# Write to GWF file
|
|
57
|
+
timeseries.write(str(file_path), format="gwf")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def combine_timeseries_to_gwf(
|
|
61
|
+
timeseries_list: list[tuple[np.ndarray, str]],
|
|
62
|
+
file_path: str | Path,
|
|
63
|
+
sampling_frequency: float = 4096,
|
|
64
|
+
start_time: float = 0,
|
|
65
|
+
overwrite: bool = False,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Combine multiple time series into a single GWF file.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
timeseries_list: List of (data, channel_name) tuples.
|
|
71
|
+
file_path: Output GWF file path.
|
|
72
|
+
sampling_frequency: Sampling rate in Hz. Default is 4096.
|
|
73
|
+
start_time: GPS start time. Default is 0.
|
|
74
|
+
overwrite: Whether to overwrite existing files. Default is False.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
ImportError: If gwpy is not available.
|
|
78
|
+
ValueError: If timeseries_list is empty or data shapes don't match.
|
|
79
|
+
"""
|
|
80
|
+
if not timeseries_list:
|
|
81
|
+
raise ValueError("timeseries_list cannot be empty")
|
|
82
|
+
|
|
83
|
+
# Convert to Path object
|
|
84
|
+
file_path = Path(file_path)
|
|
85
|
+
|
|
86
|
+
# Check if file exists
|
|
87
|
+
if file_path.exists() and not overwrite:
|
|
88
|
+
raise FileExistsError(f"File {file_path} already exists and overwrite=False")
|
|
89
|
+
|
|
90
|
+
# Validate all data arrays have the same length
|
|
91
|
+
lengths = [len(data) for data, _ in timeseries_list]
|
|
92
|
+
if not all(length == lengths[0] for length in lengths):
|
|
93
|
+
raise ValueError("All time series must have the same length")
|
|
94
|
+
|
|
95
|
+
# Create gwpy TimeSeries objects
|
|
96
|
+
gwpy_timeseries = []
|
|
97
|
+
for data, channel in timeseries_list:
|
|
98
|
+
ts = TimeSeries(data, sampling_frequency=sampling_frequency, epoch=start_time, name=channel, channel=channel)
|
|
99
|
+
gwpy_timeseries.append(ts)
|
|
100
|
+
|
|
101
|
+
# Ensure output directory exists
|
|
102
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
|
|
104
|
+
# Write multiple channels to GWF file
|
|
105
|
+
# gwpy handles multiple timeseries automatically
|
|
106
|
+
write_frames(str(file_path), gwpy_timeseries)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class GWFOutputMixin:
|
|
110
|
+
"""Mixin to add GWF file output capabilities to simulators.
|
|
111
|
+
|
|
112
|
+
This mixin provides methods for saving simulator output to GWF frame files
|
|
113
|
+
using gwpy. It assumes the simulator has sampling_frequency, start_time, and
|
|
114
|
+
generates numpy arrays.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, **kwargs) -> None:
|
|
118
|
+
"""Initialize GWFOutputMixin.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
**kwargs: Additional arguments absorbed by subclasses.
|
|
122
|
+
"""
|
|
123
|
+
super().__init__(**kwargs)
|
|
124
|
+
|
|
125
|
+
def save_to_gwf(
|
|
126
|
+
self,
|
|
127
|
+
data: np.ndarray,
|
|
128
|
+
file_path: str | Path,
|
|
129
|
+
channel: str | None = None,
|
|
130
|
+
overwrite: bool = False,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Save simulator data to GWF file.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
data: Time series data from simulator.
|
|
136
|
+
file_path: Output GWF file path.
|
|
137
|
+
channel: Channel name. If None, uses a default based on class name.
|
|
138
|
+
overwrite: Whether to overwrite existing files.
|
|
139
|
+
"""
|
|
140
|
+
# Get parameters from simulator
|
|
141
|
+
sampling_frequency = getattr(self, "sampling_frequency", None)
|
|
142
|
+
start_time = getattr(self, "start_time", 0)
|
|
143
|
+
|
|
144
|
+
if sampling_frequency is None:
|
|
145
|
+
raise ValueError("Simulator must have sampling_frequency attribute")
|
|
146
|
+
|
|
147
|
+
# Generate default channel name if not provided
|
|
148
|
+
if channel is None:
|
|
149
|
+
class_name = self.__class__.__name__
|
|
150
|
+
# Convert CamelCase to UPPER_CASE
|
|
151
|
+
channel = re.sub("([a-z0-9])([A-Z])", r"\1_\2", class_name).upper()
|
|
152
|
+
channel = f"SIM:{channel}"
|
|
153
|
+
|
|
154
|
+
save_timeseries_to_gwf(
|
|
155
|
+
data=data,
|
|
156
|
+
file_path=file_path,
|
|
157
|
+
channel=channel,
|
|
158
|
+
sampling_frequency=sampling_frequency,
|
|
159
|
+
start_time=start_time,
|
|
160
|
+
overwrite=overwrite,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def save_batch_to_gwf( # pylint: disable=unused-argument
|
|
164
|
+
self,
|
|
165
|
+
batch: list[np.ndarray] | np.ndarray,
|
|
166
|
+
file_path: str | Path,
|
|
167
|
+
channel: str | None = None,
|
|
168
|
+
overwrite: bool = False,
|
|
169
|
+
**kwargs,
|
|
170
|
+
) -> None:
|
|
171
|
+
"""Save a batch of simulator data to GWF file.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
batch: Batch of time series data (list of arrays or 2D array).
|
|
175
|
+
file_path: Output GWF file path.
|
|
176
|
+
channel: Channel name. If None, uses a default based on class name.
|
|
177
|
+
overwrite: Whether to overwrite existing files.
|
|
178
|
+
"""
|
|
179
|
+
# Flatten batch if needed
|
|
180
|
+
if isinstance(batch, list):
|
|
181
|
+
concatenated = np.concatenate(batch)
|
|
182
|
+
elif isinstance(batch, np.ndarray) and batch.ndim == 2:
|
|
183
|
+
concatenated = batch.flatten()
|
|
184
|
+
else:
|
|
185
|
+
concatenated = batch
|
|
186
|
+
|
|
187
|
+
self.save_to_gwf(
|
|
188
|
+
data=concatenated,
|
|
189
|
+
file_path=file_path,
|
|
190
|
+
channel=channel,
|
|
191
|
+
overwrite=overwrite,
|
|
192
|
+
)
|