from pathlib import Path
import numpy as np
import pandas as pd
import re
from importlib import resources
[docs]
class GroundMotion:
"""Represents a ground motion acceleration time history.
This class provides a container for ground motion data, typically acceleration
time histories. It includes methods for reading data from common file formats
(like AT2), scaling the motion, and accessing its properties.
Attributes
----------
acc_g : numpy.ndarray
The acceleration time history in units of g (acceleration due to gravity).
dt : float
The time step of the acceleration data.
time : numpy.ndarray
The time vector corresponding to the acceleration data.
name : str, optional
A name for the ground motion event.
component : str, optional
The component of the ground motion (e.g., 'h1', 'h2', 'v').
"""
def __init__(self, acc_g, dt, name=None, component=None):
"""Initializes the GroundMotion object.
Parameters
----------
acc_g : array-like
Acceleration time history in units of g.
dt : float
Time step of the acceleration data.
name : str, optional
Name of the ground motion event, by default None.
component : str, optional
Component of the ground motion, by default None.
"""
self.acc_g = np.asarray(acc_g)
self.dt = float(dt)
self.time = np.arange(len(acc_g)) * dt
self.name = name
self.component = component
# ---------- Constructors ----------
[docs]
@classmethod
def from_at2(cls, file_path):
"""Creates a GroundMotion object from a PEER NGA (AT2) file.
Parameters
----------
file_path : str or pathlib.Path
The path to the .AT2 file.
Returns
-------
GroundMotion
A new GroundMotion instance with data from the file.
"""
file_path = Path(file_path)
acc, dt = cls._read_at2(file_path)
return cls(acc, dt, name=file_path.stem)
[docs]
@classmethod
def from_event(cls, event_name, component="hor1", base_dir=None):
"""Loads a ground motion from the built-in event database.
Parameters
----------
event_name : str
The name of the earthquake event. Currently available are (e.g., 'imperialValley_elCentro_1940', 'lomaPrieta_corralitos_1989', 'northridge_sylmar_1994', 'sanFernando_pacoidaDam_1971' ).
component : str
The specific component to load (e.g., 'hor1', 'hor2', 'up').
base_dir : str or pathlib.Path, optional
The base directory of the ground motion data. If None, it uses the
package's default data directory.
Returns
-------
GroundMotion
A new GroundMotion instance for the specified event and component.
Raises
-------
FileNotFoundError
If the specified event or component is not found.
"""
if base_dir is None:
with resources.as_file(
resources.files("structdyn.ground_motions") / "data"
) as data_dir:
base_dir = Path(data_dir)
event_dir = base_dir / event_name
if not event_dir.exists():
raise FileNotFoundError(f"Event '{event_name}' not found")
files = list(event_dir.glob("*.AT2"))
if not files:
raise FileNotFoundError("No AT2 files found")
selected = cls._select_component(files, component)
acc, dt = cls._read_at2(selected)
return cls(acc, dt, name=event_name, component=component)
[docs]
@classmethod
def from_arrays(cls, acc_g, dt, name="user_motion"):
"""Creates a GroundMotion object directly from arrays.
Parameters
----------
acc_g : array-like
Acceleration time history in units of g.
dt : float
Time step of the acceleration data.
name : str, optional
A name for the motion, by default "user_motion".
Returns
-------
GroundMotion
A new GroundMotion instance.
"""
return cls(acc_g, dt, name=name)
# ---------- Utilities ----------
@staticmethod
def _read_at2(file_path):
acc = []
dt = None
with open(file_path, "r") as f:
lines = f.readlines()
for i, line in enumerate(lines):
if "NPTS" in line and "DT" in line:
dt = float(re.search(r"DT=\s*([0-9.]+)", line).group(1))
data_start = i + 1
break
for line in lines[data_start:]:
acc.extend(float(x) for x in line.split())
return np.array(acc), dt
@staticmethod
def _select_component(files, component):
component = component.lower()
for f in files:
if component in f.stem.lower():
return f
raise ValueError(f"Component '{component}' not found")
# ---------- Operations ----------
[docs]
def scale(self, factor):
self.acc_g *= factor
return self
[docs]
def scale_to_pga(self, target_pga_g):
current = np.max(np.abs(self.acc_g))
self.acc_g *= target_pga_g / current
return self
[docs]
def to_dataframe(self):
return pd.DataFrame({"time": self.time, "acc_g": self.acc_g})