From b01e558890ba72ca615d85d3e3c452b0d73fda1f Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 8 Jan 2026 13:52:16 +1300 Subject: [PATCH 1/5] docs: add and improve Numpydoc-style docstrings across core modules - Add or enhance Numpydoc-style docstrings for public functions, classes, and methods in qcore modules - Improve parameter, returns, raises, examples, and notes sections for clarity and consistency - Update XYTSFile and NHMFault class docstrings and attributes for better documentation - Refine documentation in constants, coordinates, formats, geo, grid, nhm, point_in_polygon, shared, simulation_structure, siteamp_models, src_site_dist, timeseries, uncertainties/distributions, and xyts modules - Add Numpydoc linting GitHub Actions workflow for automated docstring checks --- .github/workflows/numpydoc.yml | 36 ++++ qcore/constants.py | 100 ++++++++-- qcore/coordinates.py | 9 +- qcore/formats.py | 163 +++++++++------ qcore/geo.py | 285 ++++++++++++++++++++------- qcore/grid.py | 24 +-- qcore/nhm.py | 108 ++++++++-- qcore/point_in_polygon.py | 11 +- qcore/shared.py | 13 +- qcore/simulation_structure.py | 159 ++++++++++++++- qcore/siteamp_models.py | 4 +- qcore/src_site_dist.py | 30 +-- qcore/timeseries.py | 52 ++--- qcore/uncertainties/distributions.py | 11 +- qcore/xyts.py | 160 +++++++-------- tests/test_xyts.py | 16 +- 16 files changed, 852 insertions(+), 329 deletions(-) create mode 100644 .github/workflows/numpydoc.yml diff --git a/.github/workflows/numpydoc.yml b/.github/workflows/numpydoc.yml new file mode 100644 index 00000000..4a0a15da --- /dev/null +++ b/.github/workflows/numpydoc.yml @@ -0,0 +1,36 @@ +name: Numpydoc Lint + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + numpydoc-lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: fd-find + version: 1.0 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.13' # Adjust version as needed + + - name: Install dependencies + run: | + pip install numpydoc + sudo apt-get install + + - name: Run Numpydoc Lint + run: | + fdfind . qcore/ -E "__init__.py" --extension py | xargs numpydoc lint diff --git a/qcore/constants.py b/qcore/constants.py index 77b0da01..f7110874 100644 --- a/qcore/constants.py +++ b/qcore/constants.py @@ -13,11 +13,34 @@ class ExtendedEnum(Enum): @classmethod def has_value(cls, value: Any) -> bool: + """Check if enum has value. + + Parameters + ---------- + value : Any + Value to check. + + Returns + ------- + bool + True if Enum has value. + """ return any(value == item.value for item in cls) @classmethod def is_substring(cls, parent_string: str) -> bool: - """Check if an enum's string value is contained in the given string""" + """Check if an enum's string value is contained in the given string + + Parameters + ---------- + parent_string : str + The string to search for the value. + + Returns + ------- + bool + True if any enum value is a substring of `parent_string`. + """ return any( not isinstance(item.value, str) or item.value in parent_string for item in cls @@ -25,9 +48,23 @@ def is_substring(cls, parent_string: str) -> bool: @classmethod def get_names(cls) -> list[str]: + """Get the names for every enum member. + + Returns + ------- + list of str + The enum member names. + """ return [item.name for item in cls] def __str__(self) -> str: + """Get a string representation of Enum value. + + Returns + ------- + str + The enum member name. + """ return self.name @@ -46,23 +83,58 @@ def __new__(cls, value: Any, str_value: str): # noqa: D102 # numpydoc ignore=GL @classmethod def has_str_value(cls, str_value: str) -> bool: + """Check if any enum member has a given string value. + + Parameters + ---------- + str_value : str + The value to check. + + Returns + ------- + bool + True if the Enum contains a member whose string value matches `str_value`. + """ return any(str_value == item.str_value for item in cls) @classmethod - def from_str(cls, str_value): - if not cls.has_str_value(str_value): - raise ValueError(f"{str_value} is not a valid {cls.__name__}") - else: - for item in cls: - if item.str_value == str_value: - return item + def from_str(cls, str_value: str) -> Any: + """Lookup an enum member from its str_value. + + Parameters + ---------- + str_value : str + The string value of the enum member. + + Returns + ------- + Any + The enum member with matching `str_value`. + + Raises + ------ + ValueError + If the enum does not contain a member with the given string value. + """ + for item in cls: + if item.str_value == str_value: + return item + raise ValueError(f"{str_value} is not a valid {cls.__name__}") @classmethod def iterate_str_values(cls, ignore_none: bool = True) -> Generator[Any, None, None]: - """Iterates over the string values of the enum, - ignores entries without a string value by default + """Iterates over the member variables of the enum. + + Parameters + ---------- + ignore_none : bool + If True, ignore all member variables whose str_value is None. + + Yields + ------ + Any + An enum member variable. """ - for item in cls: - if ignore_none and item.str_value is None: - continue - yield item.str_value + yield from ( + item for item in cls if not (ignore_none and item.str_value is None) + ) diff --git a/qcore/coordinates.py b/qcore/coordinates.py index 0b85f465..c931ebce 100644 --- a/qcore/coordinates.py +++ b/qcore/coordinates.py @@ -2,13 +2,6 @@ Module for coordinate conversions between WGS84 (latitude and longitude) and NZTM (New Zealand Transverse Mercator) coordinate systems. -Functions ----------- -- wgs_depth_to_nztm(wgs_depth_coordinates: np.ndarray) -> np.ndarray: - Converts WGS84 coordinates (latitude, longitude, depth) to NZTM coordinates. -- nztm_to_wgs_depth(nztm_coordinates: np.ndarray) -> np.ndarray: - Converts NZTM coordinates (y, x, depth) to WGS84 coordinates. - References ---------- This module provides functions for converting coordinates between WGS84 and NZTM coordinate systems. @@ -197,7 +190,7 @@ def great_circle_bearing_to_nztm_bearing( The origin point to compute the bearing from. distance : float The distance to shift. - ll_bearing : float + great_circle_bearing : float The great circle bearing for the final point. Returns diff --git a/qcore/formats.py b/qcore/formats.py index 1b139032..b9ec959c 100644 --- a/qcore/formats.py +++ b/qcore/formats.py @@ -14,16 +14,23 @@ def load_im_file_pd( imcsv: Path | str, all_ims: bool = False, comp: str | None = None ) -> pd.DataFrame | pd.Series: - """ - Loads an IM file using pandas and returns a dataframe - :param imcsv: FFP to im_csv - :param all_ims: returns all_ims. Defaultly returns only short IM names (standard pSA periods etc). - Setting this to true includes all pSA periods (and other long IM names). Extended pSA periods have - longer IM names and are filtered out by this flag. - :param comp: component to return. Default is to return all - :return: - """ + """Load an Intensity Measure (IM) CSV file into a pandas DataFrame. + + Parameters + ---------- + imcsv : Path or str + Path to the IM CSV file. + all_ims : bool, optional + Whether to load all IMs. If False (default), only standard IMs + with short names (e.g., common pSA periods) are included. + comp : str or None, optional + Specific component to return (e.g., 'pga', 'pgv'). If None, all components are returned. + Returns + ------- + pd.DataFrame or series + DataFrame or series containing IM values, indexed by station and component. + """ df = pd.read_csv(imcsv, index_col=[0, 1]) if not all_ims: @@ -49,34 +56,22 @@ def station_file_argparser() -> argparse.ArgumentParser: ... # numpydoc ignore= def station_file_argparser( parser: argparse.ArgumentParser | None = None, ) -> argparse.ArgumentParser | None: - """ - Return a parser object with formatting information of a generic station file. To facilitate the use of load_generic_station_file() - - Example: - In your script, X.py, you already have some arguments parsed by ArgumentParser(), but if you wish to handle extra arguments related to the format of a station file - - def get_args(): - parser = argparse.ArgumentParser() - arg = parser.add_argument - arg("--arg1", help="argument 1") - arg("--arg2", help="argument 2") - parser = formats.station_file_argparser(parser=parser) # pass the parser argument, update it with the return value - return parser.parse_args() - - if __name__ == '__main__': - args = get_args() - .... - - python X.py --arg1 ARG1 --arg2 ARG2 --stat_file some_station_file.csv --stat_name_col 0 --lat_col 1 --lon_col 2 --sep , --skiprows 1 + """Add station file argument options to an ArgumentParser. Parameters ---------- - parser : parser created to handle other arguments unrelated to the station file loading (default: None) + parser : argparse.ArgumentParser or None, optional + Existing parser to extend. If None, a new parser is created. Returns ------- - parser object with arguments related to station file loading + argparse.ArgumentParser + Parser object with added station file-related arguments. + Examples + -------- + >>> parser = station_file_argparser() + >>> args = parser.parse_args(["--stat_file", "stations.ll"]) """ if parser is None: parser = argparse.ArgumentParser(description="Station Data Loader") @@ -134,26 +129,32 @@ def load_generic_station_file( sep: str = r"\s+", skiprows: int = 0, ) -> pd.DataFrame: - """ - Reads the station file of any format into a pandas dataframe + """Load a generic station file into a pandas DataFrame. - Can be useful to obtain necessary format info with station_file_argparser() Parameters ---------- - stat_file: str - Path to the station file. Can be .ll or any other format - stat_name_col: column index of station name (default: 2 for .ll file) - lon_col: column index of lon (default 0 for .ll file) - lat_col: column index of lat (default 1 for .ll file) - other_cols : column indices of other columns to load eg eg. [3,5,6] - other_names : column names of other_cols eg. ["vs30","z1p0","z2p5"] - sep : delimiter (by default "\\s+" (whitespace) for .ll file - skiprows : number of rows to skip (if header rows exist) + stat_file : str + Path to the station file (e.g., .ll file or other format). + stat_name_col : int, optional + Column index for station names. Default is 2. + lon_col : int, optional + Column index for longitude. Default is 0. + lat_col : int, optional + Column index for latitude. Default is 1. + other_cols : list of int, optional + Indices of additional columns to include. + other_names : list of str, optional + Names corresponding to `other_cols`. + sep : str, optional + Column delimiter. Default is whitespace. + skiprows : int, optional + Number of rows to skip (for header lines). Returns ------- pd.DataFrame - station as index and columns lon, lat and other columns + DataFrame with index as station name and columns including longitude, + latitude, and any additional specified columns. """ cols: dict[str, int] = {"stat_name": stat_name_col} if lon_col is not None: @@ -180,17 +181,17 @@ def load_generic_station_file( @deprecated("Will be removed after Cybershake investigation concludes.") def load_station_file(station_file: str) -> pd.DataFrame: - """Reads the station file into a pandas dataframe + """Load a station file into a pandas DataFrame. Parameters ---------- station_file : str - Path to the station file + Path to the station file. Returns ------- pd.DataFrame - station as index and columns lon, lat + DataFrame indexed by station, with longitude and latitude columns. """ return pd.read_csv( station_file, @@ -204,35 +205,55 @@ def load_station_file(station_file: str) -> pd.DataFrame: @deprecated("Will be removed after Cybershake investigation concludes.") def load_vs30_file(vs30_file: str) -> pd.DataFrame: - """Reads the vs30 file into a pandas dataframe + """Load a Vs30 (shear-wave velocity) file into a pandas DataFrame. + + Parameters + ---------- + vs30_file : str + Path to the Vs30 file. - :param vs30_file: Path to the vs30 file - :return: pd.DataFrame - station as index and columns vs30 + Returns + ------- + pd.DataFrame + DataFrame indexed by station, with a single column ``vs30``. """ return pd.read_csv(vs30_file, sep=r"\s+", index_col=0, header=None, names=["vs30"]) @deprecated("Will be removed after Cybershake investigation concludes.") def load_z_file(z_file: str) -> pd.DataFrame: - """Reads the z file into a pandas dataframe + """Load a z-file containing depth parameters (e.g., z1.0, z2.5) into a pandas DataFrame. + + Parameters + ---------- + z_file : str + Path to the z file. - :param z_file: Path to the z file - :return: pd.DataFrame - station as index and columns z1p0, z2p5 + Returns + ------- + pd.DataFrame + DataFrame indexed by station, with columns ``z1p0``, ``z2p5``, and ``sigma``. """ return pd.read_csv(z_file, names=["z1p0", "z2p5", "sigma"], index_col=0, skiprows=1) @deprecated("Will be removed after Cybershake investigation concludes.") def load_station_ll_vs30(station_file: str, vs30_file: str) -> pd.DataFrame: - """Reads both station and vs30 file into a single pandas dataframe - keeps only the matching entries + """Merge station location and Vs30 data into a single DataFrame. - :param station_file: Path to the station file - :param vs30_file: Path to the vs30 file - :return: pd.DataFrame - station as index and columns lon, lat, vs30 + Parameters + ---------- + station_file : str + Path to the station file containing longitude and latitude. + vs30_file : str + Path to the Vs30 file. + + Returns + ------- + pd.DataFrame + DataFrame indexed by station, with columns ``lon``, ``lat``, and ``vs30``. """ + vs30_df = load_vs30_file(vs30_file) # type: ignore station_df = load_station_file(station_file) # type: ignore @@ -245,23 +266,35 @@ def load_rrup_file(rrup_file: str) -> pd.DataFrame: Parameters ---------- - rrup_file: str + rrup_file : str Path to the rrup file to load Returns ------- pd.DataFrame - station as index with columns rrup, rjb and optional rx + Station as index with columns rrup, rjb and optional rx """ return pd.read_csv(rrup_file, header=0, index_col=0, engine="c") @deprecated("Will be removed after Cybershake investigation concludes.") def load_fault_selection_file(fault_selection_file: str | Path) -> dict[str, int]: - """ - Loads a fault selection file, returning a dictionary of fault:count pairs - :param fault_selection_file: The relative or absolute path to the fault selection file - :return: A dictionary of fault:count pairs for all faults found in the file + """Load a fault selection file into a dictionary of fault names and counts. + + Parameters + ---------- + fault_selection_file : str or Path + Path to the fault selection file. + + Returns + ------- + dict of str to int + Dictionary mapping fault names to selected counts. + + Raises + ------ + ValueError + If the file contains malformed lines or duplicate fault entries. """ faults: dict[str, int] = {} with open(fault_selection_file) as fault_file: diff --git a/qcore/geo.py b/qcore/geo.py index 8fafa2ef..3b297ecd 100644 --- a/qcore/geo.py +++ b/qcore/geo.py @@ -2,9 +2,6 @@ qcore geometry utilities. """ -from math import acos, asin, atan, atan2, cos, degrees, pi, radians, sin, sqrt -from typing import overload - import numpy as np import numpy.typing as npt @@ -21,11 +18,11 @@ def get_distances( Parameters ---------- locations : np.ndarray - List of locations + list of locations Shape [n_locations, 2], column format (lon, lat) - lon : Union[float, np.ndarray] + lon : float | np.ndarray Array or singular float of Longitude reference locations to compare - lat : Union[float, np.ndarray] + lat : float | np.ndarray Array or singular float of Latitude reference locations to compare Returns @@ -48,12 +45,26 @@ def closest_location( locations: np.ndarray, lon: float, lat: float ) -> tuple[int, float]: """ - Find position and distance of closest location in 2D np.array of (lon, lat). + Find the index and distance of the closest location to a reference point. + + Parameters + ---------- + locations : np.ndarray + Array of shape (n_locations, 2) containing longitude and latitude pairs (degrees). + lon : float + Reference longitude in degrees. + lat : float + Reference latitude in degrees. + + Returns + ------- + tuple of (int, float) + Index of the closest location and its distance in kilometers. """ d = get_distances(locations, lon, lat) i = np.argmin(d) - return int(i), d[i] + return int(i), float(d[i]) def oriented_bearing_wrt_normal( @@ -116,10 +127,21 @@ def oriented_bearing_wrt_normal( def gen_mat(mrot: float, mlon: float, mlat: float) -> tuple[np.ndarray, np.ndarray]: """ - Precursor for xy2ll and ll2xy functions. - mrot: model rotation - mlon: model centre longitude - mlat: model centre latitude + Generate model rotation and inverse matrices for coordinate transformations. + + Parameters + ---------- + mrot : float + Model rotation angle in degrees. + mlon : float + Model center longitude in degrees. + mlat : float + Model center latitude in degrees. + + Returns + ------- + tuple of np.ndarray + Flattened (3×3) transformation matrix and its inverse. """ arg = radians(mrot) cosA = cos(arg) @@ -156,9 +178,19 @@ def gen_mat(mrot: float, mlon: float, mlat: float) -> tuple[np.ndarray, np.ndarr def xy2ll(xy_km: np.ndarray, amat: np.ndarray) -> np.ndarray: """ - Converts km offsets to longitude and latitude. - xy_km: 2D np array of [X, Y] offsets from origin (km) - amat: from gen_mat function + Convert XY offsets (in km) to geographic coordinates (longitude, latitude). + + Parameters + ---------- + xy_km : np.ndarray + Array of shape (n, 2) representing offsets in kilometers from the model origin. + amat : np.ndarray + Transformation matrix generated by `gen_mat`. + + Returns + ------- + np.ndarray + Array of shape (n, 2) containing longitude and latitude in degrees. """ x = xy_km[:, 0] / R_EARTH sinB = np.sin(x) @@ -187,9 +219,19 @@ def xy2ll(xy_km: np.ndarray, amat: np.ndarray) -> np.ndarray: def ll2xy(ll: np.ndarray, ainv: np.ndarray) -> np.ndarray: """ - Converts longitude and latitude to km offsets. - ll: 2D np array of [lon, lat] - ainv: from gen_mat function + Convert geographic coordinates to XY offsets (in km). + + Parameters + ---------- + ll : np.ndarray + Array of shape (n, 2) containing longitude and latitude (degrees). + ainv : np.ndarray + Inverse transformation matrix from `gen_mat`. + + Returns + ------- + np.ndarray + Array of shape (n, 2) containing X, Y offsets in kilometers. """ lon = np.radians(ll[:, 0]) lat = np.radians(90.0 - ll[:, 1]) @@ -211,33 +253,25 @@ def ll2xy(ll: np.ndarray, ainv: np.ndarray) -> np.ndarray: ) -def xy2gp(xy: np.ndarray, nx: int, ny: int, hh: float) -> np.ndarray: - """ - Converts km offsets to grid points. - xy: 2D np array of [X, Y] offsets from origin (km) - nx: number of X grid positions - ny: number of Y grid positions - hh: grid spacing +def gp2xy(gp: np.ndarray, nx: int, ny: int, hh: float) -> np.ndarray: """ - gp = np.copy(xy) - - # distance from corner - gp[:, 0] += (nx - 1) * hh * 0.5 - gp[:, 1] += (ny - 1) * hh * 0.5 - - # gridpoint from top corner - gp /= hh - - return np.round(gp).astype(np.int32, copy=False) + Convert grid indices to XY offsets in kilometers. + Parameters + ---------- + gp : np.ndarray + Grid points array of shape (n, 2). + nx : int + Number of grid points along X. + ny : int + Number of grid points along Y. + hh : float + Grid spacing in kilometers. -def gp2xy(gp: np.ndarray, nx: int, ny: int, hh: float) -> np.ndarray: - """ - Converts grid points to km offsets. - xy: 2D np array of [X, Y] gridpoints - nx: number of X grid positions - ny: number of Y grid positions - hh: grid spacing + Returns + ------- + np.ndarray + Array of shape (n, 2) containing X, Y offsets (km) relative to the grid center. """ xy = gp.astype(np.float32) * hh @@ -252,7 +286,23 @@ def ll_shift( lat: float, lon: float, distance: float, bearing: float ) -> tuple[float, float]: """ - Shift lat/long by distance at bearing. + Compute a new latitude and longitude by shifting from a point by a given distance and bearing. + + Parameters + ---------- + lat : float + Starting latitude in degrees. + lon : float + Starting longitude in degrees. + distance : float + Distance to move in kilometers. + bearing : float + Bearing angle in degrees clockwise from north. + + Returns + ------- + tuple of (float, float) + New latitude and longitude in degrees. """ # formula is for radian values lat, lon, bearing = list(map(radians, [lat, lon, bearing])) @@ -268,7 +318,19 @@ def ll_shift( def ll_mid(lon1: float, lat1: float, lon2: float, lat2: float) -> tuple[float, float]: """ - Return midpoint between a pair of lat, long points. + Compute the geographic midpoint between two points. + + Parameters + ---------- + lon1, lat1 : float + Longitude and latitude of the first point (degrees). + lon2, lat2 : float + Longitude and latitude of the second point (degrees). + + Returns + ------- + tuple of (float, float) + Midpoint longitude and latitude in degrees. """ # functions based on radians lon1, lat1, lat2, dlon = list(map(radians, [lon1, lat1, lat2, (lon2 - lon1)])) @@ -284,7 +346,19 @@ def ll_mid(lon1: float, lat1: float, lon2: float, lat2: float) -> tuple[float, f def ll_dist(lon1: float, lat1: float, lon2: float, lat2: float) -> float: """ - Return distance between a pair of lat, long points. + Compute great-circle distance between two points. + + Parameters + ---------- + lon1, lat1 : float + Longitude and latitude of the first point (degrees). + lon2, lat2 : float + Longitude and latitude of the second point (degrees). + + Returns + ------- + float + Distance between the points in kilometers. """ # functions based on radians lat1, lat2, dlon, dlat = list( @@ -299,8 +373,21 @@ def ll_bearing( lon1: float, lat1: float, lon2: float, lat2: float, midpoint: bool = False ): """ - Initial bearing when traveling from 1 -> 2. - Direction facing from point 1 when looking at point 2. + Compute the initial bearing from one geographic point to another. + + Parameters + ---------- + lon1, lat1 : float + Longitude and latitude of the first point (degrees). + lon2, lat2 : float + Longitude and latitude of the second point (degrees). + midpoint : bool, optional + If True, compute bearing at the midpoint. Default is False. + + Returns + ------- + float + Bearing in degrees clockwise from north. """ if midpoint: lon1, lat1 = ll_mid(lon1, lat1, lon2, lat2) @@ -318,7 +405,19 @@ def ll_bearing( def angle_diff(b1: float, b2: float) -> float: """ - Return smallest difference (clockwise, -180 -> 180) from b1 to b2. + Compute the signed smallest difference between two bearings. + + Parameters + ---------- + b1 : float + First bearing in degrees. + b2 : float + Second bearing in degrees. + + Returns + ------- + float + Difference in degrees, within (-180, 180]. """ r = (b2 - b1) % 360 if r > 180: @@ -328,9 +427,17 @@ def angle_diff(b1: float, b2: float) -> float: def avg_wbearing(angles: list[list[float]]) -> float: """ - Return average angle given angles and weightings. - NB: angles are clockwise from North, not anti-clockwise from East. - angles: 2d list of (angle, weight) + Compute the weighted average of a set of bearings. + + Parameters + ---------- + angles : list of [float, float] + Each element is [angle, weight], where angle is in degrees. + + Returns + ------- + float + Weighted average bearing in degrees clockwise from north. """ x = 0 y = 0 @@ -368,19 +475,32 @@ def path_from_corners( output: str | None = "sim.modelpath_hr", min_edge_points: int = 100, close: bool = True, -) -> list[tuple[float, float]] | None: +) -> list[tuple[float | int, float | int]] | None: """ - corners: python list (4 by 2) containing (lon, lat) in order - otherwise take from velocity model - output: where to store path of (lon, lat) values - min_edge_points: at least this many points wanted along edges + Generate a path connecting the corners of a region with optional subdivision and output. + + Parameters + ---------- + corners : list of tuple of float + List of (lon, lat) coordinates defining the polygon corners in order. + output : str or None, optional + Path to save the generated path points, or None to return them. + min_edge_points : int, optional + Minimum number of points per edge. Default is 100. + close : bool, optional + Whether to close the polygon by connecting back to the first corner. Default is True. + + Returns + ------- + list of tuple of float or None + List of (lon, lat) coordinates if `output` is None, otherwise None. """ # close the box by going back to origin if close: corners.append(corners[0]) - # until each side has at least wanted number of points + # until each side has at least the wanted number of points while len(corners) < 4 * min_edge_points: # work backwards, insertions don't change later indexes for i in range(len(corners) - 1, 0, -1): @@ -469,17 +589,46 @@ def ll_cross_along_track_dist( d13: float | None = None, ) -> tuple[float, float]: """ - Returns both the distance of point 3 to the nearest point on the great circle line that passes through point 1 and - point 2 and how far away that nearest point is from point 1, along the great line circle - If any of a12, a13, d13 are given the calculations for them are skipped - If all of a12, a13, d13 are given, none of the lon, lat values are used and may be junk data - Taken from https://www.movable-type.co.uk/scripts/latlong.html - :param lon1, lat1: The lon, lat coordinates for point 1 - :param lon2, lat2: The lon, lat coordinates for point 2 - :param lon3, lat3: The lon, lat coordinates for point 3 - :param a12: The angle between point 1 (lon1, lat1) and point 2 (lon2, lat2) in radians - :param a13: The angle between point 1 (lon1, lat1) and point 3 (lon3, lat3) in radians - :param d13: The distance between point 1 (lon1, lat1) and point 3 (lon3, lat3) in km + Compute the cross-track and along-track distances from a third point to a great-circle path. + + This function calculates both: + - The shortest (cross-track) distance from point 3 to the great-circle line + defined by points 1 and 2. + - The along-track distance from point 1 to the nearest point on that line. + + If any of `a12`, `a13`, or `d13` are provided, the corresponding calculations + are skipped. If all three are given, longitude and latitude values are ignored. + + Based on: + https://www.movable-type.co.uk/scripts/latlong.html + + Parameters + ---------- + lon1 : float + Longitude of point 1 in degrees. + lat1 : float + Latitude of point 1 in degrees. + lon2 : float + Longitude of point 2 in degrees. + lat2 : float + Latitude of point 2 in degrees. + lon3 : float + Longitude of point 3 in degrees. + lat3 : float + Latitude of point 3 in degrees. + a12 : float, optional + Initial bearing from point 1 to point 2 in radians. If None, computed automatically. + a13 : float, optional + Initial bearing from point 1 to point 3 in radians. If None, computed automatically. + d13 : float, optional + Distance between point 1 and point 3 in kilometers. If None, computed automatically. + + Returns + ------- + tuple of float + A tuple containing: + - Cross-track distance (float): Distance from point 3 to the great-circle path, in kilometers. + - Along-track distance (float): Distance from point 1 to the nearest point on the great-circle path, in kilometers. """ if a12 is None: a12 = radians(ll_bearing(lon1, lat1, lon2, lat2)) diff --git a/qcore/grid.py b/qcore/grid.py index 61b0d83f..5125fb4d 100644 --- a/qcore/grid.py +++ b/qcore/grid.py @@ -1,16 +1,5 @@ """ This module provides functions for working with planar regions defined by geographical coordinates. - -Functions ---------- -grid_corners - Returns the corners of a plane from a series of parameters. - -coordinate_meshgrid - Creates a meshgrid of points in a bounded plane region. - -gridpoint_count_in_length - Calculate the number of gridpoints that fit into a given length. """ from typing import Optional @@ -238,12 +227,17 @@ def coordinate_patchgrid( ny is the number of points in the origin->y_bottom direction and nx the number of points in the origin->x_upper direction. - Note - ---- + Raises + ------ + ValueError + If resolution, nx and ny are not provided. + + Notes + ----- The patch grid may have different sizes than given in as resolution if the resolution does not divide the lengths of the sides of the plane evenly. - Example - ------- + Examples + -------- >>> origin = np.array([-43.5321, 172.6362, 0.0]) # Christchurch, NZ >>> x_upper = np.array([-43.5311, 172.6462, 0.0]) # ~800m to the east >>> y_bottom = np.array([-43.5421, 172.6362, 0.0]) # ~1.2km to the south diff --git a/qcore/nhm.py b/qcore/nhm.py index 1359d9fc..412924b5 100644 --- a/qcore/nhm.py +++ b/qcore/nhm.py @@ -59,29 +59,78 @@ def mag2mom_nm(mw: float) -> float: @dataclass class NHMFault: - """Contains the information for a single fault from a NHM file.""" + """ + Contains the information for a single fault from a NHM file. + + Notes + ----- + This class stores geometric, kinematic, and statistical parameters + describing an individual fault, as defined in the New Zealand + National Hazard Model (NHM) fault database. + """ name: str + """Name of the fault.""" + tectonic_type: str + """Tectonic setting type of the fault.""" + fault_type: str + """Fault style of the rupture (e.g., REVERSE, NORMAL, STRIKE-SLIP).""" + length: float + """Fault length in kilometers.""" + length_sigma: float + """Uncertainty (standard deviation) in fault length, in kilometers.""" + dip: float + """Fault dip angle in degrees.""" + dip_sigma: float + """Uncertainty (standard deviation) in dip angle, in degrees.""" + dip_dir: float + """Dip direction (azimuth) in degrees.""" + rake: float + """Slip rake angle in degrees.""" + dbottom: float + """Depth to the bottom of the fault plane, in kilometers.""" + dbottom_sigma: float + """Uncertainty (standard deviation) in bottom depth, in kilometers.""" + dtop: float + """Mean depth to the top of the fault plane, in kilometers.""" + dtop_min: float + """Minimum depth to the top of the fault plane, in kilometers.""" + dtop_max: float + """Maximum depth to the top of the fault plane, in kilometers.""" + slip_rate: float + """Slip rate along the fault, in millimeters per year.""" + slip_rate_sigma: float + """Uncertainty (standard deviation) in slip rate, in millimeters per year.""" + coupling_coeff: float + """Fault coupling coefficient.""" + coupling_coeff_sigma: float + """Uncertainty (standard deviation) in coupling coefficient.""" + mw: float + """Moment magnitude of the fault.""" + recur_int_median: float + """Median recurrence interval in years.""" + trace: np.ndarray + """Fault surface trace as an array of (longitude, latitude) pairs for the top edge of the fault.""" # TODO: add x y z fault plane data as in SRF info # TODO: add leonard mw function @@ -90,12 +139,25 @@ def sample_2012( self, mw_area_scaling: bool = True, mw_perturbation: bool = True ) -> "NHMFault": """ - Permutates the current NHM fault as per the OpenSHA implementation. This uses the same Mw scaling relations - as Stirling 2012 - Dtop is peturbated with a uniform distribution between min and max. - The remaining parameters are perturburbated with a truncated normal distribution (2 standard deviations) - - :return: new NHM object with the perturbated parameters containing 0 in all sigma sections + Perturb the current NHM fault according to the OpenSHA implementation, + using the same Mw scaling relations as Stirling (2012). + + The top depth is perturbed uniformly between `dtop_min` and `dtop_max`. + All other parameters are perturbed using a truncated normal distribution + within two standard deviations. + + Parameters + ---------- + mw_area_scaling : bool, optional + If True, apply magnitude scaling based on rupture area. Default is True. + mw_perturbation : bool, optional + If True, apply a random perturbation to the magnitude. Default is True. + + Returns + ------- + NHMFault + A new `NHMFault` instance with perturbed parameters, where all + sigma values are set to zero. """ mw = self.mw @@ -153,11 +215,14 @@ def sample_2012( def write(self, out_fp: TextIO, header: bool = False) -> None: """ - Writes a section of the NHM file - - :param out_fp: file pointer for open file (for writing) - :param header: flag to write the header at the start of the file - :return: + Write the fault parameters to an NHM file. + + Parameters + ---------- + out_fp : TextIO + File pointer for the open file (for writing). + header : bool, optional + If True, write the NHM header at the start of the file. Default is False. """ if header: out_fp.write(NHM_HEADER) @@ -186,14 +251,15 @@ def load_nhm( Parameters ---------- - nhm_path: str, optional + nhm_path : str, optional NHM file to load. If not provided, a default will be downloaded from the QuakeCoRE Dropbox. - skiprows: int, optional + skiprows : int, optional Skip the first skiprows lines; default: 15. Returns ------- - dict of NHMFault by name + dict[str, NHMFault] + NHMFault by name """ if not nhm_path: nhm_path = pooch.retrieve(url=NHM_MODEL_URL, known_hash=NHM_MODEL_HASH) @@ -255,12 +321,13 @@ def load_nhm_df(nhm_ffp: str, erf_name: str | None = None): ---------- nhm_ffp : str Path to the ERF file - erf_name : ERFFileType + erf_name : str or None name to identify faults from an ERF Returns ------- DataFrame + The NHM2010 model loaded as a dataframe. """ nhm_infos = load_nhm(nhm_ffp) @@ -309,8 +376,15 @@ def get_fault_header_points( Parameters ---------- - fault: NHMFault + fault : NHMFault A fault object from an NHM file + + Returns + ------- + list of dict + SRF Header values. + np.ndarray + SRF points. """ srf_points = [] srf_header: list[dict[str, int | float]] = [] diff --git a/qcore/point_in_polygon.py b/qcore/point_in_polygon.py index e90e8add..fcce55ca 100644 --- a/qcore/point_in_polygon.py +++ b/qcore/point_in_polygon.py @@ -1,3 +1,5 @@ +"Numba routines for point-in-polygon checks." + from typing import Literal import numba @@ -12,17 +14,14 @@ def is_inside_postgis( polygon: npt.NDArray[TNFloat], point: npt.NDArray[TNFloat] ) -> Literal[0, 1, 2]: # pragma: no cover - """ - Function that checks if a point is inside a polygon - Based on solutions found here - (https://stackoverflow.com/questions/36399381/whats-the-fastest-way-of-checking-if-a-point-is-inside-a-polygon-in-python) + """Function that checks if a point is inside a polygon. Parameters ---------- polygon : np.ndarray - List of points that define the polygon e.g. [[x1, y1], [x2, y2], ...] + List of points that define the polygon e.g. [[x1, y1], [x2, y2], ...]. point : np.ndarray - List of points that define the point e.g. [x, y] + Point to test [x, y]. Returns ------- diff --git a/qcore/shared.py b/qcore/shared.py index 4cc97180..3edc455a 100644 --- a/qcore/shared.py +++ b/qcore/shared.py @@ -63,7 +63,7 @@ def get_corners( Parameters ---------- - model_params : Path or str + model_params_ffp : Path or str The file path of the model_params file. gmt_format : bool, default False If True, also returns corners in GMT string format. @@ -108,7 +108,8 @@ def non_blocking_exe( stderr: Union[bool, FileIO] = True, **kwargs, ) -> subprocess.Popen: # pragma: no cover - """ + r"""Run a command without blocking the calling thread. + *DO NOT USE THIS FUNCTION* Instead, call subprocess.run or subprocess.check_call to execute processes. @@ -127,13 +128,13 @@ def non_blocking_exe( to Popen. debug : bool, default True If True, print out the command to run before running. - stdout : bool or IOBase, default True + stdout : bool or FileIO, default True The stdout file handle to send output. If True, will default to subprocess.PIPE. - stderr : bool or IOBase, default True + stderr : bool or FileIO, default True The stderr file handle to send output. If True, will default to subprocess.PIPE. - kwargs : dict + **kwargs : dict Additional arguments, passed to Popen. Returns @@ -190,7 +191,7 @@ def exe( If True, print out the command to run before running. stdin : str-like, optional If not None, then given to the running process as standard input. - kwargs : dict + **kwargs : dict Additional arguments, passed to Popen. Returns diff --git a/qcore/simulation_structure.py b/qcore/simulation_structure.py index 881dfc38..e2493c74 100644 --- a/qcore/simulation_structure.py +++ b/qcore/simulation_structure.py @@ -6,20 +6,76 @@ def get_fault_from_realisation(realisation: str) -> str: + """ + Extract the fault name from a realisation name or path. + + Parameters + ---------- + realisation : str + Realisation name or full path to the realisation. + + Returns + ------- + str + Fault name associated with the given realisation. + """ realisation = os.path.basename(realisation) # if realisation is a fullpath return realisation.rsplit("_REL", 1)[0] def get_realisation_name(fault_name: str, rel_no: int) -> str: + """ + Format a realisation name given a fault name and realisation number. + + Parameters + ---------- + fault_name : str + Name of the fault. + rel_no : int + Realisation number. + + Returns + ------- + str + Formatted realisation name (e.g., 'AlpineF2_REL03'). + """ return f"{fault_name}_REL{rel_no:0>2}" def get_srf_info_location(realisation: str) -> str: + """ + Get the relative SRF info file location for a realisation. + + Parameters + ---------- + realisation : str + Realisation name. + + Returns + ------- + str + Relative path to the SRF info file. + """ fault = get_fault_from_realisation(realisation) return os.path.join(fault, "Srf", realisation + ".info") def get_srf_dir(cybershake_root: str, realisation: str) -> str: + """ + Get the directory containing SRF files for a fault. + + Parameters + ---------- + cybershake_root : str + Cybershake root directory. + realisation : str + Realisation name. + + Returns + ------- + str + Path to the SRF directory for the fault. + """ return os.path.join( cybershake_root, "Data", @@ -30,21 +86,79 @@ def get_srf_dir(cybershake_root: str, realisation: str) -> str: def get_srf_location(realisation: str) -> str: + """ + Get the relative SRF file location for a realisation. + + Parameters + ---------- + realisation : str + Realisation name. + + Returns + ------- + str + Relative path to the SRF file. + """ fault = get_fault_from_realisation(realisation) return os.path.join(fault, "Srf", realisation + ".srf") def get_srf_path(cybershake_root: str, realisation: str) -> str: + """ + Get the absolute path to the SRF file for a realisation. + + Parameters + ---------- + cybershake_root : str + Cybershake root directory. + realisation : str + Realisation name. + + Returns + ------- + str + Path to the SRF file. + """ return os.path.join( cybershake_root, "Data", "Sources", get_srf_location(realisation) ) def get_fault_dir(cybershake_root: str, fault_name: str) -> str: + """ + Get the directory for a specific fault's simulations. + + Parameters + ---------- + cybershake_root : str + Cybershake root directory. + fault_name : str + Fault name. + + Returns + ------- + str + Path to the fault's directory within 'Runs'. + """ return os.path.join(cybershake_root, "Runs", fault_name) def get_sim_dir(cybershake_root: str, realisation: str) -> str: + """ + Get the simulation directory for a specific realisation. + + Parameters + ---------- + cybershake_root : str + Cybershake root directory. + realisation : str + Realisation name. + + Returns + ------- + str + Path to the simulation directory. + """ return os.path.join( get_fault_dir(cybershake_root, get_fault_from_realisation(realisation)), realisation, @@ -52,6 +166,20 @@ def get_sim_dir(cybershake_root: str, realisation: str) -> str: def get_im_calc_dir(sim_root: str, realisation: str | None = None) -> str: + """Get IM Calc directory recursively. + + Parameters + ---------- + sim_root : str + Path to the simulation root directory. + realisation : str, optional + Realisation to fetch IM calc directory for. + + Returns + ------- + str + Path to the IM calc directory. + """ if realisation is None: return os.path.join(sim_root, "IM_calc") else: @@ -59,6 +187,20 @@ def get_im_calc_dir(sim_root: str, realisation: str | None = None) -> str: def get_IM_csv_from_root(cybershake_root: str, realisation: str) -> str: # noqa: N802 + """Get IM csv file for realisation. + + Parameters + ---------- + cybershake_root : str + Cybershake root directory. + realisation : str + Realisation name. + + Returns + ------- + str + Path to the realisation's IM csv file. + """ return os.path.join( get_im_calc_dir(get_sim_dir(cybershake_root, realisation)), "{}.{}".format(realisation, "csv"), @@ -68,7 +210,20 @@ def get_IM_csv_from_root(cybershake_root: str, realisation: str) -> str: # noqa def get_fault_yaml_path(sim_root: str, fault_name: str | None = None) -> str: """ Gets the fault_params.yaml for the specified simulation. - Note: For the manual workflow set fault_name to None as the - fault params are stored directly in the simulation directory. + + For the manual workflow set fault_name to None as the fault params + are stored directly in the simulation directory. + + Parameters + ---------- + sim_root : str + The simulation root directory. + fault_name : str, optional + The fault name, or None. + + Returns + ------- + str + The path to the fault_params.yaml for the given fault. """ return os.path.join(sim_root, fault_name or "", "fault_params.yaml") diff --git a/qcore/siteamp_models.py b/qcore/siteamp_models.py index 177e7c00..f5436cf5 100644 --- a/qcore/siteamp_models.py +++ b/qcore/siteamp_models.py @@ -561,8 +561,6 @@ def cb2014_to_fas_amplification_factors( Parameters ---------- - freqs : np.ndarray - The SA frequencies corresponding to site-amplification factors. ampf0 : np.ndarray The amplification factors. dt : float @@ -571,6 +569,8 @@ def cb2014_to_fas_amplification_factors( The number of timesteps of the waveforms. fmin, fmidbot, fhightop, fmax : float, optional Bandpass filter parameters, see `amp_bandpass`. + freqs : np.ndarray, optional + The SA frequencies corresponding to site-amplification factors. Returns ------- diff --git a/qcore/src_site_dist.py b/qcore/src_site_dist.py index ea0a629e..8be2f128 100644 --- a/qcore/src_site_dist.py +++ b/qcore/src_site_dist.py @@ -39,26 +39,26 @@ def calc_rrup_rjb( Parameters ---------- - srf_points: np.ndarray + srf_points : np.ndarray The fault points from the srf file (qcore, srf.py, read_srf_points), - format (lon, lat, depth) - locations: np.ndarray + format (lon, lat, depth). + locations : np.ndarray The locations for which to calculate the distances, - format (lon, lat, depth) - n_stations_per_iter: int + format (lon, lat, depth). + n_stations_per_iter : int Number of stations to iterate over, default to 1000. Change based on memory requirements - return_rrup_points: bool (optional) default False - If True, returns the lon, lat, depth of the rrup points on the srf + return_rrup_points : bool (optional) default False + If True, returns the lon, lat, depth of the rrup points on the srf. Returns ------- - rrups : np.ndarray - The rrup distance for the locations, shape/order same as locations - rjb : np.ndarray - The rjb distance for the locations, shape/order same as locations - rrups_points : np.ndarray (optional) - The lon, lat, depth of the rrup points, shape/order same as locations + np.ndarray + The rrup distance for the locations, shape/order same as locations. + np.ndarray + The rjb distance for the locations, shape/order same as locations. + np.ndarray (optional) + The lon, lat, depth of the rrup points, shape/order same as locations. """ rrups = np.empty(locations.shape[0], dtype=np.float32) rjb = np.empty(locations.shape[0], dtype=np.float32) @@ -114,10 +114,10 @@ def calc_rx_ry( A list of srf header dictionaries, as retrieved from qcore.srf.get_headers with idx=True. locations : np.ndarray An array with shape (m, 2) giving the lon, lat locations of each location to get Rx, Ry values for. - type : int, optional - Allows switching between the two GC types if desired. Default is 2. hypocentre_origin : bool, optional If True, sets the Ry origin/0 point to the fault trace projection of the hypocentre. If False, the most upstrike subfault of the first fault trace is used. Only used for GC2. + type : int, optional + Allows switching between the two GC types if desired. Default is 2. Returns ------- diff --git a/qcore/timeseries.py b/qcore/timeseries.py index 2b5ad400..f8fff8d6 100644 --- a/qcore/timeseries.py +++ b/qcore/timeseries.py @@ -74,11 +74,11 @@ def bwfilter( np.ndarray The filtered waveform. - See Also - -------- - https://en.wikipedia.org/wiki/Butterworth_filter - https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.sosfiltfilt.html - (specifically, the examples comparing sosfilt and sosfiltfilt) + References + ---------- + - https://en.wikipedia.org/wiki/Butterworth_filter + - https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.sosfiltfilt.html + (specifically, the examples comparing sosfilt and sosfiltfilt) """ cutoff_frequencies: np.ndarray | float = taper_frequency @@ -130,7 +130,7 @@ def ampdeamp( Returns ------- np.ndarray - The input waveform (de)ampilfied at frequencies according to + The input waveform (de)amplified at frequencies according to the values of `amplification_factor`. """ @@ -384,12 +384,12 @@ def _read_lfseis_file(seis_file: Path) -> xr.Dataset: Parameters ---------- seis_file : Path - Path to the LF seis file. + Path to the LF seis file. Returns ------- xr.Dataset - xarray Dataset containing LF seis data. + Dataset containing LF seis data. """ with open(seis_file, "rb") as f: @@ -497,11 +497,16 @@ def read_lfseis_directory(outbin: Path | str, start_sec: float = 0) -> xr.Datase ---------- outbin : Pathlike Path to the OutBin directory containing seis files. Should contain `*seis-*.e3d` files. + start_sec : float, optional + The real-time start second of the simulation. EMOD3D simulations have + a settle time that is included in the broadband simulation. Setting + this to a non-zero number will cause alignment to occur in the + broadband simulation. Returns ------- xr.Dataset - xarray Dataset containing LF seis data. + Dataset containing LF seis data. Raises ------ @@ -560,37 +565,36 @@ def timeseries_to_text( baz: float = 0.0, title: str = "", ) -> None: - """ - Store timeseries data into a text file. + """Store timeseries data into a text file. Parameters ---------- timeseries : np.ndarray - The timeseries data to store + The timeseries data to store. filename : Path - The full file path to store the file + The full file path to store the file. dt : float - The time step of the data + The time step of the data. stat : str - The station name + The station name. comp : str - The component name + The component name. values_per_line : int, optional - The number of values per line, by default 6 + The number of values per line, by default 6. start_hr : int, optional - The start hour of the data, by default 0 + The start hour of the data, by default 0. start_min : int, optional - The start minute of the data, by default 0 + The start minute of the data, by default 0. start_sec : float, optional - The start second of the data, by default 0.0 + The start second of the data, by default 0.0. edist : float, optional - The epicentral distance, by default 0.0 + The epicentral distance, by default 0.0. az : float, optional - The azimuth forward A->B in degrees, by default 0.0 + The azimuth forward A->B in degrees, by default 0.0. baz : float, optional - The azimuth backwards B->A in degrees, by default 0.0 + The azimuth backwards B->A in degrees, by default 0.0. title : str, optional - The optional title added to header + The optional title added to header. """ nt = timeseries.shape[0] with open(filename, "wb") as txt: diff --git a/qcore/uncertainties/distributions.py b/qcore/uncertainties/distributions.py index b91601ac..3057e520 100644 --- a/qcore/uncertainties/distributions.py +++ b/qcore/uncertainties/distributions.py @@ -206,7 +206,7 @@ def truncated_log_normal( Returns ------- - float + float or array of floats Random value from the truncated log-normal distribution. """ x = np.exp( @@ -239,9 +239,16 @@ def rand_shyp(size: int = 1, seed: int | None = None) -> float | np.ndarray: """ Generate a random hypocentre value along the length of a fault. + Parameters + ---------- + size : int, optional + The number of samples to take (default is 1). + seed : int or None, optional + Random seed for reproducibility (default is None). + Returns ------- - float + float or array of floats Random value from a truncated normal distribution (mean=0, std_dev=0.25). """ return truncated_normal(0, 0.25, size=size, seed=seed) diff --git a/qcore/xyts.py b/qcore/xyts.py index 909880d3..2bfdb8f4 100644 --- a/qcore/xyts.py +++ b/qcore/xyts.py @@ -7,10 +7,6 @@ allows users to load metadata, retrieve data, and calculate PGV (Peak Ground Velocity) and MMI (Modified Mercalli Intensity) values from the XYTS file. -Classes ----------------- -- XYTSFile: Represents an XYTS file and provides methods to interact with it. - Notes ----- - This module assumes that the simulation domain is flat, and the timeseries @@ -69,97 +65,121 @@ class XYTSFile: - dip = 0: Simulation domain is flat. - t0 = 0: Complete timeseries from t = 0. - Attributes: - x0: Starting x-coordinate. - y0: Starting y-coordinate. - z0: Starting z-coordinate. - t0: Starting time. - local_nx: Number of local x-coordinates (for proc-local files only). - local_ny: Number of local y-coordinates (for proc-local files only). - local_nz: Number of local z-coordinates (for proc-local files only). - nx: Total number of x-coordinates. - ny: Total number of y-coordinates. - nz: Total number of z-coordinates. - nt: Total number of time steps. - dx: Grid spacing in the x-direction. - dy: Grid spacing in the y-direction. - hh: Grid spacing in the z-direction. - dt: Time step size. - mrot: Rotation angle for model origin. - mlat: Latitude of the model origin. - mlon: Longitude of the model origin. - dxts: Original simulation grid spacing in the x-direction. - dyts: Original simulation grid spacing in the y-direction. - nx_sim: Original simulation size in the x-direction. - ny_sim: Original simulation size in the y-direction. - dip: Dip angle. - comps: Orientation of components (X, Y, Z). - cosR: Cosine of the rotation angle. - sinR: Sine of the rotation angle. - cosP: Cosine of the dip angle. - sinP: Sine of the dip angle. - rot_matrix: Rotation matrix for components. - data: Memory-mapped array containing the data. - ll_map: Longitude-latitude map for data. - - Methods: - __init__(xyts_path, meta_only=False, proc_local_file=False): - Initializes the XYTSFile object by loading metadata and memmapping - data sections. - - corners(gmt_format=False): - Retrieves the corners of the simulation domain. - - region(corners=None): - Returns the simulation region as a tuple (x_min, x_max, y_min, y_max). - - tslice_get(step, comp=-1, outfile=None): - Retrieves timeslice data. - - pgv(mmi=False, pgvout=None, mmiout=None): - Retrieves PGV map and optionally calculates MMI. + Parameters + ---------- + xyts_path : Path | str + Path to the xyts file. + meta_only : bool + If True, only loads metadata and doesn't prepare gridpoint datum + locations (slower). + proc_local_file : bool + If True, indicates a proc-local file. + round_dt : bool + If True, round the dt value to 4dp (present only for backwards + compatibility). + + Raises + ------ + ValueError + ValueError: If the file is not an XY timeslice file. """ # Header values x0: int + """Starting x-coordinate.""" + y0: int + """Starting y-coordinate.""" + z0: int + """Starting z-coordinate.""" + t0: int - ####################### + """Starting time.""" + nx: int + """Total number of x-coordinates.""" + ny: int + """Total number of y-coordinates.""" + nz: int + """Total number of z-coordinates.""" + nt: int + """Total number of time steps.""" + dx: float + """Grid spacing in the x-direction.""" + dy: float + """Grid spacing in the y-direction.""" + hh: float + """Grid spacing in the z-direction.""" + dt: float + """Time step size.""" + mrot: float + """Rotation angle for model origin.""" + mlat: float + """Latitude of the model origin.""" + mlon: float + """Longitude of the model origin.""" + # Derived values dxts: int + """Original simulation grid spacing in the x-direction.""" + dyts: int + """Original simulation grid spacing in the y-direction.""" + nx_sim: int + """Original simulation size in the x-direction.""" + + ny_sim: int + """Original simulation size in the y-direction.""" + dip: float + """Dip angle.""" + comps: dict[str, float] + """Orientation of components (X, Y, Z).""" cos_r: float + """Cosine of the rotation angle.""" sin_r: float + """Sine of the rotation angle.""" cos_p: float + """Cosine of the dip angle.""" sin_p: float + """Sine of the dip angle.""" rot_matrix: np.ndarray + """Rotation matrix for components.""" # proc-local files only local_nx: int | None = None + """Number of local x-coordinates (for proc-local files only).""" local_ny: int | None = None + """Number of local y-coordinates (for proc-local files only).""" local_nz: int | None = None + """Number of local z-coordinates (for proc-local files only).""" + + # data arrays + data: np.memmap | None = None + """Memory-mapped array containing the data.""" + + ll_map: np.ndarray | None = None + """Longitude-latitude map for data.""" # contents data: np.memmap | None = ( @@ -175,27 +195,6 @@ def __init__( # noqa: D107 proc_local_file: bool = False, round_dt: bool = True, ): # numpydoc ignore=GL08 - """Initializes the XYTSFile object. - - Parameters - ---------- - xyts_path : Path | str - Path to the xyts file. - meta_only : bool - If True, only loads metadata and doesn't prepare gridpoint datum - locations (slower). - proc_local_file : bool - If True, indicates a proc-local file. - round_dt : bool - If True, round the dt value to 4dp (present only for backwards - compatibility). - - Raises - ------ - ValueError - ValueError: If the file is not an XY timeslice file. - """ - xytf = open(xyts_path, "rb") self.xyts_path = xyts_path @@ -271,16 +270,21 @@ def __init__( # noqa: D107 ) self.data = np.memmap( xyts_path, - dtype="%sf4" % (endian), + dtype=f"{endian}f4", mode="r", offset=72, - shape=(self.nt, len(self.comps), self.local_ny, self.local_nx), + shape=( + int(self.nt), + len(self.comps), + int(self.local_ny), + int(self.local_nx), + ), ) else: # memory map for data section self.data = np.memmap( xyts_path, - dtype="%sf4" % (endian), + dtype=f"{endian}f4", mode="r", offset=60, shape=(self.nt, len(self.comps), self.ny, self.nx), diff --git a/tests/test_xyts.py b/tests/test_xyts.py index 2194cd1b..fa823e2b 100644 --- a/tests/test_xyts.py +++ b/tests/test_xyts.py @@ -204,7 +204,7 @@ def test_xyts_invalid_file(tmp_path: Path) -> None: # Create a file with invalid header with open(invalid_file, "wb") as f: f.write(b"\x00" * 100) - + with pytest.raises(ValueError, match="File is not an XY timeslice file"): xyts.XYTSFile(str(invalid_file)) @@ -214,11 +214,12 @@ def test_tslice_get_meta_only() -> None: test_file = Path(__file__).parent / "sample1" / "xyts.e3d" if not test_file.exists(): pytest.skip("Test file not available") - + xyts_file = xyts.XYTSFile(str(test_file), meta_only=True) - + with pytest.raises( - AttributeError, match="The data attribute must be set to use `tslice_get`" + AttributeError, + match="The data attribute must be set to use `tslice_get`" ): xyts_file.tslice_get(10, comp=xyts.Component.MAGNITUDE) @@ -228,10 +229,11 @@ def test_pgv_meta_only() -> None: test_file = Path(__file__).parent / "sample1" / "xyts.e3d" if not test_file.exists(): pytest.skip("Test file not available") - + xyts_file = xyts.XYTSFile(str(test_file), meta_only=True) - + with pytest.raises( - AttributeError, match="The data and ll_map attributes must be set to use `pgv`" + AttributeError, + match="The data and ll_map attributes must be set to use `pgv`" ): xyts_file.pgv() From f445e196a24fe05b530451de63e078a63d729f90 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 8 Jan 2026 13:55:45 +1300 Subject: [PATCH 2/5] fix: revert geo imports --- qcore/geo.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qcore/geo.py b/qcore/geo.py index 3b297ecd..250b5624 100644 --- a/qcore/geo.py +++ b/qcore/geo.py @@ -2,6 +2,9 @@ qcore geometry utilities. """ +from math import acos, asin, atan, atan2, cos, degrees, pi, radians, sin, sqrt +from typing import overload + import numpy as np import numpy.typing as npt From a713b494720946c54062539586d7d4d851843322 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 8 Jan 2026 13:56:43 +1300 Subject: [PATCH 3/5] ci: numpydoc run on all pull requests --- .github/workflows/numpydoc.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/numpydoc.yml b/.github/workflows/numpydoc.yml index 4a0a15da..be440bc4 100644 --- a/.github/workflows/numpydoc.yml +++ b/.github/workflows/numpydoc.yml @@ -1,13 +1,6 @@ name: Numpydoc Lint -on: - push: - branches: - - master - pull_request: - branches: - - master - +on: [pull_request] jobs: numpydoc-lint: runs-on: ubuntu-latest @@ -24,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: '3.13' # Adjust version as needed + python-version: "3.13" # Adjust version as needed - name: Install dependencies run: | From 1f26dc9a258e0c3e5dd19aeeaa13ecaea30e2b8a Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 8 Jan 2026 13:58:21 +1300 Subject: [PATCH 4/5] docs(geo): ignore missing docs for overloads --- qcore/geo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qcore/geo.py b/qcore/geo.py index 250b5624..fe5f76da 100644 --- a/qcore/geo.py +++ b/qcore/geo.py @@ -461,7 +461,7 @@ def path_from_corners( output: str | None = None, min_edge_points: int = ..., close: bool = ..., -) -> list[tuple[float, float]]: ... +) -> list[tuple[float, float]]: ... # numpydoc ignore=GL08 @overload @@ -470,7 +470,7 @@ def path_from_corners( output: str = ..., min_edge_points: int = ..., close: bool = ..., -) -> None: ... +) -> None: ... # numpydoc ignore=GL08 def path_from_corners( From d32aee18aea4c482116365c6b6d23b6614c9689a Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 8 Jan 2026 14:04:43 +1300 Subject: [PATCH 5/5] fix: revert geo return type changes --- qcore/geo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qcore/geo.py b/qcore/geo.py index fe5f76da..065fecd0 100644 --- a/qcore/geo.py +++ b/qcore/geo.py @@ -478,7 +478,7 @@ def path_from_corners( output: str | None = "sim.modelpath_hr", min_edge_points: int = 100, close: bool = True, -) -> list[tuple[float | int, float | int]] | None: +) -> list[tuple[float, float]] | None: """ Generate a path connecting the corners of a region with optional subdivision and output.