Skip to content

Simulator

gwmock_signal.simulator

Abstract base class and CBC concrete implementation for GW signal simulation.

CBCSimulator

Bases: TransientSimulator

Compact binary coalescence simulator backed by WaveformFactory.

Generates time-domain polarizations via a pluggable waveform backend, projects them onto the requested detectors, and injects them into background strain. waveform_model is a CBC concern and is supplied at construction time, keeping the base-class simulate interface source-agnostic.

The public interface accepts gwmock-pop canonical parameter names (e.g. detector_frame_mass_1, coa_time, polarization_angle). Backend-specific translation is handled internally and is invisible to callers.

Parameters:

Name Type Description Default
waveform_model str

Time-domain approximant name or any model registered with WaveformFactory (e.g. 'IMRPhenomD').

required
waveform_backend WaveformBackend | None

Optional waveform backend instance. Defaults to LALSimulationBackend through WaveformFactory.

None
Source code in src/gwmock_signal/simulator.py
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
class CBCSimulator(TransientSimulator):
    """Compact binary coalescence simulator backed by ``WaveformFactory``.

    Generates time-domain polarizations via a pluggable waveform backend,
    projects them onto the requested detectors, and injects them into background
    strain. ``waveform_model`` is a CBC concern and is supplied at construction
    time, keeping the base-class ``simulate`` interface source-agnostic.

    The public interface accepts **gwmock-pop canonical parameter names**
    (e.g. ``detector_frame_mass_1``, ``coa_time``, ``polarization_angle``).
    Backend-specific translation is handled internally and is invisible to callers.

    Args:
        waveform_model: Time-domain approximant name or any model
            registered with ``WaveformFactory`` (e.g. ``'IMRPhenomD'``).
        waveform_backend: Optional waveform backend instance. Defaults to
            ``LALSimulationBackend`` through ``WaveformFactory``.
    """

    #: Minimum parameter keys required by the CBC pipeline (gwmock-pop canonical names).
    _REQUIRED: frozenset[str] = frozenset(
        {
            "detector_frame_mass_1",
            "detector_frame_mass_2",
            "coa_time",
            "distance",
            "inclination",
            "right_ascension",
            "declination",
            "polarization_angle",
        }
    )

    def __init__(self, waveform_model: str, waveform_backend: WaveformBackend | None = None) -> None:
        """Initialise with the waveform model name.

        Args:
            waveform_model: Time-domain approximant name or any model
                registered with ``WaveformFactory``.
            waveform_backend: Optional waveform backend instance.
        """
        self._waveform_model = waveform_model
        self._waveform_factory = WaveformFactory(backend=waveform_backend)

    @property
    def waveform_model(self) -> str:
        """PyCBC approximant or custom model name."""
        return self._waveform_model

    @property
    def required_params(self) -> frozenset[str]:
        """Return the fixed set of required CBC parameter keys."""
        return self._REQUIRED

    def generate_polarizations(
        self,
        params: Mapping[str, Any],
        sampling_frequency: float,
        minimum_frequency: float,
    ) -> tuple[TimeSeries, TimeSeries]:
        """Delegate waveform generation to ``WaveformFactory``.

        Projection-specific keys are excluded because they are consumed by
        ``TransientSimulator.simulate`` rather than by waveform generation.

        Args:
            params: CBC source parameters using gwmock-pop canonical names
                (e.g. ``detector_frame_mass_1``, ``coa_time``).
            sampling_frequency: Sample rate in Hz.
            minimum_frequency: Low-frequency cutoff in Hz.

        Returns:
            Tuple of ``(hp, hc)`` GWpy ``TimeSeries`` objects.
        """
        waveform_params = {
            k: v
            for k, v in params.items()
            if k not in {"right_ascension", "declination", "polarization_angle", "coa_time"}
        }

        result = self._waveform_factory.generate(
            self._waveform_model,
            waveform_params,
            tc=params["coa_time"],
            sampling_frequency=sampling_frequency,
            minimum_frequency=minimum_frequency,
        )
        return result["plus"], result["cross"]

    def write(  # noqa: PLR0913
        self,
        path: str | Path,
        params: Mapping[str, Any],
        detector_names: Sequence[str | CustomDetector],
        background: Mapping[str, TimeSeries],
        *,
        sampling_frequency: float,
        minimum_frequency: float,
        format: Literal["gwf", "hdf5", "npy", "txt"] = "hdf5",  # noqa: A002
        earth_rotation: bool = True,
        interpolate_if_offset: bool = True,
    ) -> DetectorStrainStack:
        """Simulate a CBC injection, write the result, and save the parameter dict.

        Calls :meth:`simulate`, writes the returned ``DetectorStrainStack`` to
        *path* via :meth:`DetectorStrainStack.write`, and saves *params* as a
        JSON sidecar at ``<stem>_params.json`` next to *path*.

        Args:
            path: Output file path.
            params: CBC source parameters (same as :meth:`simulate`).
            detector_names: IFO codes for the target network.
            background: Mapping of detector name to background ``TimeSeries``.
            sampling_frequency: Sample rate in Hz.
            minimum_frequency: Low-frequency cutoff in Hz.
            format: Output format for the strain data — one of ``'gwf'``,
                ``'hdf5'``, ``'npy'``, or ``'txt'``.  Defaults to ``'hdf5'``.
            earth_rotation: Passed through to :meth:`simulate`.
            interpolate_if_offset: Passed through to :meth:`simulate`.

        Returns:
            The ``DetectorStrainStack`` that was written to disk.
        """
        result = self.simulate(
            params,
            detector_names,
            background,
            sampling_frequency=sampling_frequency,
            minimum_frequency=minimum_frequency,
            earth_rotation=earth_rotation,
            interpolate_if_offset=interpolate_if_offset,
        )
        result.write(path, format=format)

        params_path = Path(path).with_name(Path(path).stem + "_params.json")
        params_path.write_text(json.dumps(dict(params), default=_json_default, indent=4))

        return result

