Skip to content

Stochastic

gwmock_signal.stochastic

Stochastic gravitational-wave background signal models.

StochasticBackgroundSimulator

Bases: GWSimulator

Generate isotropic SGWB detector strain as a correlated Gaussian signal.

The simulator samples Fourier coefficients from a one-sided detector covariance C_ij(f) = gamma_ij(f) S_h(f), where S_h(f) is derived from a power-law Omega_GW spectrum. It returns signal-only strain by default or adds the SGWB signal to an optional background mapping.

Parameters:

Name Type Description Default
duration float

Signal duration in seconds.

required
seed int | None

Optional random seed.

None
overlap_reduction OverlapReductionInput | None

Optional pairwise ORF arrays or callable. When omitted, the long-wavelength geometric ORF is used.

None
regularization_epsilon float

Positive diagonal regularization scale passed to the spectral Cholesky builder.

1e-12
Source code in src/gwmock_signal/stochastic/simulator.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
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
174
175
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
class StochasticBackgroundSimulator(GWSimulator):
    """Generate isotropic SGWB detector strain as a correlated Gaussian signal.

    The simulator samples Fourier coefficients from a one-sided detector
    covariance ``C_ij(f) = gamma_ij(f) S_h(f)``, where ``S_h(f)`` is derived
    from a power-law ``Omega_GW`` spectrum. It returns signal-only strain by
    default or adds the SGWB signal to an optional background mapping.

    Args:
        duration: Signal duration in seconds.
        seed: Optional random seed.
        overlap_reduction: Optional pairwise ORF arrays or callable. When
            omitted, the long-wavelength geometric ORF is used.
        regularization_epsilon: Positive diagonal regularization scale passed
            to the spectral Cholesky builder.
    """

    _REQUIRED: frozenset[str] = frozenset({"omega_ref"})

    def __init__(
        self,
        *,
        duration: float,
        seed: int | None = None,
        overlap_reduction: OverlapReductionInput | None = None,
        regularization_epsilon: float = 1.0e-12,
    ) -> None:
        """Initialize the SGWB simulator."""
        if duration <= 0.0:
            raise ValueError("duration must be positive.")
        if regularization_epsilon <= 0.0:
            raise ValueError("regularization_epsilon must be positive.")
        self.duration = duration
        self.seed = seed
        self.overlap_reduction = overlap_reduction
        self.regularization_epsilon = regularization_epsilon

    @property
    def required_params(self) -> frozenset[str]:
        """Return required SGWB spectrum parameter keys."""
        return self._REQUIRED

    def simulate(  # noqa: PLR0913
        self,
        params: Mapping[str, Any],
        detector_names: Sequence[DetectorSpec],
        background: Mapping[str, TimeSeries] | None = None,
        *,
        sampling_frequency: float,
        minimum_frequency: float,
        earth_rotation: bool = True,
        interpolate_if_offset: bool = True,
    ) -> DetectorStrainStack:
        """Generate SGWB strain on a fixed detector network."""
        del earth_rotation, interpolate_if_offset
        self._validate_params(params)
        if sampling_frequency <= 0.0:
            raise ValueError("sampling_frequency must be positive.")
        if minimum_frequency < 0.0:
            raise ValueError("minimum_frequency must be non-negative.")

        names = normalize_detector_names(detector_names)
        n_samples = round(self.duration * sampling_frequency)
        if n_samples <= 0:
            raise ValueError("duration and sampling_frequency must produce at least one sample.")

        frequency_grid = np.fft.rfftfreq(n_samples, d=1.0 / sampling_frequency)
        frequency_mask = frequency_grid >= minimum_frequency
        if not np.any(frequency_mask):
            raise ValueError("minimum_frequency leaves no FFT bins to simulate.")

        masked_frequencies = frequency_grid[frequency_mask]
        delta_frequency = sampling_frequency / n_samples
        spectrum = StochasticBackgroundSpectrum(
            omega_ref=float(params["omega_ref"]),
            spectral_index=float(params.get("spectral_index", 0.0)),
            reference_frequency=float(params.get("reference_frequency", 25.0)),
            hubble_constant_si=float(params.get("hubble_constant_si", H0_SI)),
        )
        strain_psd = spectrum.strain_psd(masked_frequencies)
        psd = dict.fromkeys(names, strain_psd)
        overlap_reduction = normalize_overlap_reduction(
            self.overlap_reduction,
            detectors=detector_names,
            names=names,
            frequencies=masked_frequencies,
        )
        csd = {pair: gamma * strain_psd for pair, gamma in overlap_reduction.items()}
        factors = self._spectral_factors(psd, csd, names=names, delta_frequency=delta_frequency)
        signal = self._sample_signal(
            np.random.default_rng(self.seed),
            factors,
            names=names,
            frequency_grid_size=frequency_grid.size,
            frequency_mask=frequency_mask,
            delta_frequency=delta_frequency,
            n_samples=n_samples,
        )
        return self._to_stack(signal, names, background=background, sampling_frequency=sampling_frequency)

    def _spectral_factors(
        self,
        psd: Mapping[str, np.ndarray],
        csd: Mapping[tuple[str, str], np.ndarray],
        *,
        names: Sequence[str],
        delta_frequency: float,
    ) -> np.ndarray:
        """Build spectral Cholesky factors using the optional gwmock-noise extra."""
        try:
            from gwmock_noise.spectral import (  # noqa: PLC0415
                assemble_hermitian_spectral_matrices,
                cholesky_factors_from_spectral_matrices,
            )
        except ModuleNotFoundError as exc:
            raise ModuleNotFoundError("Install gwmock-signal[sgwb] to simulate stochastic backgrounds.") from exc

        matrices = assemble_hermitian_spectral_matrices(names, psd, csd)
        return cholesky_factors_from_spectral_matrices(
            matrices,
            delta_frequency=delta_frequency,
            regularization_epsilon=self.regularization_epsilon,
        )

    def _sample_signal(  # noqa: PLR0913
        self,
        rng: np.random.Generator,
        factors: np.ndarray,
        *,
        names: Sequence[str],
        frequency_grid_size: int,
        frequency_mask: np.ndarray,
        delta_frequency: float,
        n_samples: int,
    ) -> Mapping[str, np.ndarray]:
        """Sample real detector strain arrays using the optional gwmock-noise extra."""
        try:
            from gwmock_noise.spectral import simulate_spectral_covariance_chunk  # noqa: PLC0415
        except ModuleNotFoundError as exc:
            raise ModuleNotFoundError("Install gwmock-signal[sgwb] to simulate stochastic backgrounds.") from exc

        return simulate_spectral_covariance_chunk(
            rng,
            factors,
            detectors=names,
            frequency_grid_size=frequency_grid_size,
            frequency_mask=frequency_mask,
            delta_frequency=delta_frequency,
            window_size=n_samples,
        )

    def _to_stack(
        self,
        signal: Mapping[str, np.ndarray],
        names: Sequence[str],
        *,
        background: Mapping[str, TimeSeries] | None,
        sampling_frequency: float,
    ) -> DetectorStrainStack:
        """Convert sampled arrays to a detector stack, adding background if supplied."""
        t0 = 0.0 if background is None else float(next(iter(background.values())).t0.value)
        if background is not None:
            for detector in names:
                if detector not in background:
                    raise KeyError(f"Missing background for detector {detector!r}.")
                if len(background[detector]) != len(signal[detector]):
                    raise ValueError("background channels must match the SGWB sample count.")
            strains = {
                detector: background[detector]
                + TimeSeries(signal[detector], t0=t0, sample_rate=sampling_frequency, unit="strain")
                for detector in names
            }
        else:
            strains = {
                detector: TimeSeries(
                    signal[detector],
                    t0=t0,
                    sample_rate=sampling_frequency,
                    unit="strain",
                    name=detector,
                )
                for detector in names
            }
        return DetectorStrainStack.from_mapping(names, strains)

