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,175 @@
|
|
|
1
|
+
"""Population reader mixin for simulators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import h5py
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("gwsim")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PopulationIterationState: # pylint: disable=too-few-public-methods
|
|
17
|
+
"""Manages state for population file iteration with checkpoint support."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, checkpoint_file: str | Path | None = None, encoding: str = "utf-8") -> None:
|
|
20
|
+
self.checkpoint_file = checkpoint_file
|
|
21
|
+
self.encoding = encoding
|
|
22
|
+
self.current_index = 0
|
|
23
|
+
self.injected_indices: list[int] = []
|
|
24
|
+
self.segment_map: dict[int, list[int]] = {}
|
|
25
|
+
self._load_checkpoint()
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def checkpoint_file(self) -> Path | None:
|
|
29
|
+
"""Get the checkpoint file path.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Path to the checkpoint file or None if not set.
|
|
33
|
+
"""
|
|
34
|
+
return self._checkpoint_file
|
|
35
|
+
|
|
36
|
+
@checkpoint_file.setter
|
|
37
|
+
def checkpoint_file(self, value: str | Path | None) -> None:
|
|
38
|
+
"""Set the checkpoint file path.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
value: Path to the checkpoint file or None to unset.
|
|
42
|
+
"""
|
|
43
|
+
if value is None:
|
|
44
|
+
self._checkpoint_file = None
|
|
45
|
+
else:
|
|
46
|
+
self._checkpoint_file = Path(value)
|
|
47
|
+
|
|
48
|
+
def _load_checkpoint(self) -> None:
|
|
49
|
+
if self.checkpoint_file and self.checkpoint_file.is_file():
|
|
50
|
+
try:
|
|
51
|
+
with open(self.checkpoint_file, encoding=self.encoding) as f:
|
|
52
|
+
data = yaml.safe_load(f)["population"]
|
|
53
|
+
self.current_index = data.get("current_index", 0)
|
|
54
|
+
self.injected_indices = data.get("injected_indices", [])
|
|
55
|
+
self.segment_map = data.get("segment_map", {})
|
|
56
|
+
logger.info(
|
|
57
|
+
"Loaded checkpoint: current_index=%s, injected=%s",
|
|
58
|
+
self.current_index,
|
|
59
|
+
self.injected_indices,
|
|
60
|
+
)
|
|
61
|
+
except (OSError, yaml.YAMLError, KeyError) as e:
|
|
62
|
+
logger.warning("Failed to load checkpoint %s: %s. Starting fresh.", self.checkpoint_file, e)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
PARAMETER_NAME_MAPPER = {
|
|
66
|
+
"pycbc": {
|
|
67
|
+
"ra": "right_ascension",
|
|
68
|
+
"dec": "declination",
|
|
69
|
+
"polarization": "polarization_angle",
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class PopulationReaderMixin: # pylint: disable=too-few-public-methods
|
|
75
|
+
"""A mixin class to read population files for GW signal simulators."""
|
|
76
|
+
|
|
77
|
+
population_counter = 0
|
|
78
|
+
|
|
79
|
+
def __init__(self, population_file: str | Path, population_file_type: str = "pycbc", **kwargs):
|
|
80
|
+
"""Initialize the PopulationReaderMixin.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
population_file: Path to the population file.
|
|
84
|
+
population_file_type: Type of the population file. Default is 'pycbc'.
|
|
85
|
+
"""
|
|
86
|
+
super().__init__(**kwargs)
|
|
87
|
+
self.population_file = Path(population_file)
|
|
88
|
+
self.population_file_type = population_file_type
|
|
89
|
+
if not self.population_file.is_file():
|
|
90
|
+
raise FileNotFoundError(f"Population file {self.population_file} does not exist.")
|
|
91
|
+
|
|
92
|
+
if population_file_type == "pycbc":
|
|
93
|
+
self.population_data = self._read_pycbc_population_file(self.population_file, **kwargs)
|
|
94
|
+
else:
|
|
95
|
+
raise ValueError(f"Unsupported population file type: {population_file_type}")
|
|
96
|
+
|
|
97
|
+
def _read_pycbc_population_file( # pylint: disable=unused-argument
|
|
98
|
+
self, file_name: str | Path, **kwargs
|
|
99
|
+
) -> pd.DataFrame:
|
|
100
|
+
"""Read a pycbc population file in HDF5 format.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
file_name: Path to the pycbc population file.
|
|
104
|
+
**kwargs: Additional arguments (not used currently).
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
A pandas DataFrame containing the population data.
|
|
108
|
+
"""
|
|
109
|
+
# Load the pycbc population file and create a pandas DataFrame
|
|
110
|
+
|
|
111
|
+
with h5py.File(file_name, "r") as f:
|
|
112
|
+
data = {key: value[()] for key, value in f.items()}
|
|
113
|
+
|
|
114
|
+
# Create a DataFrame
|
|
115
|
+
population_data = pd.DataFrame(data)
|
|
116
|
+
|
|
117
|
+
# Save the attributes to metadata
|
|
118
|
+
attrs = dict(f.attrs.items())
|
|
119
|
+
# If there is any numpy array in attrs, convert it to list
|
|
120
|
+
for key, value in attrs.items():
|
|
121
|
+
if isinstance(value, np.ndarray):
|
|
122
|
+
attrs[key] = value.tolist()
|
|
123
|
+
self._population_metadata = attrs
|
|
124
|
+
|
|
125
|
+
# Order the DataFrame by the coalescence time 'tc'
|
|
126
|
+
return (
|
|
127
|
+
population_data.sort_values(by="tc")
|
|
128
|
+
.reset_index(drop=True)
|
|
129
|
+
.rename(columns=PARAMETER_NAME_MAPPER.get("pycbc", {}))
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def get_next_injection_parameters(self) -> dict[str, float | int] | None:
|
|
133
|
+
"""Get the next set of injection parameters from the population.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
A dictionary of injection parameters for the next event,
|
|
137
|
+
or None if all events have been used.
|
|
138
|
+
"""
|
|
139
|
+
if self.population_counter < len(self.population_data):
|
|
140
|
+
output = self.population_data.iloc[self.population_counter].to_dict()
|
|
141
|
+
self.population_counter += 1
|
|
142
|
+
else:
|
|
143
|
+
output = None
|
|
144
|
+
return output
|
|
145
|
+
|
|
146
|
+
def get_injection_parameter_keys(self) -> list[str]:
|
|
147
|
+
"""Get the list of injection parameter keys from the population data.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A list of strings representing the injection parameter keys.
|
|
151
|
+
"""
|
|
152
|
+
if not self.population_data.empty:
|
|
153
|
+
output = list(self.population_data.columns)
|
|
154
|
+
else:
|
|
155
|
+
output = []
|
|
156
|
+
return output
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def metadata(self) -> dict:
|
|
160
|
+
"""Get metadata including population file information.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Dictionary containing metadata.
|
|
164
|
+
"""
|
|
165
|
+
metadata = {
|
|
166
|
+
"population_reader": {
|
|
167
|
+
"arguments": {
|
|
168
|
+
"population_file": str(self.population_file),
|
|
169
|
+
"population_file_type": self.population_file_type,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if hasattr(self, "_population_metadata"):
|
|
174
|
+
metadata["population_reader"].update(self._population_metadata)
|
|
175
|
+
return metadata
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Mixins for GW simulation classes for randomness handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from gwsim.simulator.state import StateAttribute
|
|
8
|
+
from gwsim.utils.random import Generator, get_rng, get_state, set_seed, set_state
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("gwsim")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RandomnessMixin:
|
|
14
|
+
"""Mixin providing random number generation capabilities.
|
|
15
|
+
|
|
16
|
+
This mixin should be used by simulators that require randomness.
|
|
17
|
+
It provides RNG state management, seed handling, and state persistence.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> class MySimulator(RandomnessMixin, Simulator):
|
|
21
|
+
... def __init__(self, seed=None, **kwargs):
|
|
22
|
+
... super().__init__(**kwargs)
|
|
23
|
+
... self.seed = seed
|
|
24
|
+
...
|
|
25
|
+
... def simulate(self):
|
|
26
|
+
... # Use self.rng for random operations
|
|
27
|
+
... return self.rng.random()
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# State attributes for RNG persistence
|
|
31
|
+
rng_state = StateAttribute(
|
|
32
|
+
lambda self: get_state() if self.rng else None, post_set_hook=lambda self, state: self.init_rng(state)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def __init__(self, seed: int | None = None, **kwargs):
|
|
36
|
+
"""Initialize the randomness mixin.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
seed: Random seed for reproducibility. If None, no RNG is created.
|
|
40
|
+
**kwargs: Additional arguments passed to parent classes.
|
|
41
|
+
"""
|
|
42
|
+
super().__init__(**kwargs)
|
|
43
|
+
self.seed = seed
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def seed(self) -> int | None:
|
|
47
|
+
"""Get the random seed.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
The random seed used for initialization.
|
|
51
|
+
"""
|
|
52
|
+
return self._seed
|
|
53
|
+
|
|
54
|
+
@seed.setter
|
|
55
|
+
def seed(self, value: int | None) -> None:
|
|
56
|
+
"""Set the random seed and reinitialize RNG.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
value: New random seed. If None, RNG is disabled.
|
|
60
|
+
"""
|
|
61
|
+
self._seed = value
|
|
62
|
+
if value is not None:
|
|
63
|
+
set_seed(value)
|
|
64
|
+
self.rng = get_rng()
|
|
65
|
+
self.rng_state = get_state()
|
|
66
|
+
else:
|
|
67
|
+
self.rng = None
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def rng(self) -> Generator | None:
|
|
71
|
+
"""Get the random number generator.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Random number generator instance or None if no seed was set.
|
|
75
|
+
"""
|
|
76
|
+
return self._rng
|
|
77
|
+
|
|
78
|
+
@rng.setter
|
|
79
|
+
def rng(self, value: Generator | None) -> None:
|
|
80
|
+
"""Set the random number generator.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
value: Random number generator instance.
|
|
84
|
+
"""
|
|
85
|
+
self._rng = value
|
|
86
|
+
|
|
87
|
+
def init_rng(self, state: dict | None) -> None:
|
|
88
|
+
"""Initialize RNG from saved state.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
state: Saved RNG state dictionary.
|
|
92
|
+
"""
|
|
93
|
+
if state is not None and self.rng is not None:
|
|
94
|
+
set_state(state)
|
|
95
|
+
self._rng = get_rng()
|
|
96
|
+
else:
|
|
97
|
+
logger.debug("init_rng called but state is %s and self.rng is %s.", state, self.rng)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def metadata(self) -> dict:
|
|
101
|
+
"""Get metadata including seed information.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Dictionary containing seed and other metadata.
|
|
105
|
+
"""
|
|
106
|
+
metadata = {"seed": self.seed}
|
|
107
|
+
return metadata
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Mixins for simulator classes providing optional functionality."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, cast
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from astropy.units.quantity import Quantity
|
|
11
|
+
from gwpy.timeseries import TimeSeries as GWPyTimeSeries
|
|
12
|
+
|
|
13
|
+
from gwsim.data.time_series.time_series import TimeSeries
|
|
14
|
+
from gwsim.data.time_series.time_series_list import TimeSeriesList
|
|
15
|
+
from gwsim.simulator.state import StateAttribute
|
|
16
|
+
from gwsim.utils.datetime_parser import parse_duration_to_seconds
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("gwsim")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TimeSeriesMixin: # pylint: disable=too-few-public-methods,too-many-instance-attributes
|
|
22
|
+
"""Mixin providing timing and duration management.
|
|
23
|
+
|
|
24
|
+
This mixin adds time-based parameters commonly used
|
|
25
|
+
in gravitational wave simulations.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
start_time = StateAttribute(Quantity(0, unit="s"))
|
|
29
|
+
cached_data_chunks = TimeSeriesList()
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
start_time: int = 0,
|
|
34
|
+
duration: float = 4,
|
|
35
|
+
total_duration: float | str | None = None,
|
|
36
|
+
sampling_frequency: float = 4096,
|
|
37
|
+
num_of_channels: int | None = None,
|
|
38
|
+
dtype: type = np.float64,
|
|
39
|
+
**kwargs,
|
|
40
|
+
):
|
|
41
|
+
"""Initialize timing parameters.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
start_time: Start time in GPS seconds. Default is 0.
|
|
45
|
+
duration: Duration of simulation in seconds. Default is 4.
|
|
46
|
+
total_duration
|
|
47
|
+
sampling_frequency: Sampling frequency in Hz. Default is 4096.
|
|
48
|
+
dtype: Data type for the time series data. Default is np.float64.
|
|
49
|
+
**kwargs: Additional arguments passed to parent classes.
|
|
50
|
+
"""
|
|
51
|
+
super().__init__(**kwargs)
|
|
52
|
+
# TimeSeriesMixin is the last mixin in the hierarchy, so no super().__init__() call needed
|
|
53
|
+
self.start_time = Quantity(start_time, unit="s")
|
|
54
|
+
self.duration = duration
|
|
55
|
+
self.total_duration = total_duration
|
|
56
|
+
self.sampling_frequency = sampling_frequency
|
|
57
|
+
self.dtype = dtype
|
|
58
|
+
|
|
59
|
+
# Get the number of channels.
|
|
60
|
+
if num_of_channels is not None:
|
|
61
|
+
self.num_of_channels = num_of_channels
|
|
62
|
+
if "detectors" in kwargs and kwargs["detectors"] is not None:
|
|
63
|
+
if len(kwargs["detectors"]) != num_of_channels:
|
|
64
|
+
raise ValueError("Number of detectors does not match num_of_channels.")
|
|
65
|
+
elif "detectors" in kwargs and kwargs["detectors"] is not None:
|
|
66
|
+
self.num_of_channels = len(kwargs["detectors"])
|
|
67
|
+
else:
|
|
68
|
+
self.num_of_channels = 1
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def duration(self) -> Quantity:
|
|
72
|
+
"""Get the duration of each simulation segment.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Duration in seconds.
|
|
76
|
+
"""
|
|
77
|
+
return self._duration
|
|
78
|
+
|
|
79
|
+
@duration.setter
|
|
80
|
+
def duration(self, value: float) -> None:
|
|
81
|
+
"""Set the duration of each simulation segment.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
value: Duration in seconds.
|
|
85
|
+
"""
|
|
86
|
+
if value <= 0:
|
|
87
|
+
raise ValueError("duration must be positive.")
|
|
88
|
+
self._duration = Quantity(value, unit="s")
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def sampling_frequency(self) -> Quantity:
|
|
92
|
+
"""Get the sampling frequency.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Sampling frequency in Hz.
|
|
96
|
+
"""
|
|
97
|
+
return self._sampling_frequency
|
|
98
|
+
|
|
99
|
+
@sampling_frequency.setter
|
|
100
|
+
def sampling_frequency(self, value: float) -> None:
|
|
101
|
+
"""Set the sampling frequency.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
value: Sampling frequency in Hz.
|
|
105
|
+
"""
|
|
106
|
+
if value <= 0:
|
|
107
|
+
raise ValueError("sampling_frequency must be positive.")
|
|
108
|
+
self._sampling_frequency = Quantity(value, unit="Hz")
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def total_duration(self) -> Quantity:
|
|
112
|
+
"""Get the total duration of the simulation.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Total duration in seconds.
|
|
116
|
+
"""
|
|
117
|
+
return self._total_duration
|
|
118
|
+
|
|
119
|
+
@total_duration.setter
|
|
120
|
+
def total_duration(self, value: int | float | str | None) -> None:
|
|
121
|
+
"""Set the total duration of the simulation.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
value: Total duration in seconds.
|
|
125
|
+
"""
|
|
126
|
+
if value is not None:
|
|
127
|
+
if isinstance(value, (float, int)):
|
|
128
|
+
self._total_duration = Quantity(value, unit="s")
|
|
129
|
+
elif isinstance(value, str):
|
|
130
|
+
self._total_duration = Quantity(parse_duration_to_seconds(value), unit="s")
|
|
131
|
+
else:
|
|
132
|
+
raise ValueError("total_duration must be a float, int, or str representing duration.")
|
|
133
|
+
|
|
134
|
+
if self.total_duration < 0:
|
|
135
|
+
raise ValueError("total_duration must be non-negative.")
|
|
136
|
+
|
|
137
|
+
if self.total_duration < self.duration:
|
|
138
|
+
raise ValueError("total_duration must be greater than or equal to duration.")
|
|
139
|
+
|
|
140
|
+
# Round the total_duration to the nearest multiple of duration
|
|
141
|
+
num_segments = round(self.total_duration.value / self.duration.value)
|
|
142
|
+
self._total_duration = Quantity(num_segments * self.duration, unit="s")
|
|
143
|
+
|
|
144
|
+
logger.info("Total duration set to %s seconds.", self.total_duration)
|
|
145
|
+
|
|
146
|
+
# Set the max_samples based on total_duration and duration
|
|
147
|
+
self.max_samples = int(self.total_duration.value / self.duration.value)
|
|
148
|
+
logger.info("Setting max_samples to %s based on total_duration and duration.", self.max_samples)
|
|
149
|
+
else:
|
|
150
|
+
self._total_duration = self.duration * self.max_samples
|
|
151
|
+
logger.info("total_duration not set, using duration * max_samples = %s seconds.", self.total_duration.value)
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def end_time(self) -> Quantity:
|
|
155
|
+
"""Calculate the end time of the current segment.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
End time in GPS seconds.
|
|
159
|
+
"""
|
|
160
|
+
return cast(Quantity, self.start_time + self.duration)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def final_end_time(self) -> Quantity:
|
|
164
|
+
"""Calculate the final end time of the entire simulation.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Final end time in GPS seconds.
|
|
168
|
+
"""
|
|
169
|
+
return cast(Quantity, self.start_time + self.total_duration)
|
|
170
|
+
|
|
171
|
+
def _simulate(self, *args, **kwargs) -> TimeSeriesList:
|
|
172
|
+
"""Generate time series data chunks.
|
|
173
|
+
|
|
174
|
+
This method should be implemented by subclasses to generate
|
|
175
|
+
the actual time series data.
|
|
176
|
+
"""
|
|
177
|
+
raise NotImplementedError("Subclasses must implement the _simulate method.")
|
|
178
|
+
|
|
179
|
+
def simulate(self, *args, **kwargs) -> TimeSeries:
|
|
180
|
+
"""
|
|
181
|
+
Simulate a segment of time series data.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
*args: Positional arguments for the _simulate method.
|
|
185
|
+
**kwargs: Keyword arguments for the _simulate method.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
TimeSeries: Simulated time series segment.
|
|
189
|
+
"""
|
|
190
|
+
# First create a new segment
|
|
191
|
+
segment = TimeSeries(
|
|
192
|
+
data=np.zeros(
|
|
193
|
+
(self.num_of_channels, int(self.duration.value * self.sampling_frequency.value)), dtype=self.dtype
|
|
194
|
+
),
|
|
195
|
+
start_time=self.start_time,
|
|
196
|
+
sampling_frequency=self.sampling_frequency,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Inject cached data chunks into the segment
|
|
200
|
+
self.cached_data_chunks = segment.inject_from_list(self.cached_data_chunks)
|
|
201
|
+
|
|
202
|
+
# Generate new chunks of data
|
|
203
|
+
new_chunks = self._simulate(*args, **kwargs)
|
|
204
|
+
|
|
205
|
+
# Add the new chunks to the segment
|
|
206
|
+
remaining_chunks = segment.inject_from_list(new_chunks)
|
|
207
|
+
|
|
208
|
+
# Add the remaining chunks to the cache
|
|
209
|
+
self.cached_data_chunks.extend(remaining_chunks)
|
|
210
|
+
|
|
211
|
+
# Check whether there are chunks that are outside the whole dataset duration
|
|
212
|
+
# Remove the chunks that are outside the total duration
|
|
213
|
+
for i in reversed(range(len(self.cached_data_chunks))):
|
|
214
|
+
chunk = self.cached_data_chunks[i]
|
|
215
|
+
if chunk.start_time >= self.final_end_time:
|
|
216
|
+
logger.info(
|
|
217
|
+
"Removing cached chunk starting at %s which is outside the total duration ending at %s.",
|
|
218
|
+
chunk.start_time,
|
|
219
|
+
self.final_end_time,
|
|
220
|
+
)
|
|
221
|
+
self.cached_data_chunks.pop(i)
|
|
222
|
+
elif chunk.end_time <= self.start_time:
|
|
223
|
+
logger.info(
|
|
224
|
+
"Removing cached chunk ending at %s which is before the current segment starting at %s.",
|
|
225
|
+
chunk.end_time,
|
|
226
|
+
self.start_time,
|
|
227
|
+
)
|
|
228
|
+
self.cached_data_chunks.pop(i)
|
|
229
|
+
|
|
230
|
+
return segment
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def metadata(self) -> dict:
|
|
234
|
+
"""Get metadata including timing information.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Dictionary containing timing parameters and other metadata.
|
|
238
|
+
"""
|
|
239
|
+
metadata = {
|
|
240
|
+
"time_series": {
|
|
241
|
+
"arguments": {
|
|
242
|
+
"start_time": self.start_time,
|
|
243
|
+
"duration": self.duration,
|
|
244
|
+
"sampling_frequency": self.sampling_frequency,
|
|
245
|
+
"num_of_channels": self.num_of_channels,
|
|
246
|
+
"dtype": str(self.dtype),
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return metadata
|
|
251
|
+
|
|
252
|
+
def _save_data( # pylint: disable=unused-argument
|
|
253
|
+
self,
|
|
254
|
+
data: TimeSeries,
|
|
255
|
+
file_name: str | Path | np.ndarray[Any, np.dtype[np.object_]],
|
|
256
|
+
channel_names: str | None = None,
|
|
257
|
+
**kwargs,
|
|
258
|
+
) -> None:
|
|
259
|
+
"""Save time series data to a file.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
data: Time series data to save.
|
|
263
|
+
file_name: Path to the output file.
|
|
264
|
+
**kwargs: Additional arguments for the saving function.
|
|
265
|
+
"""
|
|
266
|
+
if data.num_of_channels == 1 and isinstance(file_name, (str, Path)):
|
|
267
|
+
self._save_gwf_data(data=data[0], file_name=file_name, **kwargs)
|
|
268
|
+
elif (
|
|
269
|
+
data.num_of_channels > 1
|
|
270
|
+
and isinstance(file_name, np.ndarray)
|
|
271
|
+
and len(file_name.shape) == 1
|
|
272
|
+
and file_name.shape[0] == data.num_of_channels
|
|
273
|
+
):
|
|
274
|
+
for i in range(data.num_of_channels):
|
|
275
|
+
single_file_name = cast(Path, file_name[i])
|
|
276
|
+
self._save_gwf_data(data=data[i], file_name=single_file_name, **kwargs)
|
|
277
|
+
else:
|
|
278
|
+
raise ValueError(
|
|
279
|
+
"file_name must be a single path for single-channel data or an array of paths for multi-channel data."
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
def _save_gwf_data( # pylint: disable=unused-argument
|
|
283
|
+
self, data: GWPyTimeSeries, file_name: str | Path, channel: str | None = None, **kwargs
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Save GWPy TimeSeries data to a GWF file.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
data: GWPy TimeSeries data to save.
|
|
289
|
+
file_name: Path to the output GWF file.
|
|
290
|
+
channel: Optional channel name to set in the data.
|
|
291
|
+
**kwargs: Additional arguments for the write function.
|
|
292
|
+
"""
|
|
293
|
+
if channel is not None:
|
|
294
|
+
data.channel = channel
|
|
295
|
+
data.write(str(file_name))
|
gwsim/mixin/waveform.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Mixin for waveform generation in signal simulators."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from gwsim.waveform.factory import WaveformFactory
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("gwsim")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WaveformMixin: # pylint: disable=too-few-public-methods
|
|
15
|
+
"""Mixin class for waveform generation in signal simulators."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
waveform_model: str | Callable = "IMRPhenomXPHM",
|
|
20
|
+
waveform_arguments: dict[str, Any] | None = None,
|
|
21
|
+
**kwargs,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Initialize the WaveformMixin.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
waveform_model: Name (from registry) or callable for waveform generation.
|
|
27
|
+
waveform_arguments: Fixed parameters to pass to waveform model.
|
|
28
|
+
parameter_mapping: Dict mapping population column names to waveform parameter names.
|
|
29
|
+
minimum_frequency: Minimum GW frequency for waveform generation.
|
|
30
|
+
**kwargs: Additional arguments for other mixins.
|
|
31
|
+
"""
|
|
32
|
+
super().__init__(**kwargs)
|
|
33
|
+
self.waveform_factory = WaveformFactory()
|
|
34
|
+
if waveform_model not in self.waveform_factory.list_models():
|
|
35
|
+
# Register the model if not already registered
|
|
36
|
+
self.waveform_factory.register_model(name=str(waveform_model), factory_func=waveform_model)
|
|
37
|
+
self.waveform_model = str(waveform_model)
|
|
38
|
+
self.waveform_arguments = waveform_arguments or {}
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def metadata(self) -> dict:
|
|
42
|
+
"""Include waveform metadata."""
|
|
43
|
+
metadata = {
|
|
44
|
+
"waveform_model": self.waveform_model,
|
|
45
|
+
"waveform_arguments": self.waveform_arguments,
|
|
46
|
+
}
|
|
47
|
+
return metadata
|
gwsim/noise/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Noise models for gravitational wave detector simulations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from gwsim.noise.base import NoiseSimulator
|
|
8
|
+
from gwsim.noise.bilby_stationary_gaussian import BilbyStationaryGaussianNoiseSimulator
|
|
9
|
+
from gwsim.noise.colored_noise import ColoredNoiseSimulator
|
|
10
|
+
from gwsim.noise.correlated_noise import CorrelatedNoiseSimulator
|
|
11
|
+
from gwsim.noise.pycbc_stationary_gaussian import PyCBCStationaryGaussianNoiseSimulator
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BilbyStationaryGaussianNoiseSimulator",
|
|
15
|
+
"ColoredNoiseSimulator",
|
|
16
|
+
"CorrelatedNoiseSimulator",
|
|
17
|
+
"NoiseSimulator",
|
|
18
|
+
"PyCBCStationaryGaussianNoiseSimulator",
|
|
19
|
+
]
|