required_params property

Return the fixed set of required CBC parameter keys.

waveform_model property

PyCBC approximant or custom model name.

__init__(waveform_model, waveform_backend=None)

Initialise with the waveform model name.

Parameters:

Name Type Description Default
waveform_model str

Time-domain approximant name or any model registered with WaveformFactory.

required
waveform_backend WaveformBackend | None

Optional waveform backend instance.

None
Source code in src/gwmock_signal/simulator.py
354
355
356
357
358
359
360
361
362
363
def __init__(self, waveform_model: str, waveform_backend: WaveformBackend | None = None) -> None:
    """Initialise with the waveform model name.

    Args:
        waveform_model: Time-domain approximant name or any model
            registered with ``WaveformFactory``.
        waveform_backend: Optional waveform backend instance.
    """
    self._waveform_model = waveform_model
    self._waveform_factory = WaveformFactory(backend=waveform_backend)

generate_polarizations(params, sampling_frequency, minimum_frequency)

Delegate waveform generation to WaveformFactory.

Projection-specific keys are excluded because they are consumed by TransientSimulator.simulate rather than by waveform generation.

Parameters:

Name Type Description Default
params Mapping[str, Any]

CBC source parameters using gwmock-pop canonical names (e.g. detector_frame_mass_1, coa_time).

required
sampling_frequency float

Sample rate in Hz.

required
minimum_frequency float

Low-frequency cutoff in Hz.

required

Returns:

Type Description
tuple[TimeSeries, TimeSeries]

Tuple of (hp, hc) GWpy TimeSeries objects.

Source code in src/gwmock_signal/simulator.py
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
def generate_polarizations(
    self,
    params: Mapping[str, Any],
    sampling_frequency: float,
    minimum_frequency: float,
) -> tuple[TimeSeries, TimeSeries]:
    """Delegate waveform generation to ``WaveformFactory``.

    Projection-specific keys are excluded because they are consumed by
    ``TransientSimulator.simulate`` rather than by waveform generation.

    Args:
        params: CBC source parameters using gwmock-pop canonical names
            (e.g. ``detector_frame_mass_1``, ``coa_time``).
        sampling_frequency: Sample rate in Hz.
        minimum_frequency: Low-frequency cutoff in Hz.

    Returns:
        Tuple of ``(hp, hc)`` GWpy ``TimeSeries`` objects.
    """
    waveform_params = {
        k: v
        for k, v in params.items()
        if k not in {"right_ascension", "declination", "polarization_angle", "coa_time"}
    }

    result = self._waveform_factory.generate(
        self._waveform_model,
        waveform_params,
        tc=params["coa_time"],
        sampling_frequency=sampling_frequency,
        minimum_frequency=minimum_frequency,
    )
    return result["plus"], result["cross"]

register_waveform_model(name, factory)

Register a one-shot waveform generator under name for this instance.

Re-registering the same name with an equal factory is a no-op. Reusing the name for a different factory raises ValueError.

Parameters:

Name Type Description Default
name str

Waveform model name used later as waveform_model.

required
factory RegisteredWaveformFactory

Callable returning either (plus, cross) or a {"plus": ..., "cross": ...} mapping, or a WaveformBackend whose generate_td_waveform method should serve that name.

