Source code for cfdmod.building.peaks

"""Peak-estimation methods for building response time series.

The engineer-facing deliverables reduce a fluctuating response (acceleration,
floor force, displacement) to a single design peak. Three methods are used in
the field, selectable per deliverable:

- ``"max"`` -- the observed extreme (optionally of the absolute value).
- ``"peak-factor"`` -- Davenport: ``mean + g * std`` with the gust peak factor
  ``g`` from the response frequency and averaging duration.
- ``"gumbel"`` -- fit a Gumbel to block maxima and read the design fractile off
  it (more stable than the raw max for short records).
"""

from __future__ import annotations

from typing import Literal

import numpy as np

PeakMethod = Literal["max", "peak-factor", "gumbel"]


[docs] def gust_peak_factor(f0: float, duration: float = 600.0, *, full: bool = True) -> float: """Davenport gust peak factor ``g`` for a narrow-band process. ``g = sqrt(2 ln(nu T)) + 0.5772 / sqrt(2 ln(nu T))`` with ``nu = f0`` the mean up-crossing rate (Hz) and ``T = duration`` (s). With ``full=False`` only the leading ``sqrt(2 ln(nu T))`` term is returned (the form used in the quick-look notebooks). """ nu_t = f0 * duration if nu_t <= 1.0: raise ValueError(f"f0 * duration must exceed 1 (got {nu_t})") base = np.sqrt(2.0 * np.log(nu_t)) return float(base + 0.5772 / base if full else base)
def _gumbel_fit(block_maxima: np.ndarray) -> tuple[float, float]: """Method-of-moments Gumbel (loc, scale) from block maxima.""" m = float(np.mean(block_maxima)) s = float(np.std(block_maxima, ddof=1)) if block_maxima.size > 1 else 0.0 scale = s * np.sqrt(6.0) / np.pi loc = m - 0.5772 * scale return loc, scale
[docs] def peak_value( series: np.ndarray, method: PeakMethod = "peak-factor", *, f0: float | None = None, duration: float = 600.0, absolute: bool = True, n_blocks: int = 10, non_exceedance: float = 0.78, ) -> float: """Reduce a response time series to a single design peak. Args: method: ``"max"``, ``"peak-factor"`` (needs ``f0``), or ``"gumbel"``. f0: response frequency (Hz), required for ``"peak-factor"``. duration: full-scale averaging window (s) for the gust factor. absolute: if True, ``"max"`` uses ``max(|series|)`` and ``"peak-factor"`` builds the peak off ``|mean| + g*std``. n_blocks: number of blocks for ``"gumbel"`` block maxima. non_exceedance: design fractile ``p`` for ``"gumbel"`` (``x_p = loc - scale ln(-ln p)``). """ x = np.asarray(series, dtype=np.float64) x = x[np.isfinite(x)] if x.size == 0: return float("nan") if method == "max": return float(np.max(np.abs(x)) if absolute else np.max(x)) if method == "peak-factor": if f0 is None: raise ValueError("peak-factor method requires f0") g = gust_peak_factor(f0, duration) mean = abs(float(np.mean(x))) if absolute else float(np.mean(x)) return float(mean + g * float(np.std(x))) if method == "gumbel": vals = np.abs(x) if absolute else x n_blocks = max(1, min(n_blocks, vals.size)) blocks = np.array_split(vals, n_blocks) block_maxima = np.array([float(np.max(b)) for b in blocks if b.size]) loc, scale = _gumbel_fit(block_maxima) if scale == 0.0: return float(loc) return float(loc - scale * np.log(-np.log(non_exceedance))) raise ValueError(f"unknown peak method {method!r}")