Source code for openquake.pfd.secondary_surf_rup.petersen2011

# -*- coding: utf-8 -*-
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (C) 2024-2026 Yen-Shin Chen, OGS
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

"""
Module :mod:`openquake.pfd.secondary_surf_rup.petersen2011`
"""

import numpy as np
from openquake.pfd.params import check_choice, check_positive, check_style
from openquake.pfd.secondary_surf_rup.base import BaseSecondarySurfRup


[docs]class Petersen2011SecondarySR(BaseSecondarySurfRup): """ Implementation of the Petersen et al. (2011) model for strike-slip faults with different pixel sizes """ # Define coefficients for different pixel sizes # Pixel ("cell") size parameters from Table 4 (Page 812, Petersen et al., 2011) PIXEL_SIZES = { 25: {"a": -1.1470, "b": 2.1046, "sigma": 1.2508}, # 25 x 25 m 50: {"a": -0.9000, "b": 0.9866, "sigma": 1.1470}, # 50 x 50 m 100: {"a": -1.0114, "b": 2.5572, "sigma": 1.0917}, # 100 x 100 m 150: {"a": -1.0934, "b": 3.5526, "sigma": 1.0188}, # 150 x 150 m 200: {"a": -1.1538, "b": 4.2342, "sigma": 1.0177}, # 200 x 200 m } # Near-field interpolation points from Table 5 (page 812, Petersen et al., # 2011); p0/p1/p2 converted from percent to fractions NEAR_FIELD_POINTS = { 25: {"p0": 0.74541, "p1": 0.078690, "p2": 0.020108, "r1": 100, "r2": 200}, # 25 x 25 m 50: {"p0": 0.87162, "p1": 0.048206, "p2": 0.026177, "r1": 100, "r2": 200}, # 50 x 50 m 100: {"p0": 0.90173, "p1": 0.18523, "p2": 0.066354, "r1": 100, "r2": 200}, # 100 x 100 m 150: {"p0": 0.87394, "p1": 0.19592, "p2": 0.070477, "r1": 150, "r2": 300}, # 150 x 150 m 200: {"p0": 0.92483, "p1": 0.18975, "p2": 0.074709, "r1": 200, "r2": 400}, # 200 x 200 m } def __init__(self, pixel_size=None, version=None, cell_size=None, style=None): """ :param pixel_size: optional pixel (cell) size in meters pinned by the logic-tree branch; ``None`` defers to the ``get_prob`` call (legacy default: 25). :param version: optional variant ('default' or 'near_field'); ``None`` defers to the call (legacy default: 'default'). :param cell_size: deprecated alias of ``pixel_size``. :param style: optional faulting style declared by the logic-tree branch. Petersen et al. (2011) is a strike-slip model with no style selector, so the value does not change the numbers; it is stored (validated against the global style vocabulary) as a declaration of the branch context. """ self.pixel_size = check_positive(type(self).__name__, "pixel_size", pixel_size) self.version = check_choice(type(self).__name__, "version", version, frozenset(["default", "near_field"]), canon=lambda v: str(v).lower()) self.cell_size = check_positive(type(self).__name__, "cell_size", cell_size) self.style = check_style(type(self).__name__, style) def get_prob(self, r, pixel_size=None, version=None, cell_size=None): """ Calculate the probability of distributed-fault surface rupture as a function of distance, pixel size, and version, per Petersen et al. (2011). :param r: Distance from the principal fault trace in kilometers (up to 2 km recommended). :param pixel_size: Size of the pixel ("cell" in the paper) in meters (25, 50, 100, 150, or 200 m; default: 25 m). :param version: Model version (case-insensitive). Options: 'default' (power function, Page 812, Table 4), 'near_field' (interpolated near-field, Table 5, page 812). Default: 'default'. :param cell_size: Deprecated alias of ``pixel_size`` (the historical parameter name); when given it overrides ``pixel_size``. :returns: Probability of rupture (float or array, 0–1) for the given distance, pixel_size, and version. :raises ValueError: If r is negative or exceeds 2000 m, pixel_size is invalid, or version is invalid. :notes: - Uses power function from Table 4 (Page 812) for 'default' (far-field probabilities). - Uses near-field interpolation points from Table 5 (page 812) for 'near_field' (r < r1), as described in the text on page 819. - No magnitude dependence, per Petersen et al. (2011, Page 818). - Limited to 2 km distance from principal fault; no triggered ruptures included. """ # Fall back to constructor-pinned values, then legacy defaults if cell_size is None: cell_size = self.cell_size if pixel_size is None: pixel_size = (self.pixel_size if self.pixel_size is not None else 25) if version is None: version = self.version if self.version is not None else "default" # Validate inputs if cell_size is not None: # deprecated alias kept for old logic trees pixel_size = cell_size version = version.lower() valid_versions = ["default", "near_field"] if version not in valid_versions: raise ValueError(f"Invalid version '{version}'. Accepted values are: {', '.join(valid_versions)}") # Handle scalar/array input following Youngs2003 pattern r = np.asarray(r) if r.ndim == 0: r = np.array([r]) was_scalar = True else: was_scalar = False # Convert distance from km to m r = r * 1000 # Validate distance range #if not (r >= 0).all() or (r > 2000).any(): # raise ValueError("Distance r must be non-negative and ≤ 2000 m") if isinstance(pixel_size, str): try: pixel_size = int(pixel_size) except ValueError: raise ValueError("Pixel size must be convertible to an integer") if pixel_size not in self.PIXEL_SIZES: raise ValueError(f"Pixel size must be one of {list(self.PIXEL_SIZES)} m") # Get pixel size parameters params = self.PIXEL_SIZES[pixel_size] a, b, _ = params["a"], params["b"], params["sigma"] near_params = self.NEAR_FIELD_POINTS[pixel_size] if version == "default": # Far-field power function (Page 819, Eqn 20, Table 4) r_safe = np.where(r == 0, 0.1, r) # Use 0.1 m as minimum distance ln_P = a * np.log(r_safe) + b P_rupture = np.exp(ln_P) # Convert ln(P) to probability P_rupture = np.clip(P_rupture, 0, 1) # Ensure probability is in [0, 1] else: # version == "near_field" # Near-field interpolation (Table 5, page 812; method described on page 819) p0, p1, p2 = near_params["p0"], near_params["p1"], near_params["p2"] r1, r2 = near_params["r1"], near_params["r2"] # Initialize with default probability (p0) P_rupture = np.full_like(r, p0, dtype=float) # Linear interpolation for r < r1 mask_near = r < r1 if np.any(mask_near): # Interpolate between p0 and p1 at r1 P_rupture[mask_near] = p0 + (p1 - p0) * (r[mask_near] / r1) # Linear interpolation for r1 <= r <= r2 mask_mid = (r >= r1) & (r <= r2) if np.any(mask_mid): # Interpolate between p1 and p2 P_rupture[mask_mid] = p1 + (p2 - p1) * ((r[mask_mid] - r1) / (r2 - r1)) # Use power function for far field (r > r2) mask_far = r > r2 if np.any(mask_far): r_safe = np.where(r[mask_far] == 0, 0.1, r[mask_far]) ln_P_far = a * np.log(r_safe) + b P_rupture[mask_far] = np.exp(ln_P_far) P_rupture = np.clip(P_rupture, 0, 1) # Ensure probability is in [0, 1] # Return scalar if input was scalar, following Youngs2003 pattern if was_scalar: return float(P_rupture[0]) else: return P_rupture
[docs]class Petersen2011SecondarySR_default(Petersen2011SecondarySR): """Petersen et al. (2011) distributed-rupture model fixed to the 'default' variant.""" def get_prob(self, r, pixel_size=25, version="default", cell_size=None): """Return distributed surface-rupture probability using the 'default' variant.""" return super().get_prob(r=r, pixel_size=pixel_size, cell_size=cell_size, version="default")