required
Source code in src/gwmock_signal/simulator.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def register_waveform_model(
    self,
    name: str,
    factory: RegisteredWaveformFactory,
) -> None:
    """Register a one-shot waveform generator under ``name`` for this instance.

    Re-registering the same name with an equal factory is a no-op. Reusing
    the name for a different factory raises ``ValueError``.

    Args:
        name: Waveform model name used later as ``waveform_model``.
        factory: Callable returning either ``(plus, cross)`` or a
            ``{"plus": ..., "cross": ...}`` mapping, or a ``WaveformBackend``
            whose ``generate_td_waveform`` method should serve that name.
    """
    if not hasattr(self, "_waveform_factory"):
        raise AttributeError(
            "TransientSimulator subclasses must initialize _waveform_factory before registering waveform models."
        )

    registered_factories = self.__dict__.setdefault("_registered_waveform_models", {})
    if name in registered_factories:
        if registered_factories[name] == factory:
            return
        raise ValueError(f"Waveform model {name!r} is already registered for this simulator instance.")

    try:
        self._waveform_factory.get_model(name)
    except ValueError:
        pass
    else:
        raise ValueError(f"Waveform model {name!r} is already registered for this simulator instance.")

    if not isinstance(factory, WaveformBackend) and not callable(factory):
        raise TypeError(f"Waveform model {name!r} must be registered with a callable or WaveformBackend.")

    wrapped_factory = (
        _wrap_registered_waveform_backend(name, factory)
        if isinstance(factory, WaveformBackend)
        else _wrap_registered_waveform_callable(factory)
    )
    self._waveform_factory.register_model(name, wrapped_factory)
    registered_factories[name] = factory

simulate(params, detector_names, background=None, *, sampling_frequency, minimum_frequency, earth_rotation=True, interpolate_if_offset=True)

Run the full injection pipeline and return a DetectorStrainStack.

Calls _validate_params, generate_polarizations, project_polarizations_to_network, and inject_strain in order, then assembles the result into a DetectorStrainStack.

Parameters:

Name Type Description Default
params Mapping[str, Any]

Source parameters; must contain all required_params keys plus right_ascension, declination, and polarization_angle for antenna-response projection.

required
detector_names Sequence[str | CustomDetector]

IFO codes (e.g. 'H1', 'L1', 'V1').

required
background Mapping[str, TimeSeries] | None

Optional mapping of detector name to background TimeSeries (e.g. noise or zeros). If omitted, the projected detector response is returned directly on the waveform time grid.

None
sampling_frequency float

Sample rate in Hz.

required
minimum_frequency float

Low-frequency cutoff in Hz.

required
earth_rotation bool

If True, evaluate antenna patterns at time-dependent GPS times (recommended for longer signals). If False, use a single reference time.

True
interpolate_if_offset bool

If True, interpolate the injection when its start is not on a target sample boundary.

True

Returns:

Type Description
DetectorStrainStack

DetectorStrainStack containing the simulated strain for each

DetectorStrainStack

detector in detector_names order.

Source code in src/gwmock_signal/simulator.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
def simulate(  # noqa: PLR0913
    self,
    params: Mapping[str, Any],
    detector_names: Sequence[str | CustomDetector],
    background: Mapping[str, TimeSeries] | None = None,
    *,
    sampling_frequency: float,
    minimum_frequency: float,
    earth_rotation: bool = True,
    interpolate_if_offset: bool = True,
) -> DetectorStrainStack:
    """Run the full injection pipeline and return a ``DetectorStrainStack``.

    Calls ``_validate_params``, ``generate_polarizations``,
    ``project_polarizations_to_network``, and ``inject_strain`` in order,
    then assembles the result into a ``DetectorStrainStack``.

    Args:
        params: Source parameters; must contain all ``required_params`` keys
            plus ``right_ascension``, ``declination``, and ``polarization_angle``
            for antenna-response projection.
        detector_names: IFO codes (e.g. ``'H1'``, ``'L1'``, ``'V1'``).
        background: Optional mapping of detector name to background
            ``TimeSeries`` (e.g. noise or zeros). If omitted, the projected
            detector response is returned directly on the waveform time grid.
        sampling_frequency: Sample rate in Hz.
        minimum_frequency: Low-frequency cutoff in Hz.
        earth_rotation: If ``True``, evaluate antenna patterns at
            time-dependent GPS times (recommended for longer signals).
            If ``False``, use a single reference time.
        interpolate_if_offset: If ``True``, interpolate the injection
            when its start is not on a target sample boundary.

    Returns:
        ``DetectorStrainStack`` containing the simulated strain for each
        detector in ``detector_names`` order.
    """
    self._validate_params(params)

    # Validate projection keys before direct params[...] access.
    # _validate_params only checks subclass-defined required_params. Non-CBC subclasses can pass validation and then fail mid-pipeline with KeyError.
    projection_keys = {"right_ascension", "declination", "polarization_angle"}
    missing_projection = projection_keys - set(params)
    if missing_projection:
        raise ValueError(f"Missing required parameters: {sorted(missing_projection)}")

    # Normalise detector entries so dict lookups use plain string keys.
    str_names: list[str] = [d.name if hasattr(d, "name") and not isinstance(d, str) else d for d in detector_names]

    hp, hc = self.generate_polarizations(params, sampling_frequency, minimum_frequency)
    projected = project_polarizations_to_network(
        {"plus": hp, "cross": hc},
        detector_names,
        right_ascension=params["right_ascension"],
        declination=params["declination"],
        polarization_angle=params["polarization_angle"],
        earth_rotation=earth_rotation,
    )
    if background is None:
        return DetectorStrainStack.from_mapping(str_names, projected)
    injected = {
        name: inject_strain(
            background[name],
            projected[name],
            interpolate_if_offset=interpolate_if_offset,
        )
        for name in str_names
    }
    return DetectorStrainStack.from_mapping(str_names, injected)

