Source code for yemale.ot.density
"""Density superlevel sets selected by integrated mass."""
from collections.abc import Callable
from dataclasses import dataclass
import numpy as np
from yemale._array import count, readonly
[docs]
@dataclass(frozen=True, eq=False, repr=False)
class DensityRegion:
"""Points whose density reaches a cutoff, including ties.
Build with ``density_region`` on a law or smooth map. ``mass`` estimates
the included density integral, not the coverage of future observations.
Attributes:
mass: Estimated density integral over the region, including cutoff ties.
"""
_log_density: Callable
_log_threshold: float
mass: float
def __repr__(self):
return f"DensityRegion(mass={self.mass:.6g}, threshold={self.threshold:.6g})"
@property
def threshold(self):
"""Density cutoff; membership is compared in log space."""
with np.errstate(over="ignore", under="ignore"):
return float(np.exp(self._log_threshold))
[docs]
def contains(self, points):
"""Return membership for one point or a batch, preserving batch axes."""
return self._log_density(points) >= self._log_threshold
def _density_region(
owner, log_density, prepare, mass, n_integration_points, total_mass=1.0
):
mass = float(mass)
if not np.isfinite(mass) or not 0 < mass <= 1:
raise ValueError("mass must be finite and in (0, 1]")
size = count(n_integration_points, "n_integration_points")
if total_mass is not None and mass > total_mass:
raise ValueError(
f"requested mass {mass:g} exceeds this density's total mass, "
f"{total_mass:.12g}"
)
# The smooth density is positive throughout source space.
if total_mass is not None and mass == total_mass < 1:
return DensityRegion(log_density, -np.inf, total_mass)
cached = getattr(owner, "_density_cache", None)
if cached is None or cached[0] != size:
points, weights = prepare(size)
scores = np.asarray(log_density(points)).reshape(-1)
if np.isnan(scores).any():
raise RuntimeError("density is undefined at an integration point")
order = np.argsort(-scores)
scores = readonly(scores[order])
cumulative = np.cumsum(weights[order])
if len(cumulative):
cumulative[-1] = weights.sum()
cached = (size, scores, readonly(cumulative))
object.__setattr__(owner, "_density_cache", cached)
_, scores, cumulative = cached
if not len(cumulative) or mass > cumulative[-1] + 1e-12:
raise ValueError(
"integration does not resolve the requested mass; "
"increase n_integration_points"
)
index = min(np.searchsorted(cumulative, mass), len(scores) - 1)
threshold = float(scores[index])
end = np.searchsorted(-scores, -threshold, side="right")
return DensityRegion(log_density, threshold, float(cumulative[end - 1]))