required_params property

Return required SGWB spectrum parameter keys.

__init__(*, duration, seed=None, overlap_reduction=None, regularization_epsilon=1e-12)

Initialize the SGWB simulator.

Source code in src/gwmock_signal/stochastic/simulator.py
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def __init__(
    self,
    *,
    duration: float,
    seed: int | None = None,
    overlap_reduction: OverlapReductionInput | None = None,
    regularization_epsilon: float = 1.0e-12,
) -> None:
    """Initialize the SGWB simulator."""
    if duration <= 0.0:
        raise ValueError("duration must be positive.")
    if regularization_epsilon <= 0.0:
        raise ValueError("regularization_epsilon must be positive.")
    self.duration = duration
    self.seed = seed
    self.overlap_reduction = overlap_reduction
    self.regularization_epsilon = regularization_epsilon

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

Generate SGWB strain on a fixed detector network.

Source code in src/gwmock_signal/stochastic/simulator.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def simulate(  # noqa: PLR0913
    self,
    params: Mapping[str, Any],
    detector_names: Sequence[DetectorSpec],
    background: Mapping[str, TimeSeries] | None = None,
    *,
    sampling_frequency: float,
    minimum_frequency: float,
    earth_rotation: bool = True,
    interpolate_if_offset: bool = True,
) -> DetectorStrainStack:
    """Generate SGWB strain on a fixed detector network."""
    del earth_rotation, interpolate_if_offset
    self._validate_params(params)
    if sampling_frequency <= 0.0:
        raise ValueError("sampling_frequency must be positive.")
    if minimum_frequency < 0.0:
        raise ValueError("minimum_frequency must be non-negative.")

    names = normalize_detector_names(detector_names)
    n_samples = round(self.duration * sampling_frequency)
    if n_samples <= 0:
        raise ValueError("duration and sampling_frequency must produce at least one sample.")

    frequency_grid = np.fft.rfftfreq(n_samples, d=1.0 / sampling_frequency)
    frequency_mask = frequency_grid >= minimum_frequency
    if not np.any(frequency_mask):
        raise ValueError("minimum_frequency leaves no FFT bins to simulate.")

    masked_frequencies = frequency_grid[frequency_mask]
    delta_frequency = sampling_frequency / n_samples
    spectrum = StochasticBackgroundSpectrum(
        omega_ref=float(params["omega_ref"]),
        spectral_index=float(params.get("spectral_index", 0.0)),
        reference_frequency=float(params.get("reference_frequency", 25.0)),
        hubble_constant_si=float(params.get("hubble_constant_si", H0_SI)),
    )
    strain_psd = spectrum.strain_psd(masked_frequencies)
    psd = dict.fromkeys(names, strain_psd)
    overlap_reduction = normalize_overlap_reduction(
        self.overlap_reduction,
        detectors=detector_names,
        names=names,
        frequencies=masked_frequencies,
    )
    csd = {pair: gamma * strain_psd for pair, gamma in overlap_reduction.items()}
    factors = self._spectral_factors(psd, csd, names=names, delta_frequency=delta_frequency)
    signal = self._sample_signal(
        np.random.default_rng(self.seed),
        factors,
        names=names,
        frequency_grid_size=frequency_grid.size,
        frequency_mask=frequency_mask,
        delta_frequency=delta_frequency,
        n_samples=n_samples,
    )
    return self._to_stack(signal, names, background=background, sampling_frequency=sampling_frequency)