write(path, params, detector_names, background, *, sampling_frequency, minimum_frequency, format='hdf5', earth_rotation=True, interpolate_if_offset=True)

Simulate a CBC injection, write the result, and save the parameter dict.

Calls :meth:simulate, writes the returned DetectorStrainStack to path via :meth:DetectorStrainStack.write, and saves params as a JSON sidecar at <stem>_params.json next to path.

Parameters:

Name Type Description Default
path str | Path

Output file path.

required
params Mapping[str, Any]

CBC source parameters (same as :meth:simulate).

required
detector_names Sequence[str | CustomDetector]

IFO codes for the target network.

required
background Mapping[str, TimeSeries]

Mapping of detector name to background TimeSeries.

required
sampling_frequency float

Sample rate in Hz.

required
minimum_frequency float

Low-frequency cutoff in Hz.

required
format Literal['gwf', 'hdf5', 'npy', 'txt']

Output format for the strain data — one of 'gwf', 'hdf5', 'npy', or 'txt'. Defaults to 'hdf5'.

'hdf5'
earth_rotation bool

Passed through to :meth:simulate.

True
interpolate_if_offset bool

Passed through to :meth:simulate.

True

Returns:

Type Description
DetectorStrainStack

The DetectorStrainStack that was written to disk.

Source code in src/gwmock_signal/simulator.py
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
def write(  # noqa: PLR0913
    self,
    path: str | Path,
    params: Mapping[str, Any],
    detector_names: Sequence[str | CustomDetector],
    background: Mapping[str, TimeSeries],
    *,
    sampling_frequency: float,
    minimum_frequency: float,
    format: Literal["gwf", "hdf5", "npy", "txt"] = "hdf5",  # noqa: A002
    earth_rotation: bool = True,
    interpolate_if_offset: bool = True,
) -> DetectorStrainStack:
    """Simulate a CBC injection, write the result, and save the parameter dict.

    Calls :meth:`simulate`, writes the returned ``DetectorStrainStack`` to
    *path* via :meth:`DetectorStrainStack.write`, and saves *params* as a
    JSON sidecar at ``<stem>_params.json`` next to *path*.

    Args:
        path: Output file path.
        params: CBC source parameters (same as :meth:`simulate`).
        detector_names: IFO codes for the target network.
        background: Mapping of detector name to background ``TimeSeries``.
        sampling_frequency: Sample rate in Hz.
        minimum_frequency: Low-frequency cutoff in Hz.
        format: Output format for the strain data — one of ``'gwf'``,
            ``'hdf5'``, ``'npy'``, or ``'txt'``.  Defaults to ``'hdf5'``.
        earth_rotation: Passed through to :meth:`simulate`.
        interpolate_if_offset: Passed through to :meth:`simulate`.

    Returns:
        The ``DetectorStrainStack`` that was written to disk.
    """
    result = self.simulate(
        params,
        detector_names,
        background,
        sampling_frequency=sampling_frequency,
        minimum_frequency=minimum_frequency,
        earth_rotation=earth_rotation,
        interpolate_if_offset=interpolate_if_offset,
    )
    result.write(path, format=format)

    params_path = Path(path).with_name(Path(path).stem + "_params.json")
    params_path.write_text(json.dumps(dict(params), default=_json_default, indent=4))

    return result

GWSimulator

Bases: ABC

Abstract base class for gravitational-wave signal simulators.

Defines the stable, source-agnostic contract used across packages: subclasses must implement required_params and simulate and must return a DetectorStrainStack. _validate_params is provided as a concrete helper.

See docs/api/simulator/index.md for the API reference.

