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.
Files changed (103) hide show
  1. gwsim/__init__.py +11 -0
  2. gwsim/__main__.py +8 -0
  3. gwsim/cli/__init__.py +0 -0
  4. gwsim/cli/config.py +88 -0
  5. gwsim/cli/default_config.py +56 -0
  6. gwsim/cli/main.py +101 -0
  7. gwsim/cli/merge.py +150 -0
  8. gwsim/cli/repository/__init__.py +0 -0
  9. gwsim/cli/repository/create.py +91 -0
  10. gwsim/cli/repository/delete.py +51 -0
  11. gwsim/cli/repository/download.py +54 -0
  12. gwsim/cli/repository/list_depositions.py +63 -0
  13. gwsim/cli/repository/main.py +38 -0
  14. gwsim/cli/repository/metadata/__init__.py +0 -0
  15. gwsim/cli/repository/metadata/main.py +24 -0
  16. gwsim/cli/repository/metadata/update.py +58 -0
  17. gwsim/cli/repository/publish.py +52 -0
  18. gwsim/cli/repository/upload.py +74 -0
  19. gwsim/cli/repository/utils.py +47 -0
  20. gwsim/cli/repository/verify.py +61 -0
  21. gwsim/cli/simulate.py +220 -0
  22. gwsim/cli/simulate_utils.py +596 -0
  23. gwsim/cli/utils/__init__.py +85 -0
  24. gwsim/cli/utils/checkpoint.py +178 -0
  25. gwsim/cli/utils/config.py +347 -0
  26. gwsim/cli/utils/hash.py +23 -0
  27. gwsim/cli/utils/retry.py +62 -0
  28. gwsim/cli/utils/simulation_plan.py +439 -0
  29. gwsim/cli/utils/template.py +56 -0
  30. gwsim/cli/utils/utils.py +149 -0
  31. gwsim/cli/validate.py +255 -0
  32. gwsim/data/__init__.py +8 -0
  33. gwsim/data/serialize/__init__.py +9 -0
  34. gwsim/data/serialize/decoder.py +59 -0
  35. gwsim/data/serialize/encoder.py +44 -0
  36. gwsim/data/serialize/serializable.py +33 -0
  37. gwsim/data/time_series/__init__.py +3 -0
  38. gwsim/data/time_series/inject.py +104 -0
  39. gwsim/data/time_series/time_series.py +355 -0
  40. gwsim/data/time_series/time_series_list.py +182 -0
  41. gwsim/detector/__init__.py +8 -0
  42. gwsim/detector/base.py +156 -0
  43. gwsim/detector/detectors/E1_2L_Aligned_Sardinia.interferometer +22 -0
  44. gwsim/detector/detectors/E1_2L_Misaligned_Sardinia.interferometer +22 -0
  45. gwsim/detector/detectors/E1_Triangle_EMR.interferometer +19 -0
  46. gwsim/detector/detectors/E1_Triangle_Sardinia.interferometer +19 -0
  47. gwsim/detector/detectors/E2_2L_Aligned_EMR.interferometer +22 -0
  48. gwsim/detector/detectors/E2_2L_Misaligned_EMR.interferometer +22 -0
  49. gwsim/detector/detectors/E2_Triangle_EMR.interferometer +19 -0
  50. gwsim/detector/detectors/E2_Triangle_Sardinia.interferometer +19 -0
  51. gwsim/detector/detectors/E3_Triangle_EMR.interferometer +19 -0
  52. gwsim/detector/detectors/E3_Triangle_Sardinia.interferometer +19 -0
  53. gwsim/detector/noise_curves/ET_10_HF_psd.txt +3000 -0
  54. gwsim/detector/noise_curves/ET_10_full_cryo_psd.txt +3000 -0
  55. gwsim/detector/noise_curves/ET_15_HF_psd.txt +3000 -0
  56. gwsim/detector/noise_curves/ET_15_full_cryo_psd.txt +3000 -0
  57. gwsim/detector/noise_curves/ET_20_HF_psd.txt +3000 -0
  58. gwsim/detector/noise_curves/ET_20_full_cryo_psd.txt +3000 -0
  59. gwsim/detector/noise_curves/ET_D_psd.txt +3000 -0
  60. gwsim/detector/utils.py +90 -0
  61. gwsim/glitch/__init__.py +7 -0
  62. gwsim/glitch/base.py +69 -0
  63. gwsim/mixin/__init__.py +8 -0
  64. gwsim/mixin/detector.py +203 -0
  65. gwsim/mixin/gwf.py +192 -0
  66. gwsim/mixin/population_reader.py +175 -0
  67. gwsim/mixin/randomness.py +107 -0
  68. gwsim/mixin/time_series.py +295 -0
  69. gwsim/mixin/waveform.py +47 -0
  70. gwsim/noise/__init__.py +19 -0
  71. gwsim/noise/base.py +134 -0
  72. gwsim/noise/bilby_stationary_gaussian.py +117 -0
  73. gwsim/noise/colored_noise.py +275 -0
  74. gwsim/noise/correlated_noise.py +257 -0
  75. gwsim/noise/pycbc_stationary_gaussian.py +112 -0
  76. gwsim/noise/stationary_gaussian.py +44 -0
  77. gwsim/noise/white_noise.py +51 -0
  78. gwsim/repository/__init__.py +0 -0
  79. gwsim/repository/zenodo.py +269 -0
  80. gwsim/signal/__init__.py +11 -0
  81. gwsim/signal/base.py +137 -0
  82. gwsim/signal/cbc.py +61 -0
  83. gwsim/simulator/__init__.py +7 -0
  84. gwsim/simulator/base.py +315 -0
  85. gwsim/simulator/state.py +85 -0
  86. gwsim/utils/__init__.py +11 -0
  87. gwsim/utils/datetime_parser.py +44 -0
  88. gwsim/utils/et_2l_geometry.py +165 -0
  89. gwsim/utils/io.py +167 -0
  90. gwsim/utils/log.py +145 -0
  91. gwsim/utils/population.py +48 -0
  92. gwsim/utils/random.py +69 -0
  93. gwsim/utils/retry.py +75 -0
  94. gwsim/utils/triangular_et_geometry.py +164 -0
  95. gwsim/version.py +7 -0
  96. gwsim/waveform/__init__.py +7 -0
  97. gwsim/waveform/factory.py +83 -0
  98. gwsim/waveform/pycbc_wrapper.py +37 -0
  99. gwsim-0.1.0.dist-info/METADATA +157 -0
  100. gwsim-0.1.0.dist-info/RECORD +103 -0
  101. gwsim-0.1.0.dist-info/WHEEL +4 -0
  102. gwsim-0.1.0.dist-info/entry_points.txt +2 -0
  103. 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))
@@ -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
@@ -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
+ ]