Source code for specmod.sources.attenuation
"""Path attenuation, parameterised by ``t*``."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Protocol, runtime_checkable
import numpy as np
from numpy.typing import NDArray
__all__ = [
"ATTENUATION_MODELS",
"AttenuationModel",
"ConstantQ",
"FrequencyDependentQ",
"get_attenuation_model",
]
[docs]
@runtime_checkable
class AttenuationModel(Protocol):
"""Anything that attenuates a source spectrum along the path."""
@property
def name(self) -> str: ...
@property
def parameters(self) -> tuple[str, ...]:
"""Free parameter names, in the order the fitter should take them."""
...
[docs]
def log10_decay(
self, freq: NDArray[np.float64], *values: float
) -> NDArray[np.float64]:
"""``log10 D(f)``, given this model's parameters in order."""
...
[docs]
@dataclass(frozen=True)
class ConstantQ:
"""Frequency-independent ``t*``.
.. math:: \\log_{10} D(f) = -\\pi f t^{*} / \\ln 10
The ``ln 10`` is the conversion into base-10 logs, which is the space the
fit is performed in. Getting it wrong scales ``t*`` by 2.3 and leaves the
spectrum looking plausible.
"""
@property
def name(self) -> str:
return "constant_q"
@property
def parameters(self) -> tuple[str, ...]:
return ("ts",)
def log10_decay(
self, freq: NDArray[np.float64], *values: float
) -> NDArray[np.float64]:
(ts,) = values
return np.asarray(
-(np.pi * np.asarray(freq, dtype=np.float64) * ts / np.log(10))
)
[docs]
@dataclass(frozen=True)
class FrequencyDependentQ:
"""``t*`` with a power-law frequency dependence.
.. math:: \\log_{10} D(f) = -\\pi f^{1-a} t^{*} / \\ln 10
``a = 0`` recovers :class:`ConstantQ`. The two are separate models rather
than one with a switch because they have different free parameters, and a
fitter needs to know that before it starts.
"""
@property
def name(self) -> str:
return "frequency_dependent_q"
@property
def parameters(self) -> tuple[str, ...]:
return ("ts", "a")
def log10_decay(
self, freq: NDArray[np.float64], *values: float
) -> NDArray[np.float64]:
ts, a = values
f = np.asarray(freq, dtype=np.float64)
return np.asarray(-(np.pi * f ** (1.0 - a) * ts / np.log(10)))
#: Registered attenuation models.
ATTENUATION_MODELS: dict[str, type[ConstantQ] | type[FrequencyDependentQ]] = {
"constant_q": ConstantQ,
"frequency_dependent_q": FrequencyDependentQ,
}
[docs]
def get_attenuation_model(name: str) -> AttenuationModel:
"""Resolve a registered attenuation model by name."""
try:
cls = ATTENUATION_MODELS[name]
except KeyError:
raise ValueError(
f"Unknown attenuation model {name!r}. "
f"Available: {sorted(ATTENUATION_MODELS)}."
) from None
return cls()