Source code in src/gwmock_signal/simulator.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
class GWSimulator(ABC):
    """Abstract base class for gravitational-wave signal simulators.

    Defines the stable, source-agnostic contract used across packages:
    subclasses must implement ``required_params`` and ``simulate`` and must
    return a ``DetectorStrainStack``. ``_validate_params`` is provided as a
    concrete helper.

    See ``docs/api/simulator/index.md`` for the API reference.
    """

    @property
    @abstractmethod
    def required_params(self) -> frozenset[str]:
        """Parameter keys that must be present in the params dict passed to ``simulate``."""

    def _validate_params(self, params: Mapping[str, Any]) -> None:
        """Raise ``ValueError`` naming any key in ``required_params`` missing from ``params``.

        Args:
            params: Source parameters to validate.

        Raises:
            ValueError: If any required key is absent, naming each missing key.
        """
        missing = self.required_params - set(params)
        if missing:
            raise ValueError(f"Missing required parameters: {sorted(missing)}")

    @abstractmethod
    def simulate(  # noqa: PLR0913
        self,
        params: Mapping[str, Any],
        detector_names: Sequence[str],
        background: Mapping[str, TimeSeries] | None = None,
        *,
        sampling_frequency: float,
        minimum_frequency: float,
        earth_rotation: bool = True,
        interpolate_if_offset: bool = True,
    ) -> DetectorStrainStack:
        """Return detector strain for one source realization as a ``DetectorStrainStack``.

        Args:
            params: Source parameters.
            detector_names: IFO codes (e.g. ``'H1'``, ``'L1'``, ``'V1'``).
            background: Optional mapping of detector name to existing background
                strain. Non-transient subclasses may use this to inject into
                pre-existing data; transient subclasses may ignore it and return
                signal-only strain directly.
            sampling_frequency: Sample rate in Hz.
            minimum_frequency: Low-frequency cutoff in Hz.
            earth_rotation: Optional backend-specific flag controlling whether
                detector response should vary across the signal duration.
            interpolate_if_offset: Optional backend-specific flag controlling
                how off-grid injections should be handled.

        Returns:
            ``DetectorStrainStack`` containing one aligned strain channel per
            detector in ``detector_names`` order.
        """

required_params abstractmethod property

Parameter keys that must be present in the params dict passed to simulate.

simulate(params, detector_names, background=None, *, sampling_frequency, minimum_frequency, earth_rotation=True, interpolate_if_offset=True) abstractmethod

Return detector strain for one source realization as a DetectorStrainStack.

Parameters:

Name Type Description Default
params Mapping[str, Any]

Source parameters.

required
detector_names Sequence[str]

IFO codes (e.g. 'H1', 'L1', 'V1').

required
background Mapping[str, TimeSeries] | None

Optional mapping of detector name to existing background strain. Non-transient subclasses may use this to inject into pre-existing data; transient subclasses may ignore it and return signal-only strain directly.

None
sampling_frequency float

Sample rate in Hz.

required
minimum_frequency float

Low-frequency cutoff in Hz.

required
earth_rotation bool

Optional backend-specific flag controlling whether detector response should vary across the signal duration.

True
interpolate_if_offset bool

Optional backend-specific flag controlling how off-grid injections should be handled.

True

Returns:

Type Description
DetectorStrainStack

DetectorStrainStack containing one aligned strain channel per

DetectorStrainStack

detector in detector_names order.

Source code in src/gwmock_signal/simulator.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
@abstractmethod
def simulate(  # noqa: PLR0913
    self,
    params: Mapping[str, Any],
    detector_names: Sequence[str],
    background: Mapping[str, TimeSeries] | None = None,
    *,
    sampling_frequency: float,
    minimum_frequency: float,
    earth_rotation: bool = True,
    interpolate_if_offset: bool = True,
) -> DetectorStrainStack:
    """Return detector strain for one source realization as a ``DetectorStrainStack``.

    Args:
        params: Source parameters.
        detector_names: IFO codes (e.g. ``'H1'``, ``'L1'``, ``'V1'``).
        background: Optional mapping of detector name to existing background
            strain. Non-transient subclasses may use this to inject into
            pre-existing data; transient subclasses may ignore it and return
            signal-only strain directly.
        sampling_frequency: Sample rate in Hz.
        minimum_frequency: Low-frequency cutoff in Hz.
        earth_rotation: Optional backend-specific flag controlling whether
            detector response should vary across the signal duration.
        interpolate_if_offset: Optional backend-specific flag controlling
            how off-grid injections should be handled.

    Returns:
        ``DetectorStrainStack`` containing one aligned strain channel per
        detector in ``detector_names`` order.
    """

TransientSimulator