StochasticBackgroundSpectrum dataclass

Power-law isotropic SGWB spectrum.

Parameters:

Name Type Description Default
omega_ref float

Dimensionless energy-density amplitude at reference_frequency.

required
spectral_index float

Power-law index for Omega_GW(f).

0.0
reference_frequency float

Reference frequency in Hz.

25.0
hubble_constant_si float

Hubble constant in inverse seconds.

H0_SI
Source code in src/gwmock_signal/stochastic/spectrum.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@dataclass(frozen=True, slots=True)
class StochasticBackgroundSpectrum:
    """Power-law isotropic SGWB spectrum.

    Args:
        omega_ref: Dimensionless energy-density amplitude at
            ``reference_frequency``.
        spectral_index: Power-law index for ``Omega_GW(f)``.
        reference_frequency: Reference frequency in Hz.
        hubble_constant_si: Hubble constant in inverse seconds.
    """

    omega_ref: float
    spectral_index: float = 0.0
    reference_frequency: float = 25.0
    hubble_constant_si: float = H0_SI

    def omega(self, frequencies: NDArray[np.float64]) -> NDArray[np.float64]:
        """Evaluate ``Omega_GW(f)`` on a frequency grid."""
        if self.omega_ref < 0.0:
            raise ValueError("omega_ref must be non-negative.")
        if self.reference_frequency <= 0.0:
            raise ValueError("reference_frequency must be positive.")
        frequencies = np.asarray(frequencies, dtype=float)
        omega = np.zeros_like(frequencies)
        positive = frequencies > 0.0
        omega[positive] = self.omega_ref * (frequencies[positive] / self.reference_frequency) ** self.spectral_index
        return omega

    def strain_psd(self, frequencies: NDArray[np.float64]) -> NDArray[np.float64]:
        """Convert ``Omega_GW(f)`` to one-sided strain PSD."""
        if self.hubble_constant_si <= 0.0:
            raise ValueError("hubble_constant_si must be positive.")
        frequencies = np.asarray(frequencies, dtype=float)
        psd = np.zeros_like(frequencies)
        positive = frequencies > 0.0
        coefficient = 3.0 * self.hubble_constant_si**2 / (10.0 * np.pi**2)
        psd[positive] = coefficient * self.omega(frequencies[positive]) / frequencies[positive] ** 3
        return psd

