Source code for demregpy.synthetic
"""
Utilities for building synthetic DEM observations from response matrices.
"""
from dataclasses import dataclass
import numpy as np
__all__ = [
"SyntheticObservation",
"synthesize_counts",
]
[docs]
@dataclass(slots=True)
class SyntheticObservation:
"""
Synthetic observation returned by :func:`synthesize_counts`.
The fields store the input DEM, the corresponding noise-free counts, the
counts used as inversion input, and their uncertainties.
"""
dem: np.ndarray
dn_clean: np.ndarray
dn_in: np.ndarray
edn_in: np.ndarray
def _logt_bin_widths(logt):
logt = np.asarray(logt, dtype=float)
if logt.ndim != 1:
raise ValueError("logt must be one-dimensional")
if logt.size < 2:
raise ValueError("logt must contain at least two temperature points")
edges = np.empty(logt.size + 1, dtype=float)
edges[1:-1] = 0.5 * (logt[:-1] + logt[1:])
edges[0] = logt[0] - 0.5 * (logt[1] - logt[0])
edges[-1] = logt[-1] + 0.5 * (logt[-1] - logt[-2])
return np.diff(edges)
[docs]
def synthesize_counts(
dem,
logt,
tresp,
*,
error_fraction=0.1,
noise_fraction=None,
min_fraction=0.1,
random_state=None,
):
"""
Fold a DEM through a temperature response matrix and build synthetic counts.
Parameters
----------
dem : array_like
DEM values on the ``logt`` grid. The last axis must match ``logt``.
Leading dimensions are preserved in the output.
logt : array_like
Log10 temperature grid corresponding to the first axis of ``tresp``.
tresp : array_like
Temperature response matrix with shape ``(nt, nf)``.
error_fraction : float or array_like, optional
Relative uncertainty applied to the clean counts when building
``edn_in``. Defaults to ``0.1``.
noise_fraction : float or array_like, optional
Relative Gaussian noise applied to the clean counts. If omitted, the
returned ``dn_in`` is noise-free.
min_fraction : float, optional
Lower bound applied to noisy counts as a fraction of the clean counts.
random_state : int or numpy.random.Generator, optional
Random seed or generator used when ``noise_fraction`` is provided.
Returns
-------
SyntheticObservation
Synthetic DEM, clean counts, input counts, and uncertainties.
"""
dem = np.asarray(dem, dtype=float)
logt = np.asarray(logt, dtype=float)
tresp = np.asarray(tresp, dtype=float)
if tresp.ndim != 2:
raise ValueError("tresp must have shape (nt, nf)")
if tresp.shape[0] != logt.size:
raise ValueError("The first axis of tresp must match the length of logt")
if dem.shape[-1] != logt.size:
raise ValueError("The last axis of dem must match the length of logt")
dlogt = _logt_bin_widths(logt)
kernel = tresp * (10 ** logt * np.log(10 ** dlogt))[:, np.newaxis]
dn_clean = np.tensordot(dem, kernel, axes=([-1], [0]))
if noise_fraction is None:
dn_in = np.array(dn_clean, copy=True)
else:
rng = random_state
if not isinstance(rng, np.random.Generator):
rng = np.random.default_rng(random_state)
perturb = rng.normal(loc=0.0, scale=noise_fraction, size=dn_clean.shape)
dn_in = dn_clean * (1.0 + perturb)
dn_in = np.maximum(dn_in, min_fraction * dn_clean)
edn_in = np.asarray(error_fraction, dtype=float) * dn_clean
return SyntheticObservation(
dem=dem,
dn_clean=dn_clean,
dn_in=dn_in,
edn_in=edn_in,
)