Bases: GWSimulator

Intermediate base class for transient GW source simulators.

Provides the concrete simulate method that orchestrates the full transient injection pipeline: validation → polarizations → detector projection → strain injection → stacking. It also exposes register_waveform_model so each simulator instance can add custom waveform generators without poking private WaveformFactory attributes. Subclasses must implement generate_polarizations and required_params.

Source code in src/gwmock_signal/simulator.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
class TransientSimulator(GWSimulator):
    """Intermediate base class for transient GW source simulators.

    Provides the concrete ``simulate`` method that orchestrates the full
    transient injection pipeline: validation → polarizations → detector
    projection → strain injection → stacking. It also exposes
    ``register_waveform_model`` so each simulator instance can add custom
    waveform generators without poking private ``WaveformFactory`` attributes.
    Subclasses must implement ``generate_polarizations`` and ``required_params``.
    """

    @abstractmethod
    def generate_polarizations(
        self,
        params: Mapping[str, Any],
        sampling_frequency: float,
        minimum_frequency: float,
    ) -> tuple[TimeSeries, TimeSeries]:
        """Generate plus and cross polarization time series.

        Args:
            params: Source parameters.
            sampling_frequency: Sample rate in Hz.
            minimum_frequency: Low-frequency cutoff in Hz.

        Returns:
            Tuple of ``(hp, hc)`` GWpy ``TimeSeries`` objects.
        """

    def register_waveform_model(
        self,
        name: str,
        factory: RegisteredWaveformFactory,
    ) -> None:
        """Register a one-shot waveform generator under ``name`` for this instance.

        Re-registering the same name with an equal factory is a no-op. Reusing
        the name for a different factory raises ``ValueError``.

        Args:
            name: Waveform model name used later as ``waveform_model``.
            factory: Callable returning either ``(plus, cross)`` or a
                ``{"plus": ..., "cross": ...}`` mapping, or a ``WaveformBackend``
                whose ``generate_td_waveform`` method should serve that name.
        """
        if not hasattr(self, "_waveform_factory"):
            raise AttributeError(
                "TransientSimulator subclasses must initialize _waveform_factory before registering waveform models."
            )

        registered_factories = self.__dict__.setdefault("_registered_waveform_models", {})
        if name in registered_factories:
            if registered_factories[name] == factory:
                return
            raise ValueError(f"Waveform model {name!r} is already registered for this simulator instance.")

        try:
            self._waveform_factory.get_model(name)
        except ValueError:
            pass
        else:
            raise ValueError(f"Waveform model {name!r} is already registered for this simulator instance.")

        if not isinstance(factory, WaveformBackend) and not callable(factory):
            raise TypeError(f"Waveform model {name!r} must be registered with a callable or WaveformBackend.")

        wrapped_factory = (
            _wrap_registered_waveform_backend(name, factory)
            if isinstance(factory, WaveformBackend)
            else _wrap_registered_waveform_callable(factory)
        )
        self._waveform_factory.register_model(name, wrapped_factory)
        registered_factories[name] = factory

    def simulate(  # noqa: PLR0913
        self,
        params: Mapping[str, Any],
        detector_names: Sequence[str | CustomDetector],
        background: Mapping[str, TimeSeries] | None = None,
        *,
        sampling_frequency: float,
        minimum_frequency: float,
        earth_rotation: bool = True,
        interpolate_if_offset: bool = True,
    ) -> DetectorStrainStack:
        """Run the full injection pipeline and return a ``DetectorStrainStack``.

        Calls ``_validate_params``, ``generate_polarizations``,
        ``project_polarizations_to_network``, and ``inject_strain`` in order,
        then assembles the result into a ``DetectorStrainStack``.

        Args:
            params: Source parameters; must contain all ``required_params`` keys
                plus ``right_ascension``, ``declination``, and ``polarization_angle``
                for antenna-response projection.
            detector_names: IFO codes (e.g. ``'H1'``, ``'L1'``, ``'V1'``).
            background: Optional mapping of detector name to background
                ``TimeSeries`` (e.g. noise or zeros). If omitted, the projected
                detector response is returned directly on the waveform time grid.
            sampling_frequency: Sample rate in Hz.
            minimum_frequency: Low-frequency cutoff in Hz.
            earth_rotation: If ``True``, evaluate antenna patterns at
                time-dependent GPS times (recommended for longer signals).
                If ``False``, use a single reference time.
            interpolate_if_offset: If ``True``, interpolate the injection
                when its start is not on a target sample boundary.

        Returns:
            ``DetectorStrainStack`` containing the simulated strain for each
            detector in ``detector_names`` order.
        """
        self._validate_params(params)

        # Validate projection keys before direct params[...] access.
        # _validate_params only checks subclass-defined required_params. Non-CBC subclasses can pass validation and then fail mid-pipeline with KeyError.
        projection_keys = {"right_ascension", "declination", "polarization_angle"}
        missing_projection = projection_keys - set(params)
        if missing_projection:
            raise ValueError(f"Missing required parameters: {sorted(missing_projection)}")

        # Normalise detector entries so dict lookups use plain string keys.
        str_names: list[str] = [d.name if hasattr(d, "name") and not isinstance(d, str) else d for d in detector_names]

        hp, hc = self.generate_polarizations(params, sampling_frequency, minimum_frequency)
        projected = project_polarizations_to_network(
            {"plus": hp, "cross": hc},
            detector_names,
            right_ascension=params["right_ascension"],
            declination=params["declination"],
            polarization_angle=params["polarization_angle"],
            earth_rotation=earth_rotation,
        )
        if background is None:
            return DetectorStrainStack.from_mapping(str_names, projected)
        injected = {
            name: inject_strain(
                background[name],
                projected[name],
                interpolate_if_offset=interpolate_if_offset,
            )
            for name in str_names
        }
        return DetectorStrainStack.from_mapping(str_names, injected)