omega(frequencies)

Evaluate Omega_GW(f) on a frequency grid.

Source code in src/gwmock_signal/stochastic/spectrum.py
43
44
45
46
47
48
49
50
51
52
53
def omega(self, frequencies: NDArray[np.float64]) -> NDArray[np.float64]:
    """Evaluate ``Omega_GW(f)`` on a frequency grid."""
    if self.omega_ref < 0.0:
        raise ValueError("omega_ref must be non-negative.")
    if self.reference_frequency <= 0.0:
        raise ValueError("reference_frequency must be positive.")
    frequencies = np.asarray(frequencies, dtype=float)
    omega = np.zeros_like(frequencies)
    positive = frequencies > 0.0
    omega[positive] = self.omega_ref * (frequencies[positive] / self.reference_frequency) ** self.spectral_index
    return omega

strain_psd(frequencies)

Convert Omega_GW(f) to one-sided strain PSD.

Source code in src/gwmock_signal/stochastic/spectrum.py
55
56
57
58
59
60
61
62
63
64
def strain_psd(self, frequencies: NDArray[np.float64]) -> NDArray[np.float64]:
    """Convert ``Omega_GW(f)`` to one-sided strain PSD."""
    if self.hubble_constant_si <= 0.0:
        raise ValueError("hubble_constant_si must be positive.")
    frequencies = np.asarray(frequencies, dtype=float)
    psd = np.zeros_like(frequencies)
    positive = frequencies > 0.0
    coefficient = 3.0 * self.hubble_constant_si**2 / (10.0 * np.pi**2)
    psd[positive] = coefficient * self.omega(frequencies[positive]) / frequencies[positive] ** 3
    return psd

long_wavelength_overlap_reduction(detectors, frequencies)

Return frequency-independent tensor ORFs from detector response tensors.

This is the long-wavelength, co-located limit gamma_ij = 2 D_i : D_j. It is a useful default for ET-style low-frequency studies and tests. For separated detectors or paper-facing production datasets, pass explicit ORF arrays to :class:~gwmock_signal.stochastic.StochasticBackgroundSimulator.

Source code in src/gwmock_signal/stochastic/overlap.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
def long_wavelength_overlap_reduction(
    detectors: Sequence[DetectorSpec],
    frequencies: NDArray[np.float64],
) -> dict[tuple[str, str], NDArray[np.float64]]:
    """Return frequency-independent tensor ORFs from detector response tensors.

    This is the long-wavelength, co-located limit ``gamma_ij = 2 D_i : D_j``.
    It is a useful default for ET-style low-frequency studies and tests. For
    separated detectors or paper-facing production datasets, pass explicit ORF
    arrays to :class:`~gwmock_signal.stochastic.StochasticBackgroundSimulator`.
    """
    names = detector_names(detectors)
    tensors = detector_tensors(detectors)
    n_frequencies = np.asarray(frequencies).shape[0]
    overlap_reduction: dict[tuple[str, str], NDArray[np.float64]] = {}
    for index_a, detector_a in enumerate(names):
        for detector_b in names[index_a + 1 :]:
            gamma = 2.0 * float(np.sum(tensors[detector_a] * tensors[detector_b]))
            overlap_reduction[(detector_a, detector_b)] = np.full(n_frequencies, gamma, dtype=float)
    return overlap_reduction

The stochastic module is split into small implementation files under gwmock_signal.stochastic so additional SGWB models, ORF implementations, and dataset utilities can be added without turning the simulator into a monolithic module.