Source code for fdhpy.moss_et_al_2024

"""
Moss et al. (2024) fault displacement model (https://doi.org/10.1177/87552930241288560).
"""

import logging
from functools import cached_property
from typing import Dict, Optional, Union

import numpy as np
from scipy import stats
from scipy.interpolate import make_interp_spline
from scipy.stats._distn_infrastructure import rv_continuous

from fdhpy.cli import cli_runner
from fdhpy.fault_displacement_model import FaultDisplacementModel
from fdhpy.normalized_fault_displacement_model import NormalizedFaultDisplacementModel
from fdhpy.utils import AttributeRequiredError


[docs]class MossEtAl2024(FaultDisplacementModel): """ Moss et al. (2024) fault displacement model. Applicable to principal surface fault displacement on reverse faults. This is a normalized model (i.e., uses the D/AD or D/MD form). The reference displacement models (AD or MD) from Moss et al. (2024) for the "complete" designation are used. All calculations include total aleatory variability; in other words, the statistical distributions for D/AD and AD (or D/MD and MD) are convolved. Parameters ---------- style : str, optional Style of faulting (case-insensitive). Default is "reverse". magnitude : float Earthquake moment magnitude. Recommended range is (4.7, 8). xl : float Normalized location x/L along the rupture length, range [0, 1.0]. xl_step : float, optional Step size increment for slip profile calculations. Default is 0.05. percentile : float Aleatory quantile of interest. Use -1 for mean. metric : str, optional Definition of displacement (case-insensitive). Valid options are "principal". Default is "principal". version : str Name of the model formulation (case-insensitive). Valid options are "d/ad" or "d/md". displ_array : np.ndarray, optional Displacement test value(s) in meters. Default array is provided. use_girs : bool, optional If True (or `--use_girs` in CLI), use statistical distribution parameters (alpha and beta) from the regression model on Figures 4.3 & 4.4 in the Moss et al. (2022) technical report at https://doi.org/10.34948/N3F595. If False, use piecewise linear interpolation to obtain statistical distribution parameters (alpha and beta) from Table 2 in the Earthquake Spectra paper at https://doi.org/10.1177/87552930241288560. Default is False. complete : bool, optional If True, use Moss et al. (2024) reference displacement models (AD or MD) with the "complete" designation. If False (or `--incomplete` in CLI), use Moss et al. (2024) reference displacement models with the "all" designation. Default is True. Notes ----- - The statistical distribution parameters (alpha and beta) can be computed from Table 2 in the Earthquake Spectra paper (default) or from the regression model in the GIRS technical report. See `use_girs` parameter above for more information. - The reference displacement (AD or MD) can be based on the Moss et al. (2024) "complete" subset or "all" data. See `complete` parameter above for more information. See model help at the module level: .. code-block:: python from fdhpy import MossEtAl2024 print(MossEtAl2024.__doc__) See model help in command line: .. code-block:: console $ fd-mea24 --help """ _CONDITIONS = { "style": { "reverse": {"magnitude": (4.7, 8)}, }, "metric": { "principal": {"version": ("d/ad", "d/md")}, }, } _MODEL_NAME = "MossEtAl2024" # Override the init method to set model defaults def __init__(self, **kwargs): kwargs.setdefault("metric", "principal") kwargs.setdefault("style", "reverse") self.use_girs = kwargs.pop("use_girs", False) self.complete = kwargs.pop("complete", True) super().__init__(**kwargs) def __str__(self): parent_str = super().__str__() return parent_str[:-1] + f", use_girs={self.use_girs}, complete={self.complete})" # NOTE: magnitude, xl, and version are validated in parent class initialization # Necessary optional methods in FaultDisplacementModel parent class @property def _folded_xl(self) -> Optional[float]: return self._calc_folded_xl() @cached_property def _model_data(self) -> np.recarray: data_files = { "d/ad": "moss_2024_gamma_distribution_parameters_d_ad.csv", "d/md": "moss_2024_gamma_distribution_parameters_d_md.csv", } try: if self.version is None: raise AttributeRequiredError("version", self._MODEL_NAME) df = self._load_data(data_files[self.version]) if df is not None: return df.to_records(index=True) # Other errors handled in `_load_data` except AttributeRequiredError as e: logging.error(e) # Required methods for implementing normalized fault displacement models @property def _AD_MAG_SCALE_PARAMS(self): """Set parameters for loglinear magnitude scaling for average displacement.""" if self.complete: return {"intercept": -2.87, "slope": 0.416, "std_dev": 0.2} else: return {"intercept": -2.98, "slope": 0.427, "std_dev": 0.25} @property def _MD_MAG_SCALE_PARAMS(self): """Set parameters for loglinear magnitude scaling for maximum displacement.""" if self.complete: return {"intercept": -2.5, "slope": 0.415, "std_dev": 0.2} else: return {"intercept": -2.73, "slope": 0.422, "std_dev": 0.35} @property def _normalized_calcs(self) -> NormalizedFaultDisplacementModel: """Return an instance of the NormalizedFaultDisplacementModel for the current context.""" return NormalizedFaultDisplacementModel(self) # Mandated methods in FaultDisplacementModel parent class def _statistical_distribution_params(self) -> None: if self.xl is not None: # Compute alpha and beta parameters based on folded x/L and model version using # regression model in GIRS report Figures 4.3 & 4.4 if self.use_girs: if self.version == "d/ad": a1, a2 = 4.2797, 1.6216 b1, b2 = -0.5003, 0.5133 elif self.version == "d/md": a1, a2 = 1.422, 1.856 b1, b2 = -0.0832, 0.1994 self._alpha = a1 * self._folded_xl + a2 self._beta = b1 * self._folded_xl + b2 else: c = self._model_data params_to_interpolate = { "_alpha": c["alpha"], "_beta": c["beta"], } for _key, _array in params_to_interpolate.items(): # NOTE: np.interp does not extrapolate and scipy.interpolate.interp1d is being # depreciated, so use scipy.interpolate.make_interp_spline with k=1 for # piecewise linear interpolation with extrapolation. # f = interp1d( # xl_array, # _array, # kind="linear", # fill_value="extrapolate", # ) f = make_interp_spline(c["x_L"], _array, k=1) setattr(self, _key, f(self._folded_xl)) self._alpha = float(self._alpha) self._beta = float(self._beta) # Compute mu and sigma parameters based on magnitude and model version regr_params_map = { "d/ad": self._AD_MAG_SCALE_PARAMS, "d/md": self._MD_MAG_SCALE_PARAMS, } if self.magnitude is not None: # NOTE: mu based on magnitude is handled in self._normalized_calcs p = self._normalized_calcs._calc_mag_scale_stat_params(regr_params_map[self.version]) self._mu, self._sigma = p[0], p[1] # Set to None if not computed self._alpha = getattr(self, "_alpha", None) self._beta = getattr(self, "_beta", None) self._mu = getattr(self, "_mu", None) self._sigma = getattr(self, "_sigma", None) def _calc_displ_site(self) -> Optional[float]: """Calculate deterministic scenario displacement.""" return self._normalized_calcs._site_displ() def _calc_displ_avg(self) -> Optional[float]: """Calculate average displacement.""" return self._normalized_calcs._avg_displ(self._AD_MAG_SCALE_PARAMS) def _calc_displ_max(self) -> Optional[float]: """Calculate maximum displacement.""" return self._normalized_calcs._max_displ(self._MD_MAG_SCALE_PARAMS) def _calc_cdf(self) -> Optional[np.ndarray]: """Calculate cumulative probability.""" return self._normalized_calcs._cdf() # Ensure statistical distribution parameters are updated for current instance @property def stat_params_info( self, ) -> Dict[str, Union[Dict[str, Optional[float]], rv_continuous, Dict[str, float]]]: """ Dictionary of statistical parameters ("params"), probability distribution ("prob_distribution"), and its arguments ("prob_distribution_kwargs") for the instance. """ self._statistical_distribution_params() statistical_parameters = { "mu": self._mu, "sigma": self._sigma, "alpha": self._alpha, "beta": self._beta, } # Use nomenclature in Moss et al. (2024) probability_distribution = stats.gamma probability_distribution_kwargs = { "a": statistical_parameters["alpha"], "loc": 0, "scale": statistical_parameters["beta"], } return { "params": statistical_parameters, "prob_distribution": probability_distribution, "prob_distribution_kwargs": probability_distribution_kwargs, } @staticmethod def _add_arguments(parser): # Add arguments specific to model parser.add_argument( "--use_girs", action="store_true", help=( "If True, use statistical distribution parameters (alpha and beta) from the " "regression model on Figures 4.3 & 4.4 in the Moss et al. (2022) technical report " "at https://doi.org/10.34948/N3F595. If False, use piecewise linear interpolation " "to obtain statistical distribution parameters (alpha and beta) from Table 2 in " "the Earthquake Spectra paper at https://doi.org/10.1177/87552930241288560. " "Default is False (i.e., use EQS not GIRS)." ), ) parser.add_argument( "--incomplete", dest="complete", action="store_false", help=( "If True, use Moss et al. (2024) reference displacement models (AD or MD) with " "the 'complete' designation. If False, use Moss et al. (2024) reference " "displacement models with the 'all' designation. Default is True (i.e., use " "complete, not all)." ), ) @staticmethod def main(): cli_runner(MossEtAl2024, MossEtAl2024._add_arguments)
# Inherit the docstrings from parent class MossEtAl2024.displ_site.__doc__ = FaultDisplacementModel.displ_site.__doc__ MossEtAl2024.displ_avg.__doc__ = FaultDisplacementModel.displ_avg.__doc__ MossEtAl2024.displ_max.__doc__ = FaultDisplacementModel.displ_max.__doc__ MossEtAl2024.displ_profile.__doc__ = FaultDisplacementModel.displ_profile.__doc__ MossEtAl2024.cdf.__doc__ = FaultDisplacementModel.cdf.__doc__ MossEtAl2024.prob_exceed.__doc__ = FaultDisplacementModel.prob_exceed.__doc__ if __name__ == "__main__": MossEtAl2024.main()