required_params abstractmethod property

Parameter keys that must be present in the params dict passed to simulate.

generate_polarizations(params, sampling_frequency, minimum_frequency) abstractmethod

Generate plus and cross polarization time series.

Parameters:

Name Type Description Default
params Mapping[str, Any]

Source parameters.

required
sampling_frequency float

Sample rate in Hz.

required
minimum_frequency float

Low-frequency cutoff in Hz.

required

Returns:

Type Description
tuple[TimeSeries, TimeSeries]

Tuple of (hp, hc) GWpy TimeSeries objects.

Source code in src/gwmock_signal/simulator.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
@abstractmethod
def generate_polarizations(
    self,
    params: Mapping[str, Any],
    sampling_frequency: float,
    minimum_frequency: float,
) -> tuple[TimeSeries, TimeSeries]:
    """Generate plus and cross polarization time series.

    Args:
        params: Source parameters.
        sampling_frequency: Sample rate in Hz.
        minimum_frequency: Low-frequency cutoff in Hz.

    Returns:
        Tuple of ``(hp, hc)`` GWpy ``TimeSeries`` objects.
    """

register_waveform_model(name, factory)

Register a one-shot waveform generator under name for this instance.

Re-registering the same name with an equal factory is a no-op. Reusing the name for a different factory raises ValueError.

Parameters:

Name Type Description Default
name str

Waveform model name used later as waveform_model.

required
factory RegisteredWaveformFactory

Callable returning either (plus, cross) or a {"plus": ..., "cross": ...} mapping, or a WaveformBackend whose generate_td_waveform method should serve that name.

required
Source code in src/gwmock_signal/simulator.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def register_waveform_model(
    self,
    name: str,
    factory: RegisteredWaveformFactory,
) -> None:
    """Register a one-shot waveform generator under ``name`` for this instance.

    Re-registering the same name with an equal factory is a no-op. Reusing
    the name for a different factory raises ``ValueError``.

    Args:
        name: Waveform model name used later as ``waveform_model``.
        factory: Callable returning either ``(plus, cross)`` or a
            ``{"plus": ..., "cross": ...}`` mapping, or a ``WaveformBackend``
            whose ``generate_td_waveform`` method should serve that name.
    """
    if not hasattr(self, "_waveform_factory"):
        raise AttributeError(
            "TransientSimulator subclasses must initialize _waveform_factory before registering waveform models."
        )

    registered_factories = self.__dict__.setdefault("_registered_waveform_models", {})
    if name in registered_factories:
        if registered_factories[name] == factory:
            return
        raise ValueError(f"Waveform model {name!r} is already registered for this simulator instance.")

    try:
        self._waveform_factory.get_model(name)
    except ValueError:
        pass
    else:
        raise ValueError(f"Waveform model {name!r} is already registered for this simulator instance.")

    if not isinstance(factory, WaveformBackend) and not callable(factory):
        raise TypeError(f"Waveform model {name!r} must be registered with a callable or WaveformBackend.")

    wrapped_factory = (
        _wrap_registered_waveform_backend(name, factory)
        if isinstance(factory, WaveformBackend)
        else _wrap_registered_waveform_callable(factory)
    )
    self._waveform_factory.register_model(name, wrapped_factory)
    registered_factories[name] = factory

simulate(params, detector_names, background=None, *, sampling_frequency, minimum_frequency, earth_rotation=True, interpolate_if_offset=True)

Run the full injection pipeline and return a DetectorStrainStack.

