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,269 @@
1
+ """Zenodo publishing client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import requests
10
+ from requests import Response
11
+
12
+ from gwsim.utils.retry import retry_on_failure
13
+
14
+
15
+ def get_deposition_id_from_doi(doi: str) -> tuple[str, bool]:
16
+ """Extract deposition ID from a Zenodo DOI.
17
+
18
+ Args:
19
+ doi: The DOI string.
20
+
21
+ returns:
22
+ A tuple containing the deposition ID and a boolean indicating if it's from sandbox.
23
+ """
24
+ parts = doi.split(".")
25
+ deposition_id = parts[1]
26
+ if parts[0] == "10.5072/zenodo":
27
+ sandbox = True
28
+ elif parts[0] == "10.5281/zenodo":
29
+ sandbox = False
30
+ else:
31
+ raise ValueError(f"Invalid Zenodo DOI: {doi}")
32
+
33
+ return deposition_id, sandbox
34
+
35
+
36
+ class ZenodoClient:
37
+ """Client for interacting with Zenodo API (production and sandbox)."""
38
+
39
+ def __init__(self, access_token: str, sandbox: bool = False):
40
+ """Initialize the Zenodo client.
41
+
42
+ Args:
43
+ access_token: Zenodo API access token.
44
+ sandbox: Whether to use the sandbox environment. Default is False.
45
+ """
46
+ self.access_token = access_token
47
+ self.sandbox = sandbox
48
+ self.base_url = "https://sandbox.zenodo.org/api/" if sandbox else "https://zenodo.org/api/"
49
+
50
+ self.headers = {
51
+ "Authorization": f"Bearer {self.access_token}",
52
+ }
53
+
54
+ @retry_on_failure()
55
+ def _request(self, method: str, url: str, headers: dict, timeout: int = 60, **kwargs) -> Response:
56
+ """Make a request to the Zenodo API.
57
+
58
+ Args:
59
+ method: HTTP method (GET, POST, PUT, DELETE).
60
+ url: URL to make the request to.
61
+ headers: Headers to include in the request.
62
+ timeout: Timeout for the request in seconds. Default is 60.
63
+ **kwargs: Additional arguments to pass to requests.
64
+
65
+ Returns:
66
+ Response object.
67
+ """
68
+ response = requests.request(method, url, headers=headers, timeout=timeout, **kwargs)
69
+ response.raise_for_status()
70
+ return response
71
+
72
+ def create_deposition(self, metadata: dict[str, Any] | None = None, timeout=60) -> dict[str, Any]:
73
+ """Create a new deposition.
74
+
75
+ Args:
76
+ metadata: Optional metadata dictionary for the deposition.
77
+ timeout: Timeout for the request in seconds. Default is 60.
78
+
79
+ Returns:
80
+ Response JSON as a dictionary.
81
+ """
82
+ data = {"metadata": metadata} if metadata else {}
83
+
84
+ response: Response = self._request(
85
+ "POST",
86
+ f"{self.base_url}deposit/depositions",
87
+ headers={"Content-Type": "application/json", **self.headers},
88
+ timeout=timeout,
89
+ json=data,
90
+ )
91
+ return response.json()
92
+
93
+ def upload_file(
94
+ self, deposition_id: str, file_path: Path, timeout=300, auto_timeout: bool = True
95
+ ) -> dict[str, Any]:
96
+ """Upload a file to a deposition.
97
+
98
+ Args:
99
+ deposition_id: ID of the deposition to upload the file to.
100
+ file_path: Path to the file to upload.
101
+ timeout: Timeout for the request in seconds. Default is 300.
102
+ auto_timeout: Whether to automatically adjust timeout based on file size. Default is True.
103
+ If True, the timeout is set to max(timeout, file_size_in_MB * 10).
104
+
105
+ Returns:
106
+ Response JSON as a dictionary.
107
+ """
108
+ deposition = self.get_deposition(deposition_id)
109
+
110
+ bucket = deposition["links"]["bucket"]
111
+
112
+ if auto_timeout:
113
+ # Get the size of the file in MB
114
+ file_size = file_path.stat().st_size / (1024 * 1024)
115
+
116
+ timeout = max(timeout, int(file_size * 10)) # 10 seconds per MB, minimum 300 seconds
117
+
118
+ with file_path.open("rb") as f:
119
+ response: Response = self._request(
120
+ "PUT",
121
+ f"{bucket}/{file_path.name}",
122
+ headers=self.headers,
123
+ timeout=timeout,
124
+ data=f,
125
+ )
126
+
127
+ return response.json()
128
+
129
+ def update_metadata(self, deposition_id: str, metadata: dict[str, Any], timeout: int = 60) -> dict[str, Any]:
130
+ """Update metadata for a deposition.
131
+
132
+ Args:
133
+ deposition_id: ID of the deposition to update.
134
+ metadata: Metadata dictionary to update.
135
+ timeout: Timeout for the request in seconds. Default is 60.
136
+
137
+ Returns:
138
+ Response JSON as a dictionary.
139
+ """
140
+ data = {"metadata": metadata}
141
+ response: Response = self._request(
142
+ "PUT",
143
+ f"{self.base_url}deposit/depositions/{deposition_id}",
144
+ headers={"Content-Type": "application/json", **self.headers},
145
+ timeout=timeout,
146
+ data=json.dumps(data),
147
+ )
148
+
149
+ return response.json()
150
+
151
+ def publish_deposition(self, deposition_id: str, timeout: int = 300) -> dict[str, Any]:
152
+ """Publish a deposition.
153
+
154
+ Args:
155
+ deposition_id: ID of the deposition to publish.
156
+ timeout: Timeout for the request in seconds. Default is 300.
157
+
158
+ Returns:
159
+ Response JSON as a dictionary.
160
+ """
161
+ response: Response = self._request(
162
+ "POST",
163
+ f"{self.base_url}deposit/depositions/{deposition_id}/actions/publish",
164
+ headers=self.headers,
165
+ timeout=timeout,
166
+ )
167
+ return response.json()
168
+
169
+ def get_deposition(self, deposition_id: str, timeout: int = 60) -> dict[str, Any]:
170
+ """Retrieve a deposition's details.
171
+
172
+ Args:
173
+ deposition_id: ID of the deposition to retrieve.
174
+ timeout: Timeout for the request in seconds. Default is 60.
175
+
176
+ Returns:
177
+ Response JSON as a dictionary.
178
+ """
179
+ response: Response = self._request(
180
+ "GET",
181
+ f"{self.base_url}deposit/depositions/{deposition_id}",
182
+ headers={"Content-Type": "application/json", **self.headers},
183
+ timeout=timeout,
184
+ )
185
+ return response.json()
186
+
187
+ def download_file(
188
+ self,
189
+ deposition_id: str,
190
+ filename: str,
191
+ output_path: Path,
192
+ is_draft: bool = False,
193
+ timeout: int = 300,
194
+ file_size_in_mb: int | None = None,
195
+ ) -> dict[str, Any]:
196
+ """Download a file from Zenodo.
197
+
198
+ Args:
199
+ deposition_id: ID of the deposition.
200
+ filename: Name of the file to download.
201
+ output_path: Path to save the downloaded file.
202
+ is_draft: Whether the file is in a draft deposition. Default is False.
203
+ timeout: Timeout for the request in seconds. Default is 300.
204
+ file_size_in_mb: Optional size of the file in MB to adjust timeout. If provided
205
+
206
+ Returns:
207
+ Response JSON as a dictionary.
208
+ """
209
+ if is_draft:
210
+ deposition_id += "/draft"
211
+
212
+ file_url = f"{self.base_url}records/{deposition_id}/files/{filename}"
213
+
214
+ if file_size_in_mb is not None:
215
+ timeout = max(timeout, int(file_size_in_mb * 10)) # 10 seconds per MB
216
+
217
+ response: Response = self._request(
218
+ "GET",
219
+ file_url,
220
+ headers={"Content-Type": "application/json", **self.headers},
221
+ timeout=timeout,
222
+ stream=True,
223
+ )
224
+
225
+ # Atomic write to avoid incomplete files
226
+ output_path_tmp = output_path.with_suffix(".tmp")
227
+ with output_path_tmp.open("wb") as f:
228
+ for chunk in response.iter_content(chunk_size=8192):
229
+ f.write(chunk)
230
+ output_path_tmp.rename(output_path)
231
+ return response.json()
232
+
233
+ def list_depositions(self, status: str = "published", timeout: int = 60) -> list[dict[str, Any]]:
234
+ """List all depositions for the authenticated user.
235
+
236
+ Args:
237
+ status: Filter by deposition status ('draft', 'unsubmitted', 'published'). Default is 'published'.
238
+ timeout: Timeout for the request in seconds. Default is 60.
239
+
240
+ Returns:
241
+ List of deposition dictionaries.
242
+ """
243
+ params = {"status": status}
244
+ response = self._request(
245
+ "GET",
246
+ f"{self.base_url}deposit/depositions",
247
+ headers={"Content-Type": "application/json", **self.headers},
248
+ timeout=timeout,
249
+ params=params,
250
+ )
251
+ return response.json()
252
+
253
+ def delete_deposition(self, deposition_id: str, timeout: int = 60) -> dict[str, Any]:
254
+ """Delete a deposition.
255
+
256
+ Args:
257
+ deposition_id: ID of the deposition to delete.
258
+ timeout: Timeout for the request in seconds. Default is 60.
259
+
260
+ Returns:
261
+ Response JSON as a dictionary.
262
+ """
263
+ response = self._request(
264
+ "DELETE",
265
+ f"{self.base_url}deposit/depositions/{deposition_id}",
266
+ headers=self.headers,
267
+ timeout=timeout,
268
+ )
269
+ return response
@@ -0,0 +1,11 @@
1
+ """Initialization for the signal module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from gwsim.signal.base import SignalSimulator
6
+ from gwsim.signal.cbc import CBCSignalSimulator
7
+
8
+ __all__ = [
9
+ "CBCSignalSimulator",
10
+ "SignalSimulator",
11
+ ]
gwsim/signal/base.py ADDED
@@ -0,0 +1,137 @@
1
+ """Base class for signal simulators."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+ from typing import Any, cast
9
+
10
+ import numpy as np
11
+
12
+ from gwsim.data.time_series.time_series_list import TimeSeriesList
13
+ from gwsim.mixin.detector import DetectorMixin
14
+ from gwsim.mixin.population_reader import PopulationReaderMixin
15
+ from gwsim.mixin.time_series import TimeSeriesMixin
16
+ from gwsim.mixin.waveform import WaveformMixin
17
+ from gwsim.simulator.base import Simulator
18
+
19
+ logger = logging.getLogger("gwsim")
20
+
21
+
22
+ class SignalSimulator(PopulationReaderMixin, WaveformMixin, TimeSeriesMixin, DetectorMixin, Simulator):
23
+ """Base class for signal simulators."""
24
+
25
+ def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
26
+ self,
27
+ population_file: str | Path,
28
+ population_file_type: str = "pycbc",
29
+ waveform_model: str | Callable = "IMRPhenomXPHM",
30
+ waveform_arguments: dict[str, Any] | None = None,
31
+ start_time: int = 0,
32
+ duration: float = 1024,
33
+ sampling_frequency: float = 4096,
34
+ max_samples: int | None = None,
35
+ dtype: type = np.float64,
36
+ detectors: list[str] | None = None,
37
+ minimum_frequency: float = 5,
38
+ **kwargs,
39
+ ) -> None:
40
+ """Initialize the base signal simulator.
41
+
42
+ Args:
43
+ population_file: Path to the population file.
44
+ population_file_type: Type of the population file (e.g., 'pycbc').
45
+ waveform_model: Name (from registry) or callable for waveform generation.
46
+ waveform_arguments: Fixed parameters to pass to waveform model.
47
+ start_time: Start time of the first signal segment in GPS seconds. Default is 0.
48
+ duration: Duration of each signal segment in seconds. Default is 1024.
49
+ sampling_frequency: Sampling frequency of the signals in Hz. Default is 4096.
50
+ max_samples: Maximum number of samples to generate. None means infinite.
51
+ dtype: Data type for the time series data. Default is np.float64.
52
+ detectors: List of detector names. Default is None.
53
+ minimum_frequency: Minimum GW frequency for waveform generation. Default is 5 Hz.
54
+ **kwargs: Additional arguments absorbed by subclasses and mixins.
55
+ """
56
+ waveform_arguments = waveform_arguments or {}
57
+ required_waveform_arguments = {
58
+ "minimum_frequency": minimum_frequency,
59
+ "sampling_frequency": sampling_frequency,
60
+ }
61
+ for key, value in required_waveform_arguments.items():
62
+ if key not in waveform_arguments:
63
+ logger.info("%s not specified in waveform_arguments; setting to %s", key, value)
64
+ waveform_arguments[key] = value
65
+
66
+ super().__init__(
67
+ population_file=population_file,
68
+ population_file_type=population_file_type,
69
+ waveform_model=waveform_model,
70
+ waveform_arguments=waveform_arguments,
71
+ detectors=detectors,
72
+ start_time=start_time,
73
+ duration=duration,
74
+ sampling_frequency=sampling_frequency,
75
+ max_samples=max_samples,
76
+ dtype=dtype,
77
+ **kwargs,
78
+ )
79
+
80
+ def _simulate(self, *args, **kwargs) -> TimeSeriesList:
81
+ """Simulate signals for the current segment.
82
+
83
+ Returns:
84
+ TimeSeriesList: List of simulated signals.
85
+ """
86
+ output = []
87
+
88
+ while True:
89
+ # Get the next injection parameters
90
+ parameters = self.get_next_injection_parameters()
91
+
92
+ # If the parameters are None, break the loop
93
+ if parameters is None:
94
+ break
95
+
96
+ # Get the polarizations
97
+ polarizations = self.waveform_factory.generate(
98
+ waveform_model=self.waveform_model, parameters=parameters, **self.waveform_arguments
99
+ )
100
+
101
+ # Project onto detectors
102
+ strain = self.project_polarizations(
103
+ polarizations=polarizations,
104
+ right_ascension=parameters["right_ascension"],
105
+ declination=parameters["declination"],
106
+ polarization_angle=parameters["polarization_angle"],
107
+ **self.waveform_arguments,
108
+ )
109
+
110
+ # Register the parameters
111
+ strain.metadata.update({"injection_parameters": parameters})
112
+
113
+ output.append(strain)
114
+
115
+ # Check whether the start time of the strain is at or after the end time of the current segment
116
+ if strain.start_time >= self.end_time:
117
+ break
118
+ return TimeSeriesList(output)
119
+
120
+ @property
121
+ def metadata(self) -> dict:
122
+ """Get the metadata of the simulator.
123
+
124
+ Returns:
125
+ Metadata dictionary.
126
+ """
127
+ meta = super().metadata
128
+ return meta
129
+
130
+ def update_state(self) -> None:
131
+ """Update internal state after each sample generation.
132
+
133
+ This method can be overridden by subclasses to update any internal state
134
+ after generating a sample. The default implementation does nothing.
135
+ """
136
+ self.counter = cast(int, self.counter) + 1
137
+ self.start_time += self.duration
gwsim/signal/cbc.py ADDED
@@ -0,0 +1,61 @@
1
+ """Compact Binary Coalescence (CBC) signal simulation module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+
11
+ from gwsim.signal.base import SignalSimulator
12
+
13
+
14
+ class CBCSignalSimulator(SignalSimulator):
15
+ """CBC Signal Simulator class."""
16
+
17
+ def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments
18
+ self,
19
+ population_file: str | Path,
20
+ population_file_type: str = "pycbc",
21
+ waveform_model: str | Callable = "IMRPhenomXPHM",
22
+ waveform_arguments: dict[str, Any] | None = None,
23
+ start_time: int = 0,
24
+ duration: float = 1024,
25
+ sampling_frequency: float = 4096,
26
+ max_samples: int | None = None,
27
+ dtype: type = np.float64,
28
+ detectors: list[str] | None = None,
29
+ minimum_frequency: float = 5,
30
+ **kwargs,
31
+ ) -> None:
32
+ """Initialize the CBC signal simulator.
33
+
34
+ Args:
35
+ population_file: Path to the population file.
36
+ population_file_type: Type of the population file (e.g., 'pycbc').
37
+ waveform_model: Name (from registry) or callable for waveform generation.
38
+ waveform_arguments: Fixed parameters to pass to waveform model.
39
+ start_time: Start time of the first signal segment in GPS seconds. Default is 0.
40
+ duration: Duration of each signal segment in seconds. Default is 1024.
41
+ sampling_frequency: Sampling frequency of the signals in Hz. Default is 4096.
42
+ max_samples: Maximum number of samples to generate. None means infinite.
43
+ dtype: Data type for the time series data. Default is np.float64.
44
+ detectors: List of detector names. Default is None.
45
+ minimum_frequency: Minimum GW frequency for waveform generation. Default is 5 Hz.
46
+ **kwargs: Additional arguments absorbed by subclasses and mixins.
47
+ """
48
+ super().__init__(
49
+ population_file=population_file,
50
+ population_file_type=population_file_type,
51
+ waveform_model=waveform_model,
52
+ waveform_arguments=waveform_arguments,
53
+ start_time=start_time,
54
+ duration=duration,
55
+ sampling_frequency=sampling_frequency,
56
+ max_samples=max_samples,
57
+ dtype=dtype,
58
+ detectors=detectors,
59
+ minimum_frequency=minimum_frequency,
60
+ **kwargs,
61
+ )
@@ -0,0 +1,7 @@
1
+ """Simulator module for GWSim."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .base import Simulator
6
+
7
+ __all__ = ["Simulator"]