Source code for specmod.sources.source

"""Source spectral shapes, and what each implies about the source itself."""

from __future__ import annotations

from dataclasses import dataclass
from typing import Protocol, runtime_checkable

import numpy as np
from numpy.typing import NDArray

__all__ = [
    "SOURCE_MODELS",
    "BoatwrightSource",
    "BruneSource",
    "SourceModel",
    "get_source_model",
]


[docs] @runtime_checkable class SourceModel(Protocol): """A source spectral shape, plus what it implies about the source. The second half is the part that is easy to leave out. Two models can share a spectral shape exactly and still disagree by an order of magnitude on stress drop, because they disagree about what a given corner frequency says about the rupture dimension. """ @property def name(self) -> str: """Short identifier, as configuration refers to it.""" ... @property def corner_frequency_coefficient(self) -> tuple[float, float]: """``(k_P, k_S)`` in ``f_c = k * beta / r``. The bridge from a fitted corner frequency to a source radius, and therefore to stress drop. It belongs to the model rather than to whatever computes stress drop, because **models that share a spectral shape do not share this**. """ ...
[docs] def log10_shape( self, freq: NDArray[np.float64], log10_omega: float, f_c: float ) -> NDArray[np.float64]: """``log10 S(f)`` for a plateau ``log10_omega`` and corner ``f_c``.""" ...
@dataclass(frozen=True) class _GeneralisedSource: """The Boatwright family, of which Brune is the ``gamma = 1`` member. .. math:: \\log_{10} S(f) = \\log_{10}\\Omega - \\frac{1}{\\gamma}\\log_{10}\\left[1 + (f/f_c)^{\\gamma n}\\right] ``n`` is the high-frequency falloff — 2 for an omega-squared model — and ``gamma`` controls how sharply the spectrum turns at the corner. Larger ``gamma`` is a sharper knee. """ gamma: float n: float def log10_shape( self, freq: NDArray[np.float64], log10_omega: float, f_c: float ) -> NDArray[np.float64]: ratio = np.asarray(freq, dtype=np.float64) / f_c return np.asarray( log10_omega - (1.0 / self.gamma) * np.log10(1.0 + ratio ** (self.gamma * self.n)) )
[docs] @dataclass(frozen=True) class BruneSource(_GeneralisedSource): """Brune (1970), omega-squared with a smooth corner. The corner-frequency coefficients are Brune's own kinematic values. They are **not** interchangeable with Madariaga's — see the note in :mod:`specmod.sources`. """ gamma: float = 1.0 n: float = 2.0 @property def name(self) -> str: return "brune" @property def corner_frequency_coefficient(self) -> tuple[float, float]: return (0.42, 0.37)
[docs] @dataclass(frozen=True) class BoatwrightSource(_GeneralisedSource): """Boatwright (1980), omega-squared with a sharper corner than Brune. ``gamma = 2`` narrows the transition; the high-frequency falloff is the same ``f**-2``. The coefficients here are Brune's, because Boatwright's formulation shares the kinematic radius relation — recorded explicitly so that it is a stated choice rather than an omission. """ gamma: float = 2.0 n: float = 2.0 @property def name(self) -> str: return "boatwright" @property def corner_frequency_coefficient(self) -> tuple[float, float]: return (0.42, 0.37)
#: Registered source models, by the name configuration refers to them by. #: #: **Madariaga belongs here next.** It is omega-squared and sits at the same #: ``(gamma, n) = (1, 2)`` as Brune, so it registers as a shape identical to #: Brune's with a *different* #: :attr:`~SourceModel.corner_frequency_coefficient` — which is the entire #: content of the difference and the reason that attribute exists. Take the #: coefficients from the source paper rather than from any summary, including #: this one: they differ between P and S and between authors, and they land #: directly in published stress drops. SOURCE_MODELS: dict[str, type[BruneSource] | type[BoatwrightSource]] = { "brune": BruneSource, "boatwright": BoatwrightSource, }
[docs] def get_source_model(name: str) -> SourceModel: """Resolve a registered source model by name, with its defaults.""" try: cls = SOURCE_MODELS[name] except KeyError: raise ValueError( f"Unknown source model {name!r}. Available: {sorted(SOURCE_MODELS)}." ) from None return cls()