Calls _validate_params, generate_polarizations, project_polarizations_to_network, and inject_strain in order, then assembles the result into a DetectorStrainStack.

Parameters:

Name Type Description Default
params Mapping[str, Any]

Source parameters; must contain all required_params keys plus right_ascension, declination, and polarization_angle for antenna-response projection.

required
detector_names Sequence[str | CustomDetector]

IFO codes (e.g. 'H1', 'L1', 'V1').

required
background Mapping[str, TimeSeries] | None

Optional mapping of detector name to background TimeSeries (e.g. noise or zeros). If omitted, the projected detector response is returned directly on the waveform time grid.

None
sampling_frequency float

Sample rate in Hz.

required
minimum_frequency float

Low-frequency cutoff in Hz.

required
earth_rotation bool

If True, evaluate antenna patterns at time-dependent GPS times (recommended for longer signals). If False, use a single reference time.

True
interpolate_if_offset bool

If True, interpolate the injection when its start is not on a target sample boundary.

True

Returns:

Type Description
DetectorStrainStack

DetectorStrainStack containing the simulated strain for each

DetectorStrainStack

detector in detector_names order.

Source code in src/gwmock_signal/simulator.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
def simulate(  # noqa: PLR0913
    self,
    params: Mapping[str, Any],
    detector_names: Sequence[str | CustomDetector],
    background: Mapping[str, TimeSeries] | None = None,
    *,
    sampling_frequency: float,
    minimum_frequency: float,
    earth_rotation: bool = True,
    interpolate_if_offset: bool = True,
) -> DetectorStrainStack:
    """Run the full injection pipeline and return a ``DetectorStrainStack``.

    Calls ``_validate_params``, ``generate_polarizations``,
    ``project_polarizations_to_network``, and ``inject_strain`` in order,
    then assembles the result into a ``DetectorStrainStack``.

    Args:
        params: Source parameters; must contain all ``required_params`` keys
            plus ``right_ascension``, ``declination``, and ``polarization_angle``
            for antenna-response projection.
        detector_names: IFO codes (e.g. ``'H1'``, ``'L1'``, ``'V1'``).
        background: Optional mapping of detector name to background
            ``TimeSeries`` (e.g. noise or zeros). If omitted, the projected
            detector response is returned directly on the waveform time grid.
        sampling_frequency: Sample rate in Hz.
        minimum_frequency: Low-frequency cutoff in Hz.
        earth_rotation: If ``True``, evaluate antenna patterns at
            time-dependent GPS times (recommended for longer signals).
            If ``False``, use a single reference time.
        interpolate_if_offset: If ``True``, interpolate the injection
            when its start is not on a target sample boundary.

    Returns:
        ``DetectorStrainStack`` containing the simulated strain for each
        detector in ``detector_names`` order.
    """
    self._validate_params(params)

    # Validate projection keys before direct params[...] access.
    # _validate_params only checks subclass-defined required_params. Non-CBC subclasses can pass validation and then fail mid-pipeline with KeyError.
    projection_keys = {"right_ascension", "declination", "polarization_angle"}
    missing_projection = projection_keys - set(params)
    if missing_projection:
        raise ValueError(f"Missing required parameters: {sorted(missing_projection)}")

    # Normalise detector entries so dict lookups use plain string keys.
    str_names: list[str] = [d.name if hasattr(d, "name") and not isinstance(d, str) else d for d in detector_names]

    hp, hc = self.generate_polarizations(params, sampling_frequency, minimum_frequency)
    projected = project_polarizations_to_network(
        {"plus": hp, "cross": hc},
        detector_names,
        right_ascension=params["right_ascension"],
        declination=params["declination"],
        polarization_angle=params["polarization_angle"],
        earth_rotation=earth_rotation,
    )
    if background is None:
        return DetectorStrainStack.from_mapping(str_names, projected)
    injected = {
        name: inject_strain(
            background[name],
            projected[name],
            interpolate_if_offset=interpolate_if_offset,
        )
        for name in str_names
    }
    return DetectorStrainStack.from_mapping(str_names, injected)

GWSimulator.simulate(...) is the stable cross-package boundary. Custom backends should accept the documented keyword arguments, may use background=None or inject into a provided background, and must return a DetectorStrainStack.

For registry-based construction (gwmock-pop source_type strings), see Registry. For one-shot CBC injection without instantiating a simulator, see Pipeline (inject_cbc_signal).

TransientSimulator also exposes register_waveform_model(name, factory) for per-instance waveform registration. Use this when downstream orchestration needs to inject a custom callable waveform without reaching into private _waveform_factory state.

For narrative examples (waveforms → projection → injection), see the user guide overview. For an extensibility-oriented walkthrough, see Custom backends.