"""Hale and Abrahamson (2019) flexible sliding-mass displacement of earth dams.
The model is described in the dissertation of :cite:t:`hale19`.
Requires :mod:`pyrotd` (``pip install pygmm[slope]``).
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
import numpy.typing as npt
from .rigid_block import calc_rigid_disp
__author__ = "Albert Kottke"
#: Feet per meter, for :meth:`HaleAbrahamson19.from_metric`
_FT_PER_M = 1 / 0.3048
[docs]
@dataclass(frozen=True)
class HaleTransferFunction:
"""Transfer function of :class:`HaleAbrahamson19` and its parameters.
Attributes
----------
freqs : np.ndarray
Frequencies [Hz].
tf : np.ndarray
Complex transfer function from the input motion to the average
acceleration of the sliding mass.
damping : float
Damping ratio of the dam (Eq. 4.9).
period_ratio : float
Ratio of the first-mode period to the small-strain period (Eq. 4.4).
freq_first : float
First-mode frequency of the dam [Hz], shortened by nonlinearity.
freq_nat_eff : float
Natural frequency of the undamped system [Hz] (Eq. 4.2).
"""
freqs: np.ndarray
tf: np.ndarray
damping: float
period_ratio: float
freq_first: float
freq_nat_eff: float
[docs]
class HaleAbrahamson19:
"""Hale and Abrahamson (2019) displacement of a flexible sliding mass.
The model is described in :cite:t:`hale19`.
The input motion is filtered by a nonlinear transfer function of the dam
before a rigid sliding-block analysis is performed.
The model works in feet; use :meth:`from_metric` for meters. The transfer
function to the average acceleration of the sliding mass is
:meth:`transfer_function`.
Provide exactly one of `shear_vel` and `freq_nat`; the other is computed
from :math:`f_n = V_S / (3 H)`. This is the empirical relation of the
model; it differs from the :math:`V_S / (2.61 H)` of a homogeneous shear
wedge on a rigid base, see ``pystrata.dam.analytical.wedge_natural_freqs``.
Parameters
----------
height : float
Height [ft].
shear_vel : float, optional
Shear-wave velocity [ft/s].
freq_nat : float, optional
Natural frequency [Hz].
nl_model : str, default "darendeli"
Nonlinear model of the material: ``"darendeli"`` or
``"vucetic_dobry"``.
"""
C = np.rec.fromrecords(
[
(
0.76,
0.35,
-0.00049,
0.00329,
-0.03558,
0.35297,
-0.89726,
1.25375,
0.00024,
-1.14426,
0.00090,
-1.91824,
0.00145,
-0.00993,
0.86560,
0.57934,
0.00172,
0.40392,
0.85,
)
],
names="a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,alpha",
)[0]
NL_MODELS = ("darendeli", "vucetic_dobry")
[docs]
def __init__(
self,
height: float,
shear_vel: float | None = None,
freq_nat: float | None = None,
nl_model: str = "darendeli",
):
if nl_model not in self.NL_MODELS:
raise ValueError(f"nl_model must be one of: {self.NL_MODELS}")
self._height = height
self._nl_model = nl_model
if shear_vel is None and freq_nat is not None:
self._freq_nat = freq_nat
self._shear_vel = 3 * height * freq_nat
elif freq_nat is None and shear_vel is not None:
self._shear_vel = shear_vel
self._freq_nat = shear_vel / (3 * height)
else:
raise NotImplementedError("Provide one of `shear_vel` or `freq_nat`.")
[docs]
@classmethod
def from_metric(
cls,
height: float,
shear_vel: float | None = None,
freq_nat: float | None = None,
nl_model: str = "darendeli",
) -> HaleAbrahamson19:
"""Create the model from a height and velocity in meters.
Parameters
----------
height : float
Height [m].
shear_vel : float, optional
Shear-wave velocity [m/s].
freq_nat : float, optional
Natural frequency [Hz]; it does not depend on the units.
nl_model : str, default "darendeli"
Nonlinear model of the material, see the class.
Returns
-------
HaleAbrahamson19
Model with the height and velocity converted to feet.
"""
return cls(
height * _FT_PER_M,
shear_vel=None if shear_vel is None else shear_vel * _FT_PER_M,
freq_nat=freq_nat,
nl_model=nl_model,
)
@property
def freq_nat(self) -> float:
"""Natural frequency of the dam [Hz]."""
return self._freq_nat
@property
def height(self) -> float:
"""Height of the dam [ft]."""
return self._height
@property
def shear_vel(self) -> float:
"""Shear-wave velocity of the dam [ft/s]."""
return self._shear_vel
[docs]
def __call__(
self, time_step: float, accels: npt.ArrayLike, yield_coef: float
) -> list[float]:
"""Compute the sliding displacements.
Parameters
----------
time_step : float
Time step [s].
accels : array_like
Acceleration time series [g].
yield_coef : float
Yield coefficient [g].
Returns
-------
list of float
Final displacement [cm] for the inverted and the original
polarity of the motion.
"""
sliding_accels = self._calc_sliding_mass_accels(time_step, accels)
return [
calc_rigid_disp(time_step, sliding_accels, yield_coef, invert=invert)[
"disps"
][-1]
for invert in [True, False]
]
def _calc_sliding_mass_accels(self, time_step: float, accels: npt.ArrayLike):
try:
import pyrotd
except ImportError as err:
raise ImportError(
"HaleAbrahamson19 requires pyrotd: pip install pygmm[slope]"
) from err
# Compute the Fourier amplitude spectra
accels = np.asarray(accels)
fourier_amps = np.fft.rfft(accels)
freqs = np.fft.rfftfreq(accels.size, d=time_step)
spec_accel = pyrotd.calc_oscillator_resp(
freqs,
fourier_amps,
0.05,
1 / self.period_nl,
peak_resp_only=True,
osc_type="psa",
)
fourier_amps *= self.transfer_function(freqs, spec_accel).tf
return np.fft.irfft(fourier_amps)
@property
def period_nl(self) -> float:
r"""Nonlinear period [s] of the dam (Eq. 4.3 of :cite:t:`hale19`).
This is the period at which the 5% damped pseudo-spectral acceleration
is computed, :math:`\ln T_{NL} = a_1 \ln T_o + a_2`, where
:math:`T_o = 1 / f_n` is the small-strain period.
"""
return float(np.exp(self.C.a1 * np.log(1 / self.freq_nat) + self.C.a2))
def _calc_params(self, ln_spec_accel):
"""Compute the damping ratio and the first-mode period ratio T1 / To.
Parameters
----------
ln_spec_accel : float
Natural log of the 5% damped pseudo-spectral acceleration [g] at
the nonlinear period.
Returns
-------
damping : float
Damping ratio (Eq. 4.9).
period_ratio : float
Ratio of the first-mode period to the small-strain period
(Eq. 4.4).
"""
ln_height = np.log(self.height)
if self._nl_model == "darendeli":
c3 = 1.0
b5 = 0.0
b6 = 0.0
else:
c3 = 0.32
b5 = -2.0
b6 = -1.0
C = self.C
b1 = C.a9 * self.shear_vel + C.a10
b2 = C.a11 * self.shear_vel + C.a12
b3 = (C.a13 * ln_height + C.a14) * self.shear_vel + (C.a15 * ln_height + C.a16)
b4 = C.a17 * self.shear_vel + C.a18
damping = np.exp(b1 + b2 / (1 + np.exp(b3 + b5 + (b4 + b6) * ln_spec_accel)))
c1 = (C.a3 * ln_height + C.a4) * self.shear_vel + (C.a5 * ln_height + C.a6)
c2 = C.a7 * c1 + C.a8
period_ratio = 1 + c3 * np.exp(c1 * (c2 + ln_spec_accel))
return damping, period_ratio
[docs]
def transfer_function(
self, freqs: npt.ArrayLike, spec_accel: float
) -> HaleTransferFunction:
"""Compute the transfer function to the average sliding-mass acceleration.
This is Equation 4.1 of :cite:t:`hale19`, with the first-mode frequency
from Equations 4.2 and 4.4. The transfer function is nonlinear: it
depends on the strength of the input motion through its spectral
acceleration at the nonlinear period of the dam.
Parameters
----------
freqs : array_like
Frequencies [Hz].
spec_accel : float
5% damped pseudo-spectral acceleration [g] of the input motion at
:attr:`period_nl`.
Returns
-------
HaleTransferFunction
The transfer function and the parameters that define it.
"""
if not spec_accel > 0:
raise ValueError("spec_accel must be positive [g].")
freqs = np.asarray(freqs, dtype=float)
damping, period_ratio = self._calc_params(np.log(spec_accel))
# First-mode (damped) frequency of the dam, shortened by nonlinearity
freq_first = self.freq_nat / period_ratio
# Natural frequency of the undamped system (Eq. 4.2)
nat_freq_eff = freq_first / np.sqrt(1 - damping**2)
trans_func = self.C.alpha * (
1
+ (1j * freqs) ** 2
/ (freqs**2 - nat_freq_eff**2 - 2j * damping * nat_freq_eff * freqs)
)
# Amplitudes are at least one below the first-mode frequency
mask = (freqs < freq_first) & (np.abs(trans_func) < 1.0)
trans_func[mask] = 1.0
return HaleTransferFunction(
freqs=freqs,
tf=trans_func,
damping=float(damping),
period_ratio=float(period_ratio),
freq_first=float(freq_first),
freq_nat_eff=float(nat_freq_eff),
)