From 51e2b5d3bfb4ec4dacece9b732bd97eb93e49b67 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Tue, 16 Dec 2025 09:42:15 +1300 Subject: [PATCH 01/44] Adding NZVM3p01 files for upcoming simulations --- nzcvm_registry.yaml | 31 +++++++++++++++++++++++-------- tomography/EP2010/EP2010_New.h5 | 3 +++ tomography/EP2017/EP2017_New.h5 | 3 +++ tomography/EP2020/EP2020_New.h5 | 3 +++ tomography/EP2022/EP2022_Merge.h5 | 3 +++ tomography/EP2022/EP2022_New.h5 | 3 +++ tomography/EP2025/EP2025_New.h5 | 3 +++ 7 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 tomography/EP2010/EP2010_New.h5 create mode 100644 tomography/EP2017/EP2017_New.h5 create mode 100644 tomography/EP2020/EP2020_New.h5 create mode 100644 tomography/EP2022/EP2022_Merge.h5 create mode 100644 tomography/EP2022/EP2022_New.h5 create mode 100644 tomography/EP2025/EP2025_New.h5 diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index dea1d99..452f1b7 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -1,17 +1,24 @@ tomography: - name: EP2010 elev: [ 15, 1, -3, -8, -15, -23, -30, -38, -48, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] - path: tomography/EP2010/ep2010.h5 + path: tomography/EP2010/EP2010_New.h5 author: Eberhart-Phillips et al. (2010) title: Establishing a Versatile 3-d seismic Velocity Model for New Zealand url: https://10.1785/gssrl.81.6.992 + - name: EP2017 + elev: [ 15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] + path: tomography/EP2017/EP2017_New.h5 + author: Eberhart-Phillips et al. (2017) + title: New Zealand Wide model 2.1 seismic velocity and Qs and Qp models for New Zealand + url: https://zenodo.org/record/1043558 + - name: EP2020 elev: [ 15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] - path: tomography/EP2020/ep2020.h5 + path: tomography/EP2020/EP2020_New.h5 author: Eberhart-Phillips et al. (2020) title: New Zealand Wide model 2.2 seismic velocity and Qs and Qp models for New Zealand - url: https://10.5281/zenodo.3779523 + url: https://zenodo.org/records/3779523 - name: CHOW2020_EP2020_MIX @@ -21,13 +28,21 @@ tomography: url: - https://doi.org/10.1093/gji/ggaa381 - https://core.geo.vuw.ac.nz/d/feae69f61ea54f81bee1 - + + + - name: EP2022 + elev: [15,1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] + path: tomography/EP2022/EP2022_New.h5 + author: Eberhart-Phillips et al. (2022) + title: New Zealand Wide model 2.3 seismic velocity and Qs and Qp models for New Zealand + url: https://zenodo.org/records/5098356 + - name: EP2025 - elev: [ 1, -3, -8, -15, -23, -30, -38, -48, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] - path: tomography/EP2025/ep2025.h5 + elev: [15,1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] + path: tomography/EP2025/EP2025_New.h5 author: Eberhart-Phillips et al. (2025) - title: New Zealand Wide model 2.2 seismic velocity and Qs and Qp models for New Zealand - url: n/a + title: New Zealand Wide model 3.1 seismic velocity and Qs and Qp models for New Zealand + url: Through Email basin: - name: Canterbury_v18p1 diff --git a/tomography/EP2010/EP2010_New.h5 b/tomography/EP2010/EP2010_New.h5 new file mode 100644 index 0000000..6f0725b --- /dev/null +++ b/tomography/EP2010/EP2010_New.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:469158e5f6013147b31490e887be00979e02ddcf520921982986246374ff140e +size 216365550 diff --git a/tomography/EP2017/EP2017_New.h5 b/tomography/EP2017/EP2017_New.h5 new file mode 100644 index 0000000..595bfbb --- /dev/null +++ b/tomography/EP2017/EP2017_New.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f4e0c9f68d6c011f5dc321050082c279082fff6db9904fb99bad3a64137f3aa +size 300590835 diff --git a/tomography/EP2020/EP2020_New.h5 b/tomography/EP2020/EP2020_New.h5 new file mode 100644 index 0000000..027a64e --- /dev/null +++ b/tomography/EP2020/EP2020_New.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c77e69a5c8cee988e0ecc94cc2179388ab6342ed0a5215b6992be2ddde262de0 +size 302585527 diff --git a/tomography/EP2022/EP2022_Merge.h5 b/tomography/EP2022/EP2022_Merge.h5 new file mode 100644 index 0000000..a3b9d48 --- /dev/null +++ b/tomography/EP2022/EP2022_Merge.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc162d10a9a49229bca18bd148a3f9737de00600f6942022df0cd71e47afa170 +size 302770576 diff --git a/tomography/EP2022/EP2022_New.h5 b/tomography/EP2022/EP2022_New.h5 new file mode 100644 index 0000000..77be18c --- /dev/null +++ b/tomography/EP2022/EP2022_New.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e95edd6fa8b77f600889572c0447c2806443ec63a2df0d8d1621ffa5fe5e585 +size 302770632 diff --git a/tomography/EP2025/EP2025_New.h5 b/tomography/EP2025/EP2025_New.h5 new file mode 100644 index 0000000..91a3546 --- /dev/null +++ b/tomography/EP2025/EP2025_New.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a606efa4ef330d4a66bb34f34c4bac220ced43d120a4e1de4e5e9499be37f0a +size 279828379 From f3a8d38201d86c7041c8847b0534b663c67729b8 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Wed, 17 Dec 2025 14:16:42 +1300 Subject: [PATCH 02/44] Latest Rectilinear lat/lon grid (1 km spacing) --- tomography/EP2010/EP2010_New.h5 | 4 ++-- tomography/EP2017/EP2017_New.h5 | 4 ++-- tomography/EP2020/EP2020_New.h5 | 4 ++-- tomography/EP2022/EP2022_Merge.h5 | 3 --- tomography/EP2022/EP2022_New.h5 | 4 ++-- tomography/EP2025/EP2025_New.h5 | 4 ++-- 6 files changed, 10 insertions(+), 13 deletions(-) delete mode 100644 tomography/EP2022/EP2022_Merge.h5 diff --git a/tomography/EP2010/EP2010_New.h5 b/tomography/EP2010/EP2010_New.h5 index 6f0725b..9e23a5c 100644 --- a/tomography/EP2010/EP2010_New.h5 +++ b/tomography/EP2010/EP2010_New.h5 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:469158e5f6013147b31490e887be00979e02ddcf520921982986246374ff140e -size 216365550 +oid sha256:37d1eac30bbdedeb7a75985d701cd43e2cdc4a5f530140d69c6d3556ee44e7d2 +size 221080274 diff --git a/tomography/EP2017/EP2017_New.h5 b/tomography/EP2017/EP2017_New.h5 index 595bfbb..528fd2d 100644 --- a/tomography/EP2017/EP2017_New.h5 +++ b/tomography/EP2017/EP2017_New.h5 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f4e0c9f68d6c011f5dc321050082c279082fff6db9904fb99bad3a64137f3aa -size 300590835 +oid sha256:8c0b8a10cac4c6a799109e3560e5fa16bade54b839c6e5899949f64ac757dfdd +size 304064657 diff --git a/tomography/EP2020/EP2020_New.h5 b/tomography/EP2020/EP2020_New.h5 index 027a64e..9643709 100644 --- a/tomography/EP2020/EP2020_New.h5 +++ b/tomography/EP2020/EP2020_New.h5 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c77e69a5c8cee988e0ecc94cc2179388ab6342ed0a5215b6992be2ddde262de0 -size 302585527 +oid sha256:56d28ab8c13a295573b2086eee24501f2e0be5aa1c8b6d1f5698091865f27ca9 +size 306468708 diff --git a/tomography/EP2022/EP2022_Merge.h5 b/tomography/EP2022/EP2022_Merge.h5 deleted file mode 100644 index a3b9d48..0000000 --- a/tomography/EP2022/EP2022_Merge.h5 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fc162d10a9a49229bca18bd148a3f9737de00600f6942022df0cd71e47afa170 -size 302770576 diff --git a/tomography/EP2022/EP2022_New.h5 b/tomography/EP2022/EP2022_New.h5 index 77be18c..cadc33b 100644 --- a/tomography/EP2022/EP2022_New.h5 +++ b/tomography/EP2022/EP2022_New.h5 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e95edd6fa8b77f600889572c0447c2806443ec63a2df0d8d1621ffa5fe5e585 -size 302770632 +oid sha256:734b7a1a319eb91e2f0a9c23d2f61226bcc9f086dfa882b61e07ad0dd77d2535 +size 306689477 diff --git a/tomography/EP2025/EP2025_New.h5 b/tomography/EP2025/EP2025_New.h5 index 91a3546..6f0d9c8 100644 --- a/tomography/EP2025/EP2025_New.h5 +++ b/tomography/EP2025/EP2025_New.h5 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a606efa4ef330d4a66bb34f34c4bac220ced43d120a4e1de4e5e9499be37f0a -size 279828379 +oid sha256:ac2f5f9e1b8af08f5423aebaf83d1d516ae3ea4277282c57eb3376a92ffcbb97 +size 284439990 From 37cc24866b6fbf973e1da50b8916ca6185cc8e9a Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Wed, 17 Dec 2025 14:17:59 +1300 Subject: [PATCH 03/44] Updating registry --- nzcvm_registry.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index 452f1b7..d0bddca 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -31,18 +31,17 @@ tomography: - name: EP2022 - elev: [15,1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] + elev: [15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750] path: tomography/EP2022/EP2022_New.h5 author: Eberhart-Phillips et al. (2022) title: New Zealand Wide model 2.3 seismic velocity and Qs and Qp models for New Zealand url: https://zenodo.org/records/5098356 - name: EP2025 - elev: [15,1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] + elev: [15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750] path: tomography/EP2025/EP2025_New.h5 author: Eberhart-Phillips et al. (2025) title: New Zealand Wide model 3.1 seismic velocity and Qs and Qp models for New Zealand - url: Through Email basin: - name: Canterbury_v18p1 From 1ed5b464f145191a9021b05d75b298b59b66a418 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Wed, 17 Dec 2025 17:09:50 +1300 Subject: [PATCH 04/44] add testing code --- pyproject.toml | 17 +++ tests/test_registry.py | 240 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 pyproject.toml create mode 100644 tests/test_registry.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4263f63 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ + +[project] +name = "nzcvm_data" +version = "0.1.0" +description = "Integrity checks for NZCVM data registry" +dependencies = [ + "numpy>=2.3.5", + "pytest-subtests>=0.15.0", + "schema>=0.7.8", +] + + +[project.optional-dependencies] +test = ["h5py>=3.15.1", "pytest>=9.0.2", "pyyaml>=6.0.3"] +# Even though this isn't a python package, these dev dependencies can +# still prove useful for writing tests. +dev = ["ruff", "numpydoc", "ty"] diff --git a/tests/test_registry.py b/tests/test_registry.py new file mode 100644 index 0000000..a3fbc32 --- /dev/null +++ b/tests/test_registry.py @@ -0,0 +1,240 @@ +from pathlib import Path +from typing import TypedDict + +import h5py +import numpy as np +import pytest +import yaml +from pytest_subtests import SubTests +from schema import Optional, Or, Regex, Schema, SchemaError + + +@pytest.fixture(scope="session") +def nzcvm_registry_path() -> Path: + return Path(__file__).parent.parent / "nzcvm_registry.yaml" + + +@pytest.fixture(scope="session") +def nzcvm_root() -> Path: + return Path(__file__).parent.parent + + +def test_nzcvm_registry_schema(nzcvm_registry_path: Path) -> None: + path = Regex( + # See https://stackoverflow.com/a/537876 + r"[^\0]+", + error="Must be valid unix path.", + ) + ident = Regex(r"^[a-zA-Z_][a-zA-Z0-9_]*$", error="Must be valid python identifier.") + # Source - https://stackoverflow.com/a + # Posted by Daveo, modified by community. See post 'Timeline' for change history + # Retrieved 2025-12-17, License - CC BY-SA 4.0 + url = Or( + Regex( + r"https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)" + ), + "Personal communication (pending publication)", + error='Must be a valid URL, or "Personal communication (pending publication)"', + ) + surface_schema = Schema({"path": path, Optional("submodel"): ident}) + + tomography_entry = Schema( + { + "name": ident, + "elev": [Or(int, float)], + "path": path, + "author": str, + Optional("title"): str, + "url": Or(url, [url], None), # Handles single URL, list of URLs, or empty + } + ) + + basin_entry = Schema( + { + "name": ident, + Optional("type"): Or(1, 2, 3, 4), + Optional("author"): str, + Optional("notes"): [str], + Optional("wiki_images"): [path], + "boundaries": [path], + "surfaces": [surface_schema], + Optional("smoothing"): str, + } + ) + + submodel_schema = Schema( + { + "name": ident, + "type": Or("vm1d", "tomography", "relation", "perturbation", None), + Optional("module"): ident, + Optional("data"): path, + } + ) + vs30_schema = Schema({"name": str, "path": path}) + registry_schema = Schema( + { + "tomography": [tomography_entry], + "basin": [basin_entry], + "basin": [basin_entry], + "submodel": [submodel_schema], + "vs30": [vs30_schema], + } + ) + + with open(nzcvm_registry_path, "r") as f: + registry = yaml.safe_load(f) + + try: + registry_schema.validate(registry) + except SchemaError as e: + pytest.fail(f"NZCVM Registry is invalid\n{e.code}") + + +class Tomography(TypedDict): + name: str + elev: list[float] + path: str + author: str + title: str + url: str + + +def pytest_generate_tests(metafunc) -> None: + if "model" in metafunc.fixturenames: + # This assumes you have access to the registry here + # It creates a separate test case for every model entry + registry_path = Path(__file__).parent.parent / "nzcvm_registry.yaml" + with open(registry_path) as f: + registry = yaml.safe_load(f) + metafunc.parametrize("model", registry["tomography"], ids=lambda m: m["name"]) + + +def test_tomography_paths_exist(nzcvm_root: Path, model: Tomography) -> None: + relative_model_path = Path(model["path"]) + model_path = nzcvm_root / relative_model_path + assert model_path.exists() + + +def test_tomography_models_are_hdf5(nzcvm_root: Path, model: Tomography) -> None: + relative_model_path = Path(model["path"]) + model_path = nzcvm_root / relative_model_path + try: + with h5py.File(model_path, "r") as f: + assert len(f.keys()) > 0 + except Exception as e: + pytest.fail(f"{model['name']} is not a valid hdf5 file: {e}") + + +def test_tomography_elevations_match(nzcvm_root: Path, model: Tomography) -> None: + relative_model_path = Path(model["path"]) + model_path = nzcvm_root / relative_model_path + registry_elevations = sorted(model["elev"]) + with h5py.File(model_path, "r") as f: + model_elevations = sorted([float(elev) for elev in f.keys()]) + + assert registry_elevations == model_elevations, "Model elevations do not match." + + +def test_tomography_compatible_shapes( + subtests: SubTests, nzcvm_root: Path, model: Tomography +) -> None: + model_path = nzcvm_root / model["path"] + + with h5py.File(model_path, "r") as f: + for elev, group in f.items(): + name = model["name"] + with subtests.test(msg=f"Checking elevation {elev}", model=name, elev=elev): + lat_shape = group["latitudes"].shape + lon_shape = group["longitudes"].shape + + # Check data arrays against (lat, lon) + expected_data_shape = (lat_shape[0], lon_shape[0]) + + for field in ["vp", "vs", "rho"]: + actual_shape = group[field].shape + assert actual_shape == expected_data_shape, ( + f"Shape mismatch in {model_path.name} at {elev}m: " + f"{field} is {actual_shape}, expected {expected_data_shape}" + ) + + +R_EARTH = 6378.139 # km, from: https://github.com/ucgmsim/velocity_modelling/blob/27c7e6e64d7ce1a9e543d58a6e584d498358431c/velocity_modelling/constants.py#L15 +LONGITUDE_TOLERANCE = 0.01 # km +LATITUDE_TOLERANCE = 0.01 # km +LAT_DEGREES_PER_KM = np.pi / 180 * R_EARTH + + +def test_tomography_geo_gridpoints( + subtests: SubTests, nzcvm_root: Path, model: Tomography +) -> None: + relative_model_path = Path(model["path"]) + model_path = nzcvm_root / relative_model_path + + with h5py.File(model_path, "r") as f: + for elev, group in f.items(): + name = model["name"] + with subtests.test(msg=f"Checking elevation {elev}", model=name, elev=elev): + latitude = np.array(group["latitudes"]) + longitude = np.array(group["longitudes"]) + + lat_diffs_km = np.diff(latitude) * LAT_DEGREES_PER_KM + assert np.all(lat_diffs_km > 0), "Latitudes not strictly ascending" + assert latitude[0] >= -90 and latitude[-1] <= 90, ( + "Latitudes must be between -90 and 90." + ) + assert lat_diffs_km == pytest.approx( + np.full(len(lat_diffs_km), lat_diffs_km[0]), abs=LATITUDE_TOLERANCE + ) + + lon_diffs_deg = np.diff(longitude) # Shape (N-1,) + assert np.all(lon_diffs_deg > 0), "Longitudes not strictly ascending" + assert longitude[0] >= 0 and longitude[-1] <= 185, ( + "Longitudes must be between 0 and 185." + ) + cos_lats = np.cos(np.radians(latitude)) + + grid_lon_spacings_km = ( + lon_diffs_deg[np.newaxis, :] + * LAT_DEGREES_PER_KM + * cos_lats[:, np.newaxis] + ) + + target_km = lon_diffs_deg[0] * LAT_DEGREES_PER_KM * cos_lats[0] + + max_error = np.max(np.abs(grid_lon_spacings_km - target_km)) + + assert max_error < LONGITUDE_TOLERANCE, ( + f"Longitude spacing variation ({max_error:.6f} km) " + f"exceeds tolerance ({LONGITUDE_TOLERANCE} km)" + ) + + +QUALITY_BOUNDS = {"vp": (0, 10.0), "vs": (0, 6.0), "rho": (0, 5.0)} + + +@pytest.mark.parametrize("quality", ["vp", "rho", "vs"]) +def test_tomography_quality( + subtests: SubTests, nzcvm_root: Path, model: Tomography, quality: str +) -> None: + relative_model_path = Path(model["path"]) + model_path = nzcvm_root / relative_model_path + + with h5py.File(model_path, "r") as f: + for elev, group in f.items(): + name = model["name"] + with subtests.test(msg=f"Checking elevation {elev}", model=name, elev=elev): + quality_values = np.array(group[quality]) + assert not np.isnan(quality_values).any(), ( + f"Quality {quality} contains NaN values." + ) + bounds = QUALITY_BOUNDS.get(quality) + assert bounds + min, max = bounds + quality_min = quality_values.min() + quality_max = quality_values.max() + assert quality_min >= min, ( + f"Quality {quality} minimum value ({quality_min=}) is less than {min}." + ) + assert quality_max <= max, ( + f"Quality {quality} maximum value ({quality_max=}) is less than {max}." + ) From 59aa2a5333967c5af0bab37708ad34f9460b206f Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 10:15:14 +1300 Subject: [PATCH 05/44] test: update QUALITY_BOUNDS values for vp and vs in test_registry - Increase upper bound for vp from 10.0 to 11.0 - Increase upper bound for vs from 6.0 to 6.5 - Add note clarifying values are not physically derived --- tests/test_registry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_registry.py b/tests/test_registry.py index 52524f1..9bce52e 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -212,7 +212,8 @@ def test_tomography_geo_gridpoints( ) -QUALITY_BOUNDS = {"vp": (0, 10.0), "vs": (0, 6.0), "rho": (0, 5.0)} +# NOTE: Values for QUALITY_BOUNDS are not physically derived +QUALITY_BOUNDS = {"vp": (0, 11.0), "vs": (0, 6.5), "rho": (0, 5.0)} @pytest.mark.parametrize("quality", ["vp", "rho", "vs"]) From 0f6c199511b36d22adf6e57eb0806bb0093e25a9 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 10:15:43 +1300 Subject: [PATCH 06/44] test: remove longitude spacing tolerance check from tomography geo gridpoints test --- tests/test_registry.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/tests/test_registry.py b/tests/test_registry.py index 9bce52e..4282240 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -194,22 +194,6 @@ def test_tomography_geo_gridpoints( assert longitude[0] >= 0 and longitude[-1] <= 185, ( "Longitudes must be between 0 and 185." ) - cos_lats = np.cos(np.radians(latitude)) - - grid_lon_spacings_km = ( - lon_diffs_deg[np.newaxis, :] - * LAT_DEGREES_PER_KM - * cos_lats[:, np.newaxis] - ) - - target_km = lon_diffs_deg[0] * LAT_DEGREES_PER_KM * cos_lats[0] - - max_error = np.max(np.abs(grid_lon_spacings_km - target_km)) - - assert max_error < LONGITUDE_TOLERANCE, ( - f"Longitude spacing variation ({max_error:.6f} km) " - f"exceeds tolerance ({LONGITUDE_TOLERANCE} km)" - ) # NOTE: Values for QUALITY_BOUNDS are not physically derived From 301c8fe5f621a9bc14fd2e266a1b75d256e166c0 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 10:15:53 +1300 Subject: [PATCH 07/44] chore(pyproject): add empty types optional dep --- pyproject.toml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4263f63..101a593 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,11 +3,7 @@ name = "nzcvm_data" version = "0.1.0" description = "Integrity checks for NZCVM data registry" -dependencies = [ - "numpy>=2.3.5", - "pytest-subtests>=0.15.0", - "schema>=0.7.8", -] +dependencies = ["numpy>=2.3.5", "pytest-subtests>=0.15.0", "schema>=0.7.8"] [project.optional-dependencies] @@ -15,3 +11,4 @@ test = ["h5py>=3.15.1", "pytest>=9.0.2", "pyyaml>=6.0.3"] # Even though this isn't a python package, these dev dependencies can # still prove useful for writing tests. dev = ["ruff", "numpydoc", "ty"] +types = [] From 25032bc2c173c4c189b07bd198dc66cc3524b8f4 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 10:21:16 +1300 Subject: [PATCH 08/44] tests: update quality bounds to ayushi's values --- tests/test_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_registry.py b/tests/test_registry.py index 4282240..8018f82 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -197,7 +197,7 @@ def test_tomography_geo_gridpoints( # NOTE: Values for QUALITY_BOUNDS are not physically derived -QUALITY_BOUNDS = {"vp": (0, 11.0), "vs": (0, 6.5), "rho": (0, 5.0)} +QUALITY_BOUNDS = {"vp": (0, 11.0), "vs": (0, 7.0), "rho": (0, 5.0)} @pytest.mark.parametrize("quality", ["vp", "rho", "vs"]) From 8e9c53312b00e74ac85385e294ec3c4db79d7738 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 12:06:23 +1300 Subject: [PATCH 09/44] ci: add github actions --- .github/workflows/pytest.yml | 15 +++++++++++++++ .github/workflows/ruff.yml | 8 ++++++++ .github/workflows/types.yml | 22 ++++++++++++++++++++++ .github/workflows/yamllint.yml | 17 +++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 .github/workflows/pytest.yml create mode 100644 .github/workflows/ruff.yml create mode 100644 .github/workflows/types.yml create mode 100644 .github/workflows/yamllint.yml diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..a5723e9 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,15 @@ +name: Pytest Check +on: [pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup UV + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: | + **/pyproject.toml + run: uv run --extra test pytest tests diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..8e10796 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,8 @@ +name: Ruff +on: [pull_request] +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 diff --git a/.github/workflows/types.yml b/.github/workflows/types.yml new file mode 100644 index 0000000..522d468 --- /dev/null +++ b/.github/workflows/types.yml @@ -0,0 +1,22 @@ +name: Type Check + +on: [pull_request] + +jobs: + typecheck: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Run type checking with ty + run: uv run --extra dev --extra types ty check diff --git a/.github/workflows/yamllint.yml b/.github/workflows/yamllint.yml new file mode 100644 index 0000000..c35dfdb --- /dev/null +++ b/.github/workflows/yamllint.yml @@ -0,0 +1,17 @@ +name: "Yamllint GitHub Actions" +on: + - pull_request +jobs: + yamllint: + name: "Yamllint" + runs-on: ubuntu-latest + steps: + - name: "Checkout" + uses: actions/checkout@master + - name: "Yamllint" + uses: karancode/yamllint-github-action@master + with: + yamllint_strict: true + yamllint_comment: true + env: + GITHUB_ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 964decc1bff39b81ab06ec108ed628d8bb30735b Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 12:06:49 +1300 Subject: [PATCH 10/44] test: add comprehensive basin, vs30, and submodel validation tests - Add Basin, Vs30, and Submodel TypedDicts for schema validation - Parametrize tests for basin, vs30, and submodel entries from registry - Add tests for basin boundaries, surfaces, smoothing, and containment - Add tests for vs30 file existence, HDF5 validity, and gridpoint checks - Add tests for submodel data existence and content validation - Improve test coverage and robustness for registry-driven datasets --- tests/test_registry.py | 402 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 397 insertions(+), 5 deletions(-) diff --git a/tests/test_registry.py b/tests/test_registry.py index 8018f82..fc8c43e 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -1,10 +1,16 @@ +import itertools +import re +from json import JSONDecodeError from pathlib import Path -from typing import TypedDict +from typing import NotRequired, TypedDict import h5py import numpy as np import pytest +import shapely import yaml +from attr import dataclass +from pytest import Metafunc from pytest_subtests import SubTests from schema import Optional, Or, Regex, Schema, SchemaError @@ -102,14 +108,47 @@ class Tomography(TypedDict): url: str -def pytest_generate_tests(metafunc) -> None: +class Surface(TypedDict): + path: str + submodel: NotRequired[str] + + +class Basin(TypedDict): + name: str + author: str + notes: list[str] + wiki_images: list[str] + boundaries: list[str] + surfaces: list[Surface] + smoothing: NotRequired[str] + + +class Vs30(TypedDict): + name: str + path: str + + +class Submodel(TypedDict): + name: str + type: str + module: str + data: NotRequired[str] + + +def pytest_generate_tests(metafunc: Metafunc) -> None: + registry_path = Path(__file__).parent.parent / "nzcvm_registry.yaml" + with open(registry_path) as f: + registry = yaml.safe_load(f) if "model" in metafunc.fixturenames: # This assumes you have access to the registry here # It creates a separate test case for every model entry - registry_path = Path(__file__).parent.parent / "nzcvm_registry.yaml" - with open(registry_path) as f: - registry = yaml.safe_load(f) metafunc.parametrize("model", registry["tomography"], ids=lambda m: m["name"]) + elif "basin" in metafunc.fixturenames: + metafunc.parametrize("basin", registry["basin"], ids=lambda m: m["name"]) + elif "vs30" in metafunc.fixturenames: + metafunc.parametrize("vs30", registry["vs30"], ids=lambda m: m["name"]) + elif "submodel" in metafunc.fixturenames: + metafunc.parametrize("submodel", registry["submodel"], ids=lambda m: m["name"]) def test_tomography_paths_exist(nzcvm_root: Path, model: Tomography) -> None: @@ -226,3 +265,356 @@ def test_tomography_quality( assert quality_max <= max, ( f"Quality {quality} maximum value ({quality_max=}) is less than {max}." ) + + +def test_basin_boundaries_exist( + subtests: SubTests, nzcvm_root: Path, basin: Basin +) -> None: + for boundary in basin["boundaries"]: + boundary_relative_path = Path(boundary) + name = basin["name"] + with subtests.test( + msg=f"Checking boundary {boundary_relative_path.stem}", + basin=name, + boundary=boundary_relative_path.stem, + ): + boundary_path = nzcvm_root / boundary_relative_path + assert boundary_path.exists() + + +def test_basin_boundaries_are_valid_geojson( + subtests: SubTests, nzcvm_root: Path, basin: Basin +) -> None: + for boundary in basin["boundaries"]: + boundary_relative_path = Path(boundary) + name = basin["name"] + boundary_name = boundary_relative_path.stem + + with subtests.test( + msg=f"Checking boundary {boundary_name}", basin=name, boundary=boundary_name + ): + boundary_path = nzcvm_root / boundary_relative_path + + try: + geojson_str = boundary_path.read_text() + geom_collection = shapely.from_geojson(geojson_str) + except (OSError, ValueError, Exception) as e: + pytest.fail(f"Model {name} has invalid boundary {boundary_name}: {e}") + + assert isinstance(geom_collection, shapely.GeometryCollection) + assert shapely.is_valid(geom_collection), ( + "Geometry is topologically invalid" + ) + + assert not shapely.is_empty(geom_collection), "Geometry is empty" + + assert all( + isinstance(geom, shapely.Polygon) for geom in geom_collection.geoms + ) + + +def test_basin_paths_exist(nzcvm_root: Path, basin: Basin) -> None: + if match := re.match(r"^(\w+)_v\d+p\d+$", basin["name"]): + basin_canonical_name = match.group(1) + assert basin_canonical_name + basin_path = nzcvm_root / "regional" / basin_canonical_name + assert basin_path.exists() + if "wiki_images" in basin: + assert all( + (basin_path / Path(image)).exists() for image in basin["wiki_images"] + ) + else: + pytest.fail("basin name does not follow structure name_vxxpxx") + + +def test_basin_surfaces_exist( + subtests: SubTests, nzcvm_root: Path, basin: Basin +) -> None: + for surface in basin["surfaces"]: + relative_surface_path = Path(surface["path"]) + name = basin["name"] + surface_name = relative_surface_path.stem + with subtests.test( + msg=f"Checking surface {surface_name}", basin=name, surface=surface_name + ): + surface_path = nzcvm_root / relative_surface_path + assert surface_path.exists() + + +def test_basin_surfaces_are_valid_hdf5( + subtests: SubTests, nzcvm_root: Path, basin: Basin +) -> None: + for surface in basin["surfaces"]: + relative_surface_path = Path(surface["path"]) + name = basin["name"] + surface_name = relative_surface_path.stem + with subtests.test( + msg=f"Checking surface {surface_name}", basin=name, surface=surface_name + ): + surface_path = nzcvm_root / relative_surface_path + try: + with h5py.File(surface_path, "r") as f: + assert "elevation" in f.keys() + assert "latitude" in f.keys() + assert "longitude" in f.keys() + except Exception as e: + pytest.fail(f"{surface_name} is not a valid hdf5 file: {e}") + + +def test_surface_geo_gridpoints( + subtests: SubTests, nzcvm_root: Path, basin: Basin +) -> None: + for surface in basin["surfaces"]: + relative_surface_path = Path(surface["path"]) + name = basin["name"] + surface_name = relative_surface_path.stem + with subtests.test( + msg=f"Checking surface {surface_name}", basin=name, surface=surface_name + ): + surface_path = nzcvm_root / relative_surface_path + with h5py.File(surface_path, "r") as f: + latitude = np.array(f["latitude"]) + longitude = np.array(f["longitude"]) + + lat_diffs_km = np.diff(latitude) * LAT_DEGREES_PER_KM + assert np.all(lat_diffs_km > 0), "Latitudes not strictly ascending" + assert latitude[0] >= -90 and latitude[-1] <= 90, ( + "Latitudes must be between -90 and 90." + ) + + lon_diffs_deg = np.diff(longitude) # Shape (N-1,) + assert np.all(lon_diffs_deg > 0), "Longitudes not strictly ascending" + assert longitude[0] >= 0 and longitude[-1] <= 185, ( + "Longitudes must be between 0 and 185." + ) + + elevation = np.array(f["elevation"]) + assert elevation.shape == (len(latitude), len(longitude)), ( + "Elevation shape must be match latitude and longitude" + ) + assert not np.isnan(elevation).any(), "Elevations cannot be NaN" + assert elevation.min() >= -10000, "Elevations cannot be below -10000m" + assert elevation.max() <= 10000, "Elevations cannot be above 10000m" + + +def read_smoothing_boundary(smoothing_path: Path) -> shapely.LineString: + coords: list[tuple[float, float]] = [] + with open(smoothing_path, "r") as f: + for line in f: + line = line.strip() + if not line: + continue + line_coords = re.split(r"\s+", line) + assert len(line_coords) == 2 + lon_str, lat_str = line_coords + assert lon_str and lat_str + coords.append((float(lon_str), float(lat_str))) + + return shapely.LineString(coords) + + +from pathlib import Path + +import shapely + + +def test_basin_smoothing_contained_in_boundaries( + subtests: SubTests, nzcvm_root: Path, basin: Basin, tmp_path: Path +) -> None: + if "smoothing" not in basin: + pytest.skip("basin has no smoothing boundary") + + # 1. Load Basin Boundaries + boundaries = [] + for boundary in basin["boundaries"]: + boundary_path = nzcvm_root / Path(boundary) + geojson_str = boundary_path.read_text() + geom_collection = shapely.from_geojson(geojson_str) + boundaries.append(geom_collection) + + boundary_geometry = shapely.union_all(boundaries) + + # 2. Load Smoothing Boundary + smoothing_surface_path = nzcvm_root / Path(basin["smoothing"]) + smoothing_boundary = read_smoothing_boundary(smoothing_surface_path) + + # 3. Perform Check + is_contained = boundary_geometry.contains(smoothing_boundary) + + # 4. Handle Failure and Save Artifacts + if not is_contained: + # Calculate intersection for debugging + intersection = shapely.intersection(boundary_geometry, smoothing_boundary) + + # Use a slug for the filename (assuming basin has a name or ID) + basin_name = basin.get("name", "unknown_basin").replace(" ", "_") + + +def test_basin_smoothing_contained_in_boundaries( + subtests: SubTests, nzcvm_root: Path, basin: Basin +) -> None: + if "smoothing" not in basin: + pytest.skip("basin has no smoothing boundary") + + boundaries = [] + for boundary in basin["boundaries"]: + boundary_relative_path = Path(boundary) + boundary_path = nzcvm_root / boundary_relative_path + geojson_str = boundary_path.read_text() + geom_collection = shapely.from_geojson(geojson_str) + boundaries.append(geom_collection) + + # Add a small smoothing boundary buffer to account for the fact + # that smoothing boundary is not perfectly contained in the basin + # boundary. + boundary_geometry = shapely.buffer(shapely.union_all(boundaries), 0.001) + smoothing_surface_relative_path = basin["smoothing"] + smoothing_surface_path = nzcvm_root / Path(smoothing_surface_relative_path) + smoothing_boundary = read_smoothing_boundary(smoothing_surface_path) + + assert boundary_geometry.contains(smoothing_boundary) + + +def test_basin_surfaces_contain_boundaries( + subtests: SubTests, nzcvm_root: Path, basin: Basin +) -> None: + for boundary, surface in itertools.product(basin["boundaries"], basin["surfaces"]): + boundary_relative_path = Path(boundary) + surface_relative_path = Path(surface["path"]) + basin_name = basin["name"] + boundary_name = boundary_relative_path.stem + surface_name = surface_relative_path.stem + + with subtests.test( + msg=f"Checking boundary {boundary_name}", + basin=basin_name, + boundary=boundary_name, + surface=surface_name, + ): + boundary_path = nzcvm_root / boundary_relative_path + + geojson_str = boundary_path.read_text() + geom_collection = shapely.from_geojson(geojson_str) + surface_path = nzcvm_root / surface_relative_path + with h5py.File(surface_path, "r") as f: + latitudes = np.array(f["latitude"]) + longitudes = np.array(f["longitude"]) + elevation_boundary = shapely.box( + xmin=longitudes.min(), + xmax=longitudes.max(), + ymin=latitudes.min(), + ymax=latitudes.max(), + ) + assert shapely.contains(elevation_boundary, geom_collection) + + +def test_vs30_file_exists(nzcvm_root: Path, vs30: Vs30) -> None: + relative_path = Path(vs30["path"]) + name = vs30["name"] + + vs30_path = nzcvm_root / relative_path + assert vs30_path.exists(), f"Vs30 file missing at {vs30_path}" + + +def test_vs30_is_valid_hdf5(nzcvm_root: Path, vs30: Vs30) -> None: + relative_path = Path(vs30["path"]) + name = vs30["name"] + vs30_path = nzcvm_root / relative_path + + try: + with h5py.File(vs30_path, "r") as f: + assert "elevation" in f.keys(), ( + "Dataset 'elevation' (vs30) missing from HDF5" + ) + assert "latitude" in f.keys() + assert "longitude" in f.keys() + except Exception as e: + pytest.fail(f"Vs30 file {name} is not a valid hdf5 file: {e}") + + +def test_vs30_geo_gridpoints(nzcvm_root: Path, vs30: Vs30) -> None: + relative_path = Path(vs30["path"]) + name = vs30["name"] + vs30_path = nzcvm_root / relative_path + + with h5py.File(vs30_path, "r") as f: + latitude = np.array(f["latitude"]) + longitude = np.array(f["longitude"]) + vs30_values = np.array(f["elevation"]) + + lat_diffs = np.diff(latitude) + assert np.all(lat_diffs > 0), "Latitudes not strictly ascending" + assert latitude[0] >= -90 and latitude[-1] <= 90, ( + "Latitudes out of world bounds" + ) + + lon_diffs = np.diff(longitude) + assert np.all(lon_diffs > 0), "Longitudes not strictly ascending" + assert longitude[0] >= 0 and longitude[-1] <= 185, ( + "Longitudes out of NZCVM bounds" + ) + + assert vs30_values.shape == (len(latitude), len(longitude)), ( + f"Vs30 shape {vs30_values.shape} does not match lat/lon dimensions" + ) + + assert not np.isnan(vs30_values).any(), "Vs30 data contains NaNs" + assert vs30_values.min() >= 0, ( + f"Vs30 values below 0 detected: {vs30_values.min()}" + ) + assert vs30_values.max() <= 2000, ( + f"Vs30 values above 2000 detected: {vs30_values.max()}" + ) + + +def test_submodel_data_exists_where_relevant( + nzcvm_root: Path, submodel: Submodel +) -> None: + assert ("data" in submodel) == (submodel["type"] == "vm1d"), ( + "Submodel has data iff submodel is a vm1d" + ) + if "data" in submodel: + data_relative_path = Path(submodel["data"]) + data_path = nzcvm_root / data_relative_path + assert data_path.exists() + + +@dataclass +class SubmodelData: + vp: float + vs: float + rho: float + qp: float + qs: float + thickness: float + + +def parse_submodel_data(submodel_path: Path) -> list[SubmodelData]: + rows = [] + with open(submodel_path, "r") as f: + header = next(f).strip() + assert header == "DEF HST" + for line in f: + row = re.split(r"\s+", line.strip()) + floats = [float(x) for x in row] + assert len(floats) == 6 + rows.append(SubmodelData(*floats)) + return rows + + +def test_submodel_data_is_valid( + subtests: SubTests, nzcvm_root: Path, submodel: Submodel +) -> None: + if "data" in submodel: + data_relative_path = Path(submodel["data"]) + data_path = nzcvm_root / data_relative_path + data = parse_submodel_data(data_path) + vp_min, vp_max = QUALITY_BOUNDS["vp"] + assert all(vp_min <= row.vp <= vp_max for row in data) + vs_min, vs_max = QUALITY_BOUNDS["vs"] + assert all(vs_min <= row.vs <= vs_max for row in data) + assert all(row.thickness >= 0 for row in data) + assert all(row.qp > 0 for row in data) + assert all(row.qs > 0 for row in data) + else: + pytest.skip(f"Submodel {submodel['name']} has no data") From 3f7fa9ab60c0a9ba91ba84576958c07de2fd3dd2 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 12:13:30 +1300 Subject: [PATCH 11/44] fix(registry): correct basement file paths and remove invalid image entry - Update Canterbury basement file paths to use correct lowercase naming - Fix spacing in Wellington basement file path - Remove invalid Gisborne basement image reference from registry --- nzcvm_registry.yaml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index d0bddca..066ba9b 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -56,8 +56,7 @@ basin: submodel: miocene_submod_v1 - path: regional/Canterbury/Canterbury_Paleogene_WGS84.h5 submodel: paleogene_submod_v1 - - path: regional/Canterbury/Canterbury_Basement_WGS84.h5 - + - path: regional/Canterbury/Canterbury_basement_WGS84.h5 - name: Canterbury_v18p2 boundaries: @@ -71,8 +70,7 @@ basin: submodel: miocene_submod_v1 - path: regional/Canterbury/Canterbury_Paleogene_WGS84.h5 submodel: paleogene_submod_v1 - - path: regional/Canterbury/Canterbury_Basement_WGS84.h5 - + - path: regional/Canterbury/Canterbury_basement_WGS84.h5 - name: Canterbury_v18p3 boundaries: @@ -86,8 +84,7 @@ basin: submodel: miocene_submod_v1 - path: regional/Canterbury/Canterbury_Paleogene_WGS84.h5 submodel: paleogene_submod_v1 - - path: regional/Canterbury/Canterbury_Basement_WGS84.h5 - + - path: regional/Canterbury/Canterbury_basement_WGS84.h5 - name: Canterbury_v19p1 type: 4 @@ -337,7 +334,7 @@ basin: surfaces: - path: surface/NZ_DEM_HD.h5 submodel: canterbury1d_v2 - - path : regional/Wellington/Wellington_basement_WGS84_v19p6.h5 + - path: regional/Wellington/Wellington_basement_WGS84_v19p6.h5 smoothing: regional/Wellington/Wellington_smoothing.txt @@ -738,7 +735,6 @@ basin: wiki_images: - images/NI_mideast.png - images/Gisborne_basin_map.png - - images/grisborne_basement.png notes: - (Comment from the author) - Stop at Moto River, working around the East Cape from Gisborne. Haurere Point should be included in Whakatane From e9d794fbb0bbace9f722b060832f3e740371da91 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 12:13:41 +1300 Subject: [PATCH 12/44] style(registry): reformat YAML for readability and consistent indentation - expand elevation arrays to multiline lists for all tomography models - align indentation and list formatting throughout basin and submodel sections - fix inconsistent spacing and minor alignment issues in nested structures - improve overall YAML readability without changing data content --- nzcvm_registry.yaml | 599 +++++++++++++++++++++++++++++++------------- 1 file changed, 429 insertions(+), 170 deletions(-) diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index 066ba9b..62cd8ac 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -1,44 +1,350 @@ +--- tomography: - name: EP2010 - elev: [ 15, 1, -3, -8, -15, -23, -30, -38, -48, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] + elev: + [ + 15, + 1, + -3, + -8, + -15, + -23, + -30, + -38, + -48, + -65, + -85, + -105, + -130, + -155, + -185, + -225, + -275, + -370, + -620, + -750, + ] path: tomography/EP2010/EP2010_New.h5 author: Eberhart-Phillips et al. (2010) title: Establishing a Versatile 3-d seismic Velocity Model for New Zealand url: https://10.1785/gssrl.81.6.992 - name: EP2017 - elev: [ 15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] + elev: + [ + 15, + 1, + -1, + -3, + -5, + -8, + -15, + -23, + -30, + -34, + -38, + -42, + -48, + -55, + -65, + -85, + -105, + -130, + -155, + -185, + -225, + -275, + -370, + -620, + -750, + ] path: tomography/EP2017/EP2017_New.h5 author: Eberhart-Phillips et al. (2017) title: New Zealand Wide model 2.1 seismic velocity and Qs and Qp models for New Zealand url: https://zenodo.org/record/1043558 - + - name: EP2020 - elev: [ 15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] + elev: + [ + 15, + 1, + -1, + -3, + -5, + -8, + -15, + -23, + -30, + -34, + -38, + -42, + -48, + -55, + -65, + -85, + -105, + -130, + -155, + -185, + -225, + -275, + -370, + -620, + -750, + ] path: tomography/EP2020/EP2020_New.h5 author: Eberhart-Phillips et al. (2020) title: New Zealand Wide model 2.2 seismic velocity and Qs and Qp models for New Zealand url: https://zenodo.org/records/3779523 - - name: CHOW2020_EP2020_MIX - elev: [ 15.0, 2.25, 2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25, 0.0, -0.25, -0.5, -0.75, -1.0, -1.25, -1.5, -1.75, -2.0, -2.25, -2.5, -2.75, -3.0, -3.25, -3.5, -3.75, -4.0, -4.25, -4.5, -4.75, -5.0, -5.25, -5.5, -5.75, -6.0, -6.25, -6.5, -6.75, -7.0, -7.25, -7.5, -7.75, -8.0, -9.0, -10.0, -11.0, -12.0, -13.0, -14.0, -15.0, -16.0, -17.0, -18.0, -19.0, -20.0, -21.0, -22.0, -23.0, -24.0, -25.0, -26.0, -27.0, -28.0, -29.0, -30.0, -31.0, -32.0, -33.0, -34.0, -35.0, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0, -45.0, -46.0, -47.0, -48.0, -49.0, -50.0, -52.0, -56.0, -60.0, -64.0, -68.0, -72.0, -76.0, -80.0, -84.0, -88.0, -92.0, -96.0, -100.0, -104.0, -108.0, -112.0, -116.0, -120.0, -124.0, -128.0, -132.0, -136.0, -140.0, -144.0, -148.0, -152.0, -156.0, -160.0, -164.0, -168.0, -172.0, -176.0, -180.0, -184.0, -188.0, -192.0, -196.0, -200.0, -204.0, -208.0, -212.0, -216.0, -220.0, -224.0, -228.0, -232.0, -236.0, -240.0, -244.0, -248.0, -252.0, -256.0, -260.0, -264.0, -268.0, -272.0, -276.0, -280.0, -284.0, -288.0, -292.0, -296.0, -300.0, -304.0, -308.0, -312.0, -316.0, -320.0, -324.0, -328.0, -332.0, -336.0, -340.0, -344.0, -348.0, -352.0, -356.0, -360.0, -364.0, -368.0, -372.0, -376.0, -380.0, -384.0, -388.0, -392.0, -396.0, -400.0, -620.0, -750.0, ] + elev: + [ + 15.0, + 2.25, + 2.0, + 1.75, + 1.5, + 1.25, + 1.0, + 0.75, + 0.5, + 0.25, + 0.0, + -0.25, + -0.5, + -0.75, + -1.0, + -1.25, + -1.5, + -1.75, + -2.0, + -2.25, + -2.5, + -2.75, + -3.0, + -3.25, + -3.5, + -3.75, + -4.0, + -4.25, + -4.5, + -4.75, + -5.0, + -5.25, + -5.5, + -5.75, + -6.0, + -6.25, + -6.5, + -6.75, + -7.0, + -7.25, + -7.5, + -7.75, + -8.0, + -9.0, + -10.0, + -11.0, + -12.0, + -13.0, + -14.0, + -15.0, + -16.0, + -17.0, + -18.0, + -19.0, + -20.0, + -21.0, + -22.0, + -23.0, + -24.0, + -25.0, + -26.0, + -27.0, + -28.0, + -29.0, + -30.0, + -31.0, + -32.0, + -33.0, + -34.0, + -35.0, + -36.0, + -37.0, + -38.0, + -39.0, + -40.0, + -41.0, + -42.0, + -43.0, + -44.0, + -45.0, + -46.0, + -47.0, + -48.0, + -49.0, + -50.0, + -52.0, + -56.0, + -60.0, + -64.0, + -68.0, + -72.0, + -76.0, + -80.0, + -84.0, + -88.0, + -92.0, + -96.0, + -100.0, + -104.0, + -108.0, + -112.0, + -116.0, + -120.0, + -124.0, + -128.0, + -132.0, + -136.0, + -140.0, + -144.0, + -148.0, + -152.0, + -156.0, + -160.0, + -164.0, + -168.0, + -172.0, + -176.0, + -180.0, + -184.0, + -188.0, + -192.0, + -196.0, + -200.0, + -204.0, + -208.0, + -212.0, + -216.0, + -220.0, + -224.0, + -228.0, + -232.0, + -236.0, + -240.0, + -244.0, + -248.0, + -252.0, + -256.0, + -260.0, + -264.0, + -268.0, + -272.0, + -276.0, + -280.0, + -284.0, + -288.0, + -292.0, + -296.0, + -300.0, + -304.0, + -308.0, + -312.0, + -316.0, + -320.0, + -324.0, + -328.0, + -332.0, + -336.0, + -340.0, + -344.0, + -348.0, + -352.0, + -356.0, + -360.0, + -364.0, + -368.0, + -372.0, + -376.0, + -380.0, + -384.0, + -388.0, + -392.0, + -396.0, + -400.0, + -620.0, + -750.0, + ] path: tomography/CHOW2020_EP2020_MIX/chow2020_ep2020_mix.h5 author: Chow et al. (2020) url: - https://doi.org/10.1093/gji/ggaa381 - https://core.geo.vuw.ac.nz/d/feae69f61ea54f81bee1 - - + - name: EP2022 - elev: [15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750] + elev: + [ + 15, + 1, + -1, + -3, + -5, + -8, + -15, + -23, + -30, + -34, + -38, + -42, + -48, + -55, + -65, + -85, + -105, + -130, + -155, + -185, + -225, + -275, + -370, + -620, + -750, + ] path: tomography/EP2022/EP2022_New.h5 author: Eberhart-Phillips et al. (2022) title: New Zealand Wide model 2.3 seismic velocity and Qs and Qp models for New Zealand url: https://zenodo.org/records/5098356 - + - name: EP2025 - elev: [15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750] + elev: + [ + 15, + 1, + -1, + -3, + -5, + -8, + -15, + -23, + -30, + -34, + -38, + -42, + -48, + -55, + -65, + -85, + -105, + -130, + -155, + -185, + -225, + -275, + -370, + -620, + -750, + ] path: tomography/EP2025/EP2025_New.h5 author: Eberhart-Phillips et al. (2025) title: New Zealand Wide model 3.1 seismic velocity and Qs and Qp models for New Zealand @@ -132,7 +438,6 @@ basin: - path: regional/Canterbury/Canterbury_basement_WGS84.h5 smoothing: regional/Canterbury/Canterbury_smoothing.txt - - name: NorthCanterbury_v19p1 type: 1 author: Robin Lee @@ -146,7 +451,6 @@ basin: submodel: canterbury1d_v2 - path: regional/NorthCanterbury/NorthCanterbury_basement_WGS84_v19p1.h5 - - name: NorthCanterbury_v25p8 type: 1 author: Robin Lee / Ayushi Tiwari @@ -175,7 +479,6 @@ basin: submodel: bpv_submod_v4 - path: regional/BanksPeninsulaVolcanics/BanksPeninsulaVolcanics_Miocene_WGS84.h5 - - name: Kaikoura_v19p1 type: 2 author: Robin Lee @@ -191,7 +494,6 @@ basin: - path: regional/Kaikoura/Kaikoura_basement_WGS84.h5 smoothing: regional/Kaikoura/Kaikoura_smoothing.txt - - name: Kaikoura_v25p5 type: 2 author: Robin Lee @@ -206,7 +508,6 @@ basin: submodel: canterbury1d_v2 - path: regional/Kaikoura/Kaikoura_basement_WGS84_v25p5.h5 smoothing: regional/Kaikoura/Kaikoura_smoothing_v25p5.txt - - name: Cheviot_v19p1 type: 1 @@ -222,7 +523,6 @@ basin: - path: regional/Cheviot/Cheviot_basement_WGS84.h5 smoothing: regional/Cheviot/Cheviot_smoothing.txt - - name: Hanmer_v19p1 type: 1 author: Robin Lee @@ -237,7 +537,6 @@ basin: submodel: canterbury1d_v2 - path: regional/Hanmer/Hanmer_basement_WGS84_v19p1.h5 - - name: Hanmer_v25p3 type: 1 author: Ayushi Tiwari @@ -265,7 +564,6 @@ basin: - path: regional/Marlborough/Marlborough_basement_WGS84.h5 smoothing: regional/Marlborough/Marlborough_smoothing.txt - - name: Nelson_v19p1 type: 2 author: Robin Lee @@ -282,7 +580,6 @@ basin: - path: regional/Nelson/Nelson_basement_WGS84_v19p1.h5 smoothing: regional/Nelson/Nelson_smoothing.txt - - name: Nelson_v25p5 type: 3 author: Robin Lee @@ -317,7 +614,6 @@ basin: - path: regional/Nelson/Nelson_basement_WGS84_v25p5.h5 smoothing: regional/Nelson/Nelson_smoothing.txt - - name: Wellington_v19p1 boundaries: - regional/Wellington/Wellington_outline_WGS84.geojson @@ -327,7 +623,6 @@ basin: - path: regional/Wellington/Wellington_basement_WGS84_v19p1.h5 smoothing: regional/Wellington/Wellington_smoothing.txt - - name: Wellington_v19p6 boundaries: - regional/Wellington/Wellington_outline_WGS84.geojson @@ -337,7 +632,6 @@ basin: - path: regional/Wellington/Wellington_basement_WGS84_v19p6.h5 smoothing: regional/Wellington/Wellington_smoothing.txt - - name: Wellington_v21p8 type: 3 author: Robin Lee / Matt Hill @@ -390,7 +684,6 @@ basin: - path: regional/WaikatoHauraki/WaikatoHauraki_basement_WGS84.h5 smoothing: regional/WaikatoHauraki/WaikatoHauraki_smoothing.txt - - name: Wanaka_v20p6 type: 1 author: Cameron Douglas (USER2020) @@ -405,8 +698,6 @@ basin: submodel: canterbury1d_v2 - path: regional/Wanaka/Wanaka_basement_WGS84.h5 - - - name: Mackenzie_v20p6 type: 1 author: Cameron Douglas (USER2020) @@ -421,7 +712,6 @@ basin: submodel: canterbury1d_v2 - path: regional/Mackenzie/Mackenzie_basement_WGS84.h5 - - name: Wakatipu_v20p7 type: 1 author: Tim Tuckey (USER2020) @@ -435,7 +725,6 @@ basin: submodel: canterbury1d_v2 - path: regional/Wakatipu/Wakatipu_basement_WGS84.h5 - - name: Alexandra_v20p7 type: 1 author: Cameron Douglas (USER2020) @@ -452,7 +741,6 @@ basin: submodel: canterbury1d_v2 - path: regional/Alexandra/Alexandra_basement_WGS84.h5 - - name: Ranfurly_v20p7 type: 1 author: Cameron Douglas (USER2020) @@ -469,7 +757,6 @@ basin: submodel: canterbury1d_v2 - path: regional/Ranfurly/Ranfurly_basement_WGS84.h5 - - name: NE_Otago_v20p7 type: 1 author: Cameron Douglas (USER2020) @@ -490,7 +777,6 @@ basin: submodel: canterbury1d_v2 - path: regional/NE_Otago/NE_Otago_basement_WGS84.h5 - - name: Mosgiel_v20p7 type: 1 author: Cameron Douglas (USER2020) @@ -511,8 +797,6 @@ basin: submodel: canterbury1d_v2 - path: regional/Mosgiel/Mosgiel_basement_WGS84.h5 - - - name: Balclutha_v20p7 type: 1 author: Cameron Douglas (USER2020) @@ -551,8 +835,6 @@ basin: - path: regional/Dunedin/Dunedin_basement_WGS84.h5 smoothing: regional/Dunedin/Dunedin_smoothing.txt - - - name: Murchison_v20p7 type: 1 author: Tim Tuckey (USER2020) @@ -567,8 +849,6 @@ basin: submodel: canterbury1d_v2 - path: regional/Murchison/Murchison_basement_WGS84.h5 - - - name: Waitaki_v20p8 type: 1 author: Cameron Douglas (USER2020) @@ -588,7 +868,6 @@ basin: - path: regional/Waitaki/Waitaki_basement_WGS84.h5 smoothing: regional/Waitaki/Waitaki_smoothing.txt - - name: Hakataramea_v20p8 type: 1 author: Cameron Douglas (USER2020) @@ -605,7 +884,6 @@ basin: submodel: canterbury1d_v2 - path: regional/Hakataramea/Hakataramea_basement_WGS84.h5 - - name: Karamea_v20p11 type: 1 author: Tim Tuckey (USER2020) @@ -620,7 +898,6 @@ basin: - path: regional/Karamea/Karamea_basement_WGS84.h5 smoothing: regional/Karamea/Karamea_smoothing.txt - - name: Collingwood_v20p11 type: 1 author: Tim Tuckey (USER2020) @@ -637,7 +914,6 @@ basin: - path: regional/Collingwood/Collingwood_basement_WGS84.h5 smoothing: regional/Collingwood/Collingwood_smoothing.txt - - name: SpringsJunction_v20p11 type: 1 author: Tim Tuckey (USER2020) @@ -651,7 +927,6 @@ basin: submodel: canterbury1d_v2 - path: regional/SpringsJunction/SpringsJunction_basement_WGS84.h5 - - name: HawkesBay_v21p7 type: 1 author: William Lee (USER2021) @@ -746,7 +1021,6 @@ basin: - path: regional/Gisborne/Gisborne_basement_WGS84.h5 smoothing: regional/Gisborne/Gisborne_smoothing.txt - - name: SouthernHawkesBay_v21p12 type: 1 author: William Lee (USER2021) @@ -767,7 +1041,6 @@ basin: submodel: canterbury1d_v2 - path: regional/SouthernHawkesBay/SouthernHawkesBay_basement_WGS84.h5 - - name: Wairarapa_v21p12 type: 1 author: William Lee (USER2021) @@ -775,7 +1048,7 @@ basin: - images/NI_south.png - images/Wairarapa_basin_map.png notes: - - (Comment from the author) "Consider adding east coastal basins (e.g. Uruti Point)" + - (Comment from the author) "Consider adding east coastal basins (e.g. Uruti Point)" boundaries: - regional/Wairarapa/Wairarapa_outline_WGS84.geojson surfaces: @@ -784,7 +1057,6 @@ basin: - path: regional/Wairarapa/Wairarapa_basement_WGS84.h5 smoothing: regional/Wairarapa/Wairarapa_smoothing.txt - - name: OmaioBay_v22p3 type: 1 author: Cameron Davis / Emma Coumbe (USER2022) @@ -808,7 +1080,6 @@ basin: - path: regional/OmaioBay/OmaioBay_basement_WGS84_v22p3.h5 smoothing: regional/OmaioBay/OmaioBay_smoothing_v22p3.txt - - name: OmaioBay_v25p5 type: 1 author: Cameron Davis / Emma Coumbe (USER2022) @@ -831,8 +1102,6 @@ basin: - path: regional/OmaioBay/OmaioBay_basement_WGS84.h5 smoothing: regional/OmaioBay/OmaioBay_smoothing.txt - - - name: Whangaparoa_v23p4 type: 1 author: Cameron Davis / Emma Coumbe (USER2022) @@ -848,8 +1117,7 @@ basin: - path: surface/NZ_DEM_HD.h5 submodel: canterbury1d_v2 - path: regional/Whangaparoa/Whangaparoa_basement_WGS84.h5 - smoothing: regional/Whangaparoa/Whangaparoa_smoothing.txt - + smoothing: regional/Whangaparoa/Whangaparoa_smoothing.txt - name: PalmerstonNorth_v25p5 type: 1 @@ -857,11 +1125,11 @@ basin: wiki_images: - images/PalmerstonNorth_basin_map_v25p5.png boundaries: - - regional/PalmerstonNorth/PalmerstonNorth_outline_WGS84.geojson + - regional/PalmerstonNorth/PalmerstonNorth_outline_WGS84.geojson surfaces: - - path: surface/NZ_DEM_HD.h5 - submodel: canterbury1d_v2 - - path: regional/PalmerstonNorth/PalmerstonNorth_basement_WGS84_v25p5.h5 + - path: surface/NZ_DEM_HD.h5 + submodel: canterbury1d_v2 + - path: regional/PalmerstonNorth/PalmerstonNorth_basement_WGS84_v25p5.h5 smoothing: regional/PalmerstonNorth/PalmerstonNorth_smoothing.txt - name: PalmerstonNorth_v25p8 @@ -875,11 +1143,10 @@ basin: - path: surface/NZ_DEM_HD.h5 submodel: palmerstonnorth_v1 - path: regional/PalmerstonNorth/PalmerstonNorth_pliocenetop_WGS84_v25p8.h5 - submodel: pn_pliocene_submod_v1 + submodel: pn_pliocene_submod_v1 - path: regional/PalmerstonNorth/PalmerstonNorth_basement_WGS84_v25p8.h5 smoothing: regional/PalmerstonNorth/PalmerstonNorth_smoothing.txt - - name: Southland_v25p5 type: 2 author: Archie Goodrick @@ -908,7 +1175,6 @@ basin: submodel: canterbury1d_v2 - path: regional/TeAnau/TeAnau_basement_WGS84.h5 - - name: TolagaBay_v25p5 type: 1 author: Cameron Davis / Emma Coumbe (USER2022) @@ -918,7 +1184,7 @@ basin: boundaries: - regional/TolagaBay/TolagaBay_outline_WGS84.geojson - + surfaces: - path: surface/NZ_DEM_HD.h5 submodel: canterbury1d_v2 @@ -939,7 +1205,6 @@ basin: - path: regional/Waiapu/Waiapu_basement_WGS84.h5 smoothing: regional/Waiapu/Waiapu_smoothing.txt - - name: WestCoast_v25p5 type: 1 author: Ayushi Tiwari / Hunter Brotherston @@ -948,12 +1213,11 @@ basin: boundaries: - regional/WestCoast/WestCoast_outline_WGS84.geojson surfaces: - - path: surface/NZ_DEM_HD.h5 - submodel: canterbury1d_v2 - - path: regional/WestCoast/WestCoast_basement_WGS84.h5 + - path: surface/NZ_DEM_HD.h5 + submodel: canterbury1d_v2 + - path: regional/WestCoast/WestCoast_basement_WGS84.h5 smoothing: regional/WestCoast/WestCoast_smoothing.txt - - name: Westport_v25p5 type: 1 author: Ayushi Tiwari / Kaleb Finn (ENCN493) @@ -980,155 +1244,150 @@ basin: - path: regional/Westport/Westport_basement_WGS84.h5 smoothing: regional/Westport/Westport_smoothing.txt - - name: QueenCharlotte_v25p8 type: 1 author: Ayushi Tiwari wiki_images: - - images/QueenCharlotte_basin_map.png + - images/QueenCharlotte_basin_map.png boundaries: - - regional/QueenCharlotte/QueenCharlotte_outline_WGS84.geojson + - regional/QueenCharlotte/QueenCharlotte_outline_WGS84.geojson surfaces: - - path: surface/NZ_DEM_HD.h5 - submodel: canterbury1d_v2 - - path: regional/QueenCharlotte/QueenCharlotte_basement_WGS84_v25p8.h5 + - path: surface/NZ_DEM_HD.h5 + submodel: canterbury1d_v2 + - path: regional/QueenCharlotte/QueenCharlotte_basement_WGS84_v25p8.h5 - name: CastleHill_v25p8 type: 1 author: Ayushi Tiwari wiki_images: - - images/CastleHill_basin_map.png + - images/CastleHill_basin_map.png boundaries: - - regional/CastleHill/CastleHill_outline_WGS84.geojson + - regional/CastleHill/CastleHill_outline_WGS84.geojson surfaces: - - path: surface/NZ_DEM_HD.h5 - submodel: canterbury1d_v2 - - path: regional/CastleHill/CastleHill_basement_WGS84.h5 - + - path: surface/NZ_DEM_HD.h5 + submodel: canterbury1d_v2 + - path: regional/CastleHill/CastleHill_basement_WGS84.h5 - name: Whakatane_v25p8 type: 1 author: Ayushi Tiwari wiki_images: - - images/Whakatane_basin_map.png + - images/Whakatane_basin_map.png boundaries: - - regional/Whakatane/Whakatane_outline_WGS84.geojson + - regional/Whakatane/Whakatane_outline_WGS84.geojson surfaces: - - path: surface/NZ_DEM_HD.h5 - submodel: canterbury1d_v2 - - path: regional/Whakatane/Whakatane_basement_WGS84_v25p8.h5 + - path: surface/NZ_DEM_HD.h5 + submodel: canterbury1d_v2 + - path: regional/Whakatane/Whakatane_basement_WGS84_v25p8.h5 smoothing: regional/Whakatane/Whakatane_smoothing.txt - name: TeAraroa_v25p9 type: 1 author: James Hoskin wiki_images: - - images/TeAraroa_basin_map.png + - images/TeAraroa_basin_map.png boundaries: - - regional/TeAraroa/TeAraroa_outline_WGS84.geojson + - regional/TeAraroa/TeAraroa_outline_WGS84.geojson surfaces: - - path : surface/NZ_DEM_HD.h5 + - path: surface/NZ_DEM_HD.h5 submodel: canterbury1d_v2 - - path : regional/TeAraroa/TeAraroa_basement_WGS84.h5 + - path: regional/TeAraroa/TeAraroa_basement_WGS84.h5 smoothing: regional/TeAraroa/TeAraroa_smoothing.txt - name: Rarakau_v25p9 type: 1 author: Toby Bates wiki_images: - - images/Rarakau_basin_map.png + - images/Rarakau_basin_map.png boundaries: - - regional/Rarakau/Rarakau_outline_WGS84.geojson + - regional/Rarakau/Rarakau_outline_WGS84.geojson surfaces: - - path : surface/NZ_DEM_HD.h5 - submodel: canterbury1d_v2 - - path : regional/Rarakau/Rarakau_basement_WGS84.h5 + - path: surface/NZ_DEM_HD.h5 + submodel: canterbury1d_v2 + - path: regional/Rarakau/Rarakau_basement_WGS84.h5 smoothing: regional/Rarakau/Rarakau_smoothing.txt - vs30: - name: nz_with_offshore path: vs30/NZ_Vs30_HD_With_Offshore.h5 - name: nz_without_offshore path: vs30/NZ_Vs30.h5 - submodel: - - name: nan_submod - type: null - - - name: canterbury1d_v1 - type: vm1d - module: canterbury1d_submod - data: vm1d/Cant1D_v1.fd_modfile - - - name: canterbury1d_v2 - type: vm1d - module: canterbury1d_submod - data: vm1d/Cant1D_v2.fd_modfile - - - name: canterbury1d_v2_pliocene_enforced - type: vm1d - module: canterbury1d_submod - data: vm1d/Cant1D_v2_Pliocene_Enforced.fd_modfile - - - name: canterbury1d_v3_pliocene_enforced - type: vm1d - module: canterbury1d_submod - data: vm1d/Cant1D_v3_Pliocene_Enforced.fd_modfile - - - name: nelson_v1 - type: vm1d - module: canterbury1d_submod - data: vm1d/Nelson_v1.fd_modfile - - - name: palmerstonnorth_v1 - type: vm1d - module: canterbury1d_submod - data: vm1d/PalmerstonNorth_v1.fd_modfile - - - name: ep_tomography_submod_v2010 - type: tomography - module: ep_tomography_submod_v2010 - - - name: bpv_submod_v4 - type: relation - module: bpv_submod_v4 - - - name: pliocene_submod_v1 - type: relation - module: pliocene_submod_v1 - - - name: pn_pliocene_submod_v1 - type: relation - module: pn_pliocene_submod_v1 - - - - name: miocene_submod_v1 - type: relation - module: miocene_submod_v1 - - - name: paleogene_submod_v1 - type: relation - module: paleogene_submod_v1 - - - name: pliocene_submod_v2 - type: relation - module: pliocene_submod_v2 - - - name: miocene_submod_v2 - type: relation - module: miocene_submod_v2 - - - name: paleogene_submod_v2 - type: relation - module: paleogene_submod_v2 - - - name: perturbation_v20p6 - type: perturbation - - - name: perturbation_v20p10 - type: perturbation - - - name: perturbation_v20p11 - type: perturbation + - name: nan_submod + type: null + + - name: canterbury1d_v1 + type: vm1d + module: canterbury1d_submod + data: vm1d/Cant1D_v1.fd_modfile + + - name: canterbury1d_v2 + type: vm1d + module: canterbury1d_submod + data: vm1d/Cant1D_v2.fd_modfile + + - name: canterbury1d_v2_pliocene_enforced + type: vm1d + module: canterbury1d_submod + data: vm1d/Cant1D_v2_Pliocene_Enforced.fd_modfile + + - name: canterbury1d_v3_pliocene_enforced + type: vm1d + module: canterbury1d_submod + data: vm1d/Cant1D_v3_Pliocene_Enforced.fd_modfile + + - name: nelson_v1 + type: vm1d + module: canterbury1d_submod + data: vm1d/Nelson_v1.fd_modfile + + - name: palmerstonnorth_v1 + type: vm1d + module: canterbury1d_submod + data: vm1d/PalmerstonNorth_v1.fd_modfile + + - name: ep_tomography_submod_v2010 + type: tomography + module: ep_tomography_submod_v2010 + + - name: bpv_submod_v4 + type: relation + module: bpv_submod_v4 + + - name: pliocene_submod_v1 + type: relation + module: pliocene_submod_v1 + + - name: pn_pliocene_submod_v1 + type: relation + module: pn_pliocene_submod_v1 + + - name: miocene_submod_v1 + type: relation + module: miocene_submod_v1 + + - name: paleogene_submod_v1 + type: relation + module: paleogene_submod_v1 + + - name: pliocene_submod_v2 + type: relation + module: pliocene_submod_v2 + + - name: miocene_submod_v2 + type: relation + module: miocene_submod_v2 + + - name: paleogene_submod_v2 + type: relation + module: paleogene_submod_v2 + + - name: perturbation_v20p6 + type: perturbation + + - name: perturbation_v20p10 + type: perturbation + + - name: perturbation_v20p11 + type: perturbation From 7f2e3810d49ac92b3b9a702a805d66c82324667e Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 12:17:30 +1300 Subject: [PATCH 13/44] test: refactor registry tests for clarity and remove unused code - Remove duplicate and unused imports in test_registry.py - Decorate test_nzcvm_registry_schema with @no_type_check for type checking bypass - Eliminate redundant and commented-out test code for basin smoothing boundaries - Remove unused variables and streamline vs30 tests for readability --- tests/test_registry.py | 47 ++++-------------------------------------- 1 file changed, 4 insertions(+), 43 deletions(-) diff --git a/tests/test_registry.py b/tests/test_registry.py index fc8c43e..4d6942a 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -1,8 +1,7 @@ import itertools import re -from json import JSONDecodeError from pathlib import Path -from typing import NotRequired, TypedDict +from typing import NotRequired, TypedDict, no_type_check import h5py import numpy as np @@ -25,6 +24,7 @@ def nzcvm_root() -> Path: return Path(__file__).parent.parent +@no_type_check def test_nzcvm_registry_schema(nzcvm_registry_path: Path) -> None: path = Regex( # See https://stackoverflow.com/a/537876 @@ -84,7 +84,6 @@ def test_nzcvm_registry_schema(nzcvm_registry_path: Path) -> None: { "tomography": [tomography_entry], "basin": [basin_entry], - "basin": [basin_entry], "submodel": [submodel_schema], "vs30": [vs30_schema], } @@ -413,43 +412,6 @@ def read_smoothing_boundary(smoothing_path: Path) -> shapely.LineString: return shapely.LineString(coords) -from pathlib import Path - -import shapely - - -def test_basin_smoothing_contained_in_boundaries( - subtests: SubTests, nzcvm_root: Path, basin: Basin, tmp_path: Path -) -> None: - if "smoothing" not in basin: - pytest.skip("basin has no smoothing boundary") - - # 1. Load Basin Boundaries - boundaries = [] - for boundary in basin["boundaries"]: - boundary_path = nzcvm_root / Path(boundary) - geojson_str = boundary_path.read_text() - geom_collection = shapely.from_geojson(geojson_str) - boundaries.append(geom_collection) - - boundary_geometry = shapely.union_all(boundaries) - - # 2. Load Smoothing Boundary - smoothing_surface_path = nzcvm_root / Path(basin["smoothing"]) - smoothing_boundary = read_smoothing_boundary(smoothing_surface_path) - - # 3. Perform Check - is_contained = boundary_geometry.contains(smoothing_boundary) - - # 4. Handle Failure and Save Artifacts - if not is_contained: - # Calculate intersection for debugging - intersection = shapely.intersection(boundary_geometry, smoothing_boundary) - - # Use a slug for the filename (assuming basin has a name or ID) - basin_name = basin.get("name", "unknown_basin").replace(" ", "_") - - def test_basin_smoothing_contained_in_boundaries( subtests: SubTests, nzcvm_root: Path, basin: Basin ) -> None: @@ -510,7 +472,6 @@ def test_basin_surfaces_contain_boundaries( def test_vs30_file_exists(nzcvm_root: Path, vs30: Vs30) -> None: relative_path = Path(vs30["path"]) - name = vs30["name"] vs30_path = nzcvm_root / relative_path assert vs30_path.exists(), f"Vs30 file missing at {vs30_path}" @@ -518,7 +479,7 @@ def test_vs30_file_exists(nzcvm_root: Path, vs30: Vs30) -> None: def test_vs30_is_valid_hdf5(nzcvm_root: Path, vs30: Vs30) -> None: relative_path = Path(vs30["path"]) - name = vs30["name"] + vs30_path = nzcvm_root / relative_path try: @@ -529,12 +490,12 @@ def test_vs30_is_valid_hdf5(nzcvm_root: Path, vs30: Vs30) -> None: assert "latitude" in f.keys() assert "longitude" in f.keys() except Exception as e: + name = vs30["name"] pytest.fail(f"Vs30 file {name} is not a valid hdf5 file: {e}") def test_vs30_geo_gridpoints(nzcvm_root: Path, vs30: Vs30) -> None: relative_path = Path(vs30["path"]) - name = vs30["name"] vs30_path = nzcvm_root / relative_path with h5py.File(vs30_path, "r") as f: From 9fc2369f7cd8d7881420d0df2b8d6835096918ef Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 12:17:48 +1300 Subject: [PATCH 14/44] deps: add shapely dependency --- pyproject.toml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 101a593..223a1da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,8 +7,14 @@ dependencies = ["numpy>=2.3.5", "pytest-subtests>=0.15.0", "schema>=0.7.8"] [project.optional-dependencies] -test = ["h5py>=3.15.1", "pytest>=9.0.2", "pyyaml>=6.0.3"] +test = [ + "h5py>=3.15.1", + "pytest>=9.0.2", + "pyyaml>=6.0.3", + "geojson>=3.2.0", + "shapely>=2.1.2", +] # Even though this isn't a python package, these dev dependencies can # still prove useful for writing tests. dev = ["ruff", "numpydoc", "ty"] -types = [] +types = ["types-shapely>=2.1.0.20250917"] From 38b950945e105815b79434c153fbbee6fa66627e Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 12:18:30 +1300 Subject: [PATCH 15/44] ci: add all extras to ty check --- .github/workflows/types.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/types.yml b/.github/workflows/types.yml index 522d468..26351b2 100644 --- a/.github/workflows/types.yml +++ b/.github/workflows/types.yml @@ -19,4 +19,4 @@ jobs: enable-cache: true - name: Run type checking with ty - run: uv run --extra dev --extra types ty check + run: uv run --all-extras ty check From d2eca3c3e45552603355452b603d50ed06c39ae3 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 12:18:39 +1300 Subject: [PATCH 16/44] ci: add yamllint config --- .yamllint | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .yamllint diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..c5b0b01 --- /dev/null +++ b/.yamllint @@ -0,0 +1,3 @@ +rules: + line-length: + max: 200 From cff888454dbcbeb5034b3aedec4e45c45ba2e69f Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 12:22:11 +1300 Subject: [PATCH 17/44] ci: fix pytest action --- .github/workflows/pytest.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index a5723e9..5b48364 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -1,15 +1,18 @@ name: Pytest Check on: [pull_request] + jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 + - name: Setup UV uses: astral-sh/setup-uv@v5 with: enable-cache: true - cache-dependency-glob: | - **/pyproject.toml + cache-dependency-glob: "**/pyproject.toml" + + - name: Run Tests run: uv run --extra test pytest tests From d1a69463e70ec8effc4ee5a391ce6537eca23e1d Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 12:25:05 +1300 Subject: [PATCH 18/44] ci: use cached LFS checkout --- .github/workflows/pytest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 5b48364..32e365f 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: nschloe/action-cached-lfs-checkout@v1 - name: Setup UV uses: astral-sh/setup-uv@v5 From e02c4f57d537c118513a9c20028c96c72f4ed962 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Thu, 18 Dec 2025 12:45:44 +1300 Subject: [PATCH 19/44] update paths --- nzcvm_registry.yaml | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index d0bddca..aef7228 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -1,47 +1,38 @@ tomography: - name: EP2010 elev: [ 15, 1, -3, -8, -15, -23, -30, -38, -48, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] - path: tomography/EP2010/EP2010_New.h5 + path: tomography/EP2010/EP2010.h5 author: Eberhart-Phillips et al. (2010) title: Establishing a Versatile 3-d seismic Velocity Model for New Zealand url: https://10.1785/gssrl.81.6.992 - name: EP2017 elev: [ 15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] - path: tomography/EP2017/EP2017_New.h5 + path: tomography/EP2017/EP2017.h5 author: Eberhart-Phillips et al. (2017) title: New Zealand Wide model 2.1 seismic velocity and Qs and Qp models for New Zealand url: https://zenodo.org/record/1043558 - name: EP2020 elev: [ 15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] - path: tomography/EP2020/EP2020_New.h5 + path: tomography/EP2020/EP2020.h5 author: Eberhart-Phillips et al. (2020) title: New Zealand Wide model 2.2 seismic velocity and Qs and Qp models for New Zealand url: https://zenodo.org/records/3779523 - - - - name: CHOW2020_EP2020_MIX - elev: [ 15.0, 2.25, 2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25, 0.0, -0.25, -0.5, -0.75, -1.0, -1.25, -1.5, -1.75, -2.0, -2.25, -2.5, -2.75, -3.0, -3.25, -3.5, -3.75, -4.0, -4.25, -4.5, -4.75, -5.0, -5.25, -5.5, -5.75, -6.0, -6.25, -6.5, -6.75, -7.0, -7.25, -7.5, -7.75, -8.0, -9.0, -10.0, -11.0, -12.0, -13.0, -14.0, -15.0, -16.0, -17.0, -18.0, -19.0, -20.0, -21.0, -22.0, -23.0, -24.0, -25.0, -26.0, -27.0, -28.0, -29.0, -30.0, -31.0, -32.0, -33.0, -34.0, -35.0, -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0, -45.0, -46.0, -47.0, -48.0, -49.0, -50.0, -52.0, -56.0, -60.0, -64.0, -68.0, -72.0, -76.0, -80.0, -84.0, -88.0, -92.0, -96.0, -100.0, -104.0, -108.0, -112.0, -116.0, -120.0, -124.0, -128.0, -132.0, -136.0, -140.0, -144.0, -148.0, -152.0, -156.0, -160.0, -164.0, -168.0, -172.0, -176.0, -180.0, -184.0, -188.0, -192.0, -196.0, -200.0, -204.0, -208.0, -212.0, -216.0, -220.0, -224.0, -228.0, -232.0, -236.0, -240.0, -244.0, -248.0, -252.0, -256.0, -260.0, -264.0, -268.0, -272.0, -276.0, -280.0, -284.0, -288.0, -292.0, -296.0, -300.0, -304.0, -308.0, -312.0, -316.0, -320.0, -324.0, -328.0, -332.0, -336.0, -340.0, -344.0, -348.0, -352.0, -356.0, -360.0, -364.0, -368.0, -372.0, -376.0, -380.0, -384.0, -388.0, -392.0, -396.0, -400.0, -620.0, -750.0, ] - path: tomography/CHOW2020_EP2020_MIX/chow2020_ep2020_mix.h5 - author: Chow et al. (2020) - url: - - https://doi.org/10.1093/gji/ggaa381 - - https://core.geo.vuw.ac.nz/d/feae69f61ea54f81bee1 - - name: EP2022 elev: [15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750] - path: tomography/EP2022/EP2022_New.h5 + path: tomography/EP2022/EP2022.h5 author: Eberhart-Phillips et al. (2022) title: New Zealand Wide model 2.3 seismic velocity and Qs and Qp models for New Zealand url: https://zenodo.org/records/5098356 - name: EP2025 elev: [15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750] - path: tomography/EP2025/EP2025_New.h5 + path: tomography/EP2025/EP2025.h5 author: Eberhart-Phillips et al. (2025) title: New Zealand Wide model 3.1 seismic velocity and Qs and Qp models for New Zealand + url: Personal communication (pending publication) basin: - name: Canterbury_v18p1 From 600d4610aab748d52300f164724bd4d9aadd716f Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Thu, 18 Dec 2025 12:49:51 +1300 Subject: [PATCH 20/44] remove old tomography files --- .../CHOW2020_EP2020_MIX/chow2020_ep2020_mix.h5 | 3 --- tomography/EP2010/EP2010_New.h5 | 3 --- tomography/EP2010/ep2010.h5 | 4 ++-- tomography/EP2017/EP2017_New.h5 | 3 --- tomography/EP2020/EP2020_New.h5 | 3 --- tomography/EP2020/ep2020.h5 | 4 ++-- ...ep2020_interpolated_spatial_distribution.png | Bin 74450 -> 0 bytes .../ep2020_original_spatial_distribution.png | Bin 66503 -> 0 bytes tomography/EP2022/EP2022_New.h5 | 3 --- tomography/EP2025/EP2025_New.h5 | 3 --- tomography/EP2025/ep2025.h5 | 4 ++-- 11 files changed, 6 insertions(+), 24 deletions(-) delete mode 100644 tomography/CHOW2020_EP2020_MIX/chow2020_ep2020_mix.h5 delete mode 100644 tomography/EP2010/EP2010_New.h5 delete mode 100644 tomography/EP2017/EP2017_New.h5 delete mode 100644 tomography/EP2020/EP2020_New.h5 delete mode 100644 tomography/EP2020/images/ep2020_interpolated_spatial_distribution.png delete mode 100644 tomography/EP2020/images/ep2020_original_spatial_distribution.png delete mode 100644 tomography/EP2022/EP2022_New.h5 delete mode 100644 tomography/EP2025/EP2025_New.h5 diff --git a/tomography/CHOW2020_EP2020_MIX/chow2020_ep2020_mix.h5 b/tomography/CHOW2020_EP2020_MIX/chow2020_ep2020_mix.h5 deleted file mode 100644 index a82f872..0000000 --- a/tomography/CHOW2020_EP2020_MIX/chow2020_ep2020_mix.h5 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc5600a90df9806662a3afa2f6e109b93de0ac76df5dc08512bc20618a0eb51b -size 734537257 diff --git a/tomography/EP2010/EP2010_New.h5 b/tomography/EP2010/EP2010_New.h5 deleted file mode 100644 index 9e23a5c..0000000 --- a/tomography/EP2010/EP2010_New.h5 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:37d1eac30bbdedeb7a75985d701cd43e2cdc4a5f530140d69c6d3556ee44e7d2 -size 221080274 diff --git a/tomography/EP2010/ep2010.h5 b/tomography/EP2010/ep2010.h5 index 4a2b450..9e23a5c 100644 --- a/tomography/EP2010/ep2010.h5 +++ b/tomography/EP2010/ep2010.h5 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f83d94050e192d7516a9be4cb177303053748f6fe0195147001c128712dc440b -size 69006576 +oid sha256:37d1eac30bbdedeb7a75985d701cd43e2cdc4a5f530140d69c6d3556ee44e7d2 +size 221080274 diff --git a/tomography/EP2017/EP2017_New.h5 b/tomography/EP2017/EP2017_New.h5 deleted file mode 100644 index 528fd2d..0000000 --- a/tomography/EP2017/EP2017_New.h5 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8c0b8a10cac4c6a799109e3560e5fa16bade54b839c6e5899949f64ac757dfdd -size 304064657 diff --git a/tomography/EP2020/EP2020_New.h5 b/tomography/EP2020/EP2020_New.h5 deleted file mode 100644 index 9643709..0000000 --- a/tomography/EP2020/EP2020_New.h5 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:56d28ab8c13a295573b2086eee24501f2e0be5aa1c8b6d1f5698091865f27ca9 -size 306468708 diff --git a/tomography/EP2020/ep2020.h5 b/tomography/EP2020/ep2020.h5 index 901da47..9643709 100644 --- a/tomography/EP2020/ep2020.h5 +++ b/tomography/EP2020/ep2020.h5 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62f6fe774a467d5e38bb09d88ed8e42afd86070f32d98d748d523bf660705c7a -size 110617994 +oid sha256:56d28ab8c13a295573b2086eee24501f2e0be5aa1c8b6d1f5698091865f27ca9 +size 306468708 diff --git a/tomography/EP2020/images/ep2020_interpolated_spatial_distribution.png b/tomography/EP2020/images/ep2020_interpolated_spatial_distribution.png deleted file mode 100644 index 88129270090837443295d5d3c35554e78c5754cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74450 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

QXiG8Y z78h{$0SA2o5X^TUzI7w24YE!o4i$iSMsD{@1J_3uRBFvc5(j`%%z*a#Zb(!J$NWq9 zQ|%_nZ@<4dp)~ZvwEQqSAi3bgDM(vF4p-`jVIR6ylkYd>w@4~n8`Z$ZPqPy1URk6J z#rZ+qPPQ|krvfNGbtfs>PtG#^F(ufP#c#RC`vGM#uWL_Y2)Ur%HvzID>sDdcdy=#} zshD`oJVSshRfcMCr3sTym_aTs02|!=L~#16IC0P?=OYFkl?9qj*^>Uzf4408wdu*j z+EJ`1P8O4_K#65s%S5n)HU--$Vz54Ho|g4M=H))EC=orfeUI_4@G#{hk|TdeXqK<} z{7B6j&Hy&m=#eL0AlgBwBd#GQ3dJD7?ypOZF+A-pr1c>|8(?BEo+Q&{%biE+k`xcr zIu3CT`d~KT@Ijv2BvY?`zF~{!K40| zXtQMOKUYa3uJ^FuNPt6)I^LKldNSfNvn~YV%I6eh{oL3{+T|H|>;%yz(kZK#ryo4F zXp;Q;&kKOO@~~+Ee~$_Tjejw0h9yrw|MD4pZ+B9vS)NHRC88Cnu`_kds6~=Qt99(q z9}rp|;UqO$$ak};#GKH~{${ZzwQIU&q{yKSf6JbPHnR7nx1&IilLgS>bmWCdvBPL> zdJ|=5)pItOjEZ<)eKSdKUSy!xQ*MzmuLr+G2IJZDP1?7Y5$M|c@;lfgz=O`GYp@;~ z>kB}{k%zDfcBS@Oc&*=pb4ZBg;+7QH+{gf-0leBNWD9i9e>G@wzn~!&% z(86!@+hARWY3coN&+mt{;I>aqnXCju7p&KhouIC{osE$Njh(;Ibj=kov2o}`!0!gB zD;^rg3^onjy_x|2?{Vz>PA-%$f}z{s4hbzOy-H z>A4&-zpC>Gp$RY8dD}nwoeo+vI7~wwTQWf0U8yER@9)|JhsT3XfhIkYEZ=7b1CU3y z$woEPc-`dldh?Io3p=Q69)#uxh;Ee%QW;VYB0p%awO8hg_7z${?H(NBhlJ0vZU%pd zI%$f(C`fsGR4o+5A7d(nxGA6A z)go_>%N=Cb2hkabMZ@o3W_8GQGLH4MI7=L#>H8GC=^P4Yk@JYt^uuRgmuWW5!<69w z?`k*Qda%+w`)n+FP8MTsxTertl5N=l_j!eU94TAOTpDPq0*Qlknf%xPeg5JG72A|&VMoIL4AD4S> zoG`8NfaI5N`1aHxBg2AaiC;l{)OFw7iELCfsY!}koE&j)^XvuNdG|@t3hvuEI3o-t zIEcmlw>RZmPm6eY)4ojl67r9;?WSoZ0n?!@VP{GbaP-FKB*Q?uMf!}8yH=O_-=Qu} z&61~`lGC9sGZ!@NB?hguION2p3yGI?!s;++d{e;CpH6tIT|)oE*B_b{Pbpsrg>^*LjJ=9(+h=+1j*LtF!n3~f;k_H*yHd;kz# zh#C`mE?@aYd4_uXp;hr*FeRCVoBnAjpdNG6ZY z=g4#-+>IZXrAVsZAm326-4Yp)BPbS`x@~qXboYK_1uSo8X&dLuDXf0xn-Zi24Ao16 zv2{ou6iqR96T^d)J$5fpyx*-Xxa4=dPdEX{HwDvUr!Bp{x;*G3nP(Wid(TcPmiF6l z#zpd&_pv{AF2d}do2JtY40v$~f8cOxV^tW>xpQ{DpXJ+C*)y~y^uqLe*B~hZ%;+m1 z|C8x`q+~UGR*G3TeL~MdDzEibFM;4$6lj`&a1b^{(nqgb_Ux?Jh&WaX zrhnJ6NbnF(y5s~(f~;lc4eRSelKT11#$h+HLS2K!#kjiuVFT@Bao3Y;M`8>0+i-%` zWR-%{YYLNqq?p zl)ST(kOw?4>X93@PLqaJE`%cMcA;3`Y!@lRp2RkZxJqhc$g#x2+#79yC0FE8KOc7M zpKlQ8f`{HCc5rw~yl(kYP^9K*MikLewN18cq23M`tY{&_ZCWZwPMyfCW~H_Fj=0avOKFx<%vbaX;*{H z;(MPOV26XTsd_zO8~ioWWuEB_u!s$FLFrY2x>?zF@^79A3meeD5FB7(1ylg_I3jN# zRO8RQnaNPQ<5~BW4SRKNpALKIp6=Q9y>8%Tswdguz?=zLalN5fbPUZ}#LUQ6xZ>9t z*`gG?GQZ^f) z>e*A*ssYA=+DmWL9b32z5hTj;8QiEwNIqUzv{WZY2qRJjS!uj#MaHDJ3Qv!OI&{Ve zIW;^9QNxfc>|NfasuY}lgD<2SaZ*1&B!4fNunPyGLk<029`up(u$~M82jLH2^%jia zqs-d2XKwby5t*0WyiSpH9`_9usKk_Tvm-2+@5$;d)5u*p+b>$SK`Gkq0X@agU=lKD zn4mhRTusuIVHRQ2$`jun#1{oIyS3?zq((TjF5%0d!s-+BH;DK6^Nx2lEPO$kn}-;F z3F2Nf<;4ZFZ_=GGY+69K;5KLleh06FVC?%%-T@swT|YCdAJ^28_dfujJ@z|L%(-bi zYIzU-c;`_;BxGE;v+=!Q6fv*E*@bWVKd0BAeGXG+H$ZdBie@Snt-Bzt(_<}^J{o12 zk+bpH`Sbc^w~HCR0-uv~_IY8&>tAT1!WZhFK8v&Ay(+TEHbsqC5o!rw4yI0)@cQXb zH>H6{-OJrO3p*Ngm%^-G!>l6M=WlQ<>jF3hzzPs_{F*fq&@#Q!k!vmw&S9kcpbje_ zKsZexWd;K90bm-qK45NdV}#K1Jj=rqgG64(^?cCU+`a4fqW8zDxp4&mXCqsx5-fY@ zM?y}<5908HlnSg#LnYZVnlC*EQ<5QU*FP9$O!Gy)M+kK87BYt1GXL}lopVGcBK~Jp zC%*JNwsgWN~3!^O*{(UoRj~wys~lHBJ(>*q19=vIsg zLz|T{I*OB19bzPLTZ;!@Z#e+=-33dQ)i0cnNJc#UFPN1CKR9~m{36o)^))qV?vqiE z*D{;GX`;vwgS)9%tSP`U>p$_^TaYT$vEY(qRn&2LFsW`CTEd#0iNaNqH2*;&IsY zTq%iN47>m}vA#eys$HwbcsABDJt^EAul86<|8_?I)s`_-u~ko^IMGW;s%5K4?sp?w zWj-+Z(T9v)D??5|?k|SP&YVq!Dr{vtWIX3}*d zqkh+_6u_bV?6hK*A)33;vD@lfp5KG&d%FFUjmR!yY1Q?Te{4d;UMMlxSs7Z zTSPaI_XT7!e#2QLZRQl{sa|!fy9MDgHuR0WUqm*>8_r`;@~$FwFa@WT*Y`1EqgT^} z^4O510Yoz%)}UtE{)Ts+LnLt9(SNuU(Nbde(%5tXF*B}$%9zqI7S5sf;H zR%29<8pBqAG*1OP<86m6ZKD)SpW4DGSwMzaZptv4M(;kb8IO_*lMy%_|1Aj{17nBQ zz~)bjza=Brb&4)<*LY_p{CS$IAS*lrEr05?wTf@k28s$y@+j|`rq$PT!uh9A%l$6L zkB@ug{wYv%&2XS?n~%Gc&xvJdCaXC+&$?FakA-nA1Hy8lpxw%v1HU)vC&tu4a_V% zOueDnPV**K8*|oswamoruqMW$wV6*<85-}9@v1x1sdoFaQ~IO-e^6S~fSde~^15WW zhC`#DNRDh_>p83&!|52$a_f4wusF6R)~f~QM9A+x106!=g<*CCKVZgq{7SJ=UG=o@ zp5M>FrP^q?25ZnVMX0;G^IRVTLUW=XJ=O0dshv*t$hf!@%m?2V3y^@URf%Ley3P-= zBGwsr3ib<{8*=o*Hj||t4`0+wMmFU9s0&aPv2)7pr;a?AolA$#9s9;F12Rej-L5nNG-Oy z4Qd$w-;r8O`BvjoHdj4*OoK{GVdno|Koa_$G=^QPTZ)8Rtqi95&)UqUO~~VG#iL|G z1Tu)$@Ea$~rM%u;HRFJ2z_6nVMl8~0@v6O^^1JtLD!Qa)=~8)41!{b0gRvWu_bpCD z@m{}SV;sf7y#RPd);xQojX$tFGA>$FPYil9wX0;)ar$TQzGSVAj{unEw5hy2|Tf=p|RcF=> z;ZPakqWOwa?XBnNl05_22)}usJke>LWFx!{h=oi>u&M_{Ngxx{mg^$Eriwn{bS))C zkfC8Z`uS&!)!d>E7)JbdIimZr=%l5+xj#>*<0hieK9kpvGSFw0`H4ELOrw8pF#>vF7J%C64IoO1 zj&{fP4iw#KuHIi0;4ht|X91)fKmTW>gT+YU+@IWEjIdRbw!T>tnh-?MpukDM6j*y1 zRU;SKDzU1n6LFvlVBf0ZjQe(kX4jpvtx*= zn2Az4&i?v4>IZb^Z!SWdq{Cs`>>(MZir03*qx@H1|~G?&vJ8j zW@sc@%P|8j0X4Oz=(bdQ>k_}UpZ`Zrjkl8kRYaH$h**YIp6C+UBaxw^s!if!&$%!) zo0+G}=CqyAk>a;feI_W^Fe>Oh%6d<4@Y4;rfa?8}xM$e9dFJ+o5L~mNr`teyeA9kD zzIQ-KolEoD#xdzwwCrJyW^|vwex?u2gX_(;TO9rL(Z1szVE2elNolo94J5!Zhxpsz zx6Hkh51zwjk7!XE+xku3T3Pf zhCXMWkVU-i`beR~u~zxYM%YaNX0R_|K|R&*u}gG1%|D#)Z%Z;FEez51;i5pxg@^Dq z**Ng4IEkszvb6g>VMgk_dQX^OI?~ljZ1N}FXhOv$qEnxp?-+PaBv^};UUcXQeK8KW zYmT`;_HQ5d(`ZyjU2S>6BlqGAlD%fO2haYDf-!Z}niQtyZ;qvan5u2Dl!bkJl>~~d ze`gZF`X!WjV#RQQm~I6rK~D5a*=`d#HEFi2>=k%KmG7~9QE71@)#n$ymN9EQf|WS$EMqgd;fJC zphojxeC9To298>Rwt>mkCCu%Qbc65M9!IR;jUK)JlK$h|G0&`LM)E;g$}vS?%Sip0 zcxf&ddy5kvHWT1lxmCpY?OuEtfrOpjo?w^9)@sA2NMZoR&pfcpgg$5@7ek^TsnGj; z-IKPYOVzD>4Il2#Dl2A|=Wrp4IIFOa)m14tbicYj;q7FM?*F zLg91DDx#O|2z=9%d|f|{&+Xizh#(z2sI*1=xCd>%;$N%`;G8)-$q3o7@EQoB*sLu7 z?sKcoL8NE<+M36$9R;e}^F;7GwkCpW@N z^Nxu(^k1EUa&fN%2myN%mrLgJROI#AS!V)vJq$feWI{Yity%;6x-lC&3i{DzAV7n1jL-#&RGJK*j;KXP#2 zl5R+8C1U35?ik+f7St8nw|Q59**YR@y96Lg?f=@FJ!!u{LriDS9*c>Y@H%SZR{8ykQQ$7w%_5ZSob~+{TLrsO=)bNr@Je)s-X5F`<7t3b3v{6x z`kzY5@o{W&=G`H%Se&nH52?DFN1M8SG9_K~Ff1x4@98X633G7Lzq z=V@zLR34=wp&#V0(dsQ^|9eP*p`URI3I)dYuX9)5C_VRfKI5=MCYJ1;GVnIGi5w#e z2AF_PvsAA~sbXf3g(FhP=U3m#`U6yr%~#?Ks9ox)6XES(rnmFyQ{_ZLgJ=g8LO+kD zI>tlw$=9Kc0BwWJar%8a-w8V;L;@`|%U6UJ8n=c3WmZ3?Cnf+CwDmes=_|CIj#Ic@ zpIW@6iH^l3qJ(2IE&pMuzmqm+(N~Cz)fZ8fm`dbC4!70tD;KH?Vm7Hy6E6y7-!_(Y z6m!OOl!#WZ&McT*ffk&Yne?Ko@A#MT4rXP34cq{ZLU6Gp=TXZW!c`|qSbV63{emEy z!}!vP-YEcJQH${;Y2SH7H|y~2abK~l>Xo;nvV}pC?ZbdXCmq-SuVd@inq`0d=+)IL zAp^EPbvqb71^6f())(kB5OEMqS@s;kKaGzJKo%5bq6}SK;oOAC0(VhM*SdwC{KCc5 z6ws0h_9%ar4wlJpSLt;~kO9yHGdbmh{GS*CkUA^45Ick`sMnCsJnggee}iwk7)5C1 zfy?$9EfEKDR)QKV6|AF>i#L)gr}~~3M0ALMT%s&z?ss$-E>7B89jSD7c9JL&BIs(pwClJz94l%Jo^Dw3zZ3 zg>B(fD?ods6;YS4Ws&S6B*)*MZI$^n4NBMFTcn8W*Sr0{-Y$G5SuCg4l{=jNW0sak z&*Y@7^}587A~Mr|Ez5&;v)70mDA%gVfdUFu8vCN;IoDP#Kz+fGg^KAChmR>LHll`} z;fLfRcS|tyraCOrh{s6J2cMMwbtS@7n^EvmE!Z3)0Z7xng)h!T-a>BTpp_(aK>$E2 zPoX0(@xN~Vy7VzgG`Vd)RS9NPiIDYnU`rVeD)`Ri@}J8kT!NonSa+Bb=#Y_m5v3og zdU$;>y2$bLv|Jz$Tv?_yvd8Xr@SdlF8|oo}%-b!)y63Eg2e#jp{kl4r+>pN~xh@?{ zv3MJde5%n@f1Tt_(LbsPMvljJ3zo|g@+6qw`tzdss_KT$dX8VNS2Ned; zg2`$8{Pblm^yRoSoMtS!@Qx;oJvmN&fyd=-(^O%A^sY`-g+Gz8)H;TFY|!M1$7nF7 zFtYM$-;cj53ixRP;oC#a#;5FmA=NyGTkM zw0V#CX7&m^b!27Yg?LP!6Ki46ZlqCj6<5$m4x)0{BaQr%WbMDdB*Fggk?|lpymf~R zj9v2vAKheyt$bh{wZx|;)4Ld!H^O>Ae$uh@VR~k>2R`|x$Hu=LjST)`e>Yp7hc}S2 zFepjo17~%nZqM-L>N-{oU&uwtG!N5(zp+XuiwDJt4_EMf<~C>PYQd)-D^bJq>-40z**0m+a@nu^8rR?)|ppy&|j9(ZoLI=I2dYYqH-EHtp8ml11<}u;7vbFd^ z)(}!8d%*;I4i{Si&GApqJV~C#R)roKfLv-T4(z=)UW6_W^1M8?5it z#^S&VHrq;Uc>7{I2Ifxhbq3Ki(4*%&u+5da0+sA`LRSRAQ`Y0F7q=~j^Z#pXc}d+Mkyjtq>Gp@FA`!e zfo>jM$>^|1t9(-UBG#Z_UFvU`P6$=bTMmeU?OGxpf)iq`3aovX2p807H{TtZc%zM# zy6r23lf!V2-aJJt!3w`qHEyMGZmbqndHwI39k_ME)Mb;+5CRJJt9Bn4!@g#5B-WmR zUr@S4{OSM7Q1# z0Y6VKV7JHX28G3ZsV*~7SM`x}68P6^x@(7;xaRcmLzIEMA$f?mP@)qb84FD z2|1oI6NCy|?Tf*V6JniH{|(?=Bp7?5Sp9s=HH@%Nw&aFmkJ)4)igP_UAaWxCo@+aI z^PgrUz_+de>p%KH4g(95``N6EOzT8%-BtEZU@EeNF?|ojan~N|Sx@f&jSQ-?9P$J-WJb z)1cJ!Gp{VoAT;1nU;bVcvST$Hktt|*4G^cG$jc9}ArNvOGXm(QlPaEhp8tEwg0)Fu zO)e-N!iY~D0Nwqo77Q@O4~Ymb%nH)FXL&Y`SJzQkFq6)nGy|B}=MiCR%suz*11jXIy6bS+xa_Vyp=!+KbkSObJ+KkWX)OGCy$`7J&S;PDlk^1(IH=HH%iM zuK?ORVwuwgfXing|IO6^*>X9!nkgk<{W==Ju1&ECQp!ujUcE2Q7$nIJ- ztq9Z%n!vvCEfm9rQNT*A`8eHtX_9BN2L@6KN^t-SFqUP}xUCd`jG$4p^phs>?s=v3 z$F2O|7{jOXj zl*p(fw$2?-wm+fv_8bG1;etc^y(Z&eimfS`69sbd!B#N82WoH*tYH}aKfT4$g9aj3 zj(N}XViNriJ}B6wLA9B)C|2v)(CbGQN76Z1Nlj=`wX+X~$TrEkFnEe@cG)w!M<1ek zljNUGrB=^*os6V>h|;p5uAYB!>;iyIYXkL>!&1@FP+5<&RE#S1R{g53V9>F1jvi7% zX-~3C*@aNC+@F@;-C>wpF@j1od#SkKbbeg5|ntbyK&Lvzs(7 zW{Xwu1pLgEtRp=r#fZQ8$v?)?*WRiR@rMY6XoYkmqgM*lFsM1|TP}8$;4^+TU6eO) z%B!6N&nXF`4Hf0@r2{O(p67M{`1S)J7hv)RBNsBngQ{BzZ~-@A-0tStTP9KurpzlQ zjHty3n5v2}M53Up*682jVDI~d@eDBN*E5lw#c8U}oDKm_Bx@JL{ z!HYlnEy{3Z`Rr)zeM4uvO8fTn%M2H~)?V^)f5R~B z^52zlfHbm;88 z3*=MS)2;$w?X+cr4{zc4%r3<#*^PF_!Atujzxai@)hl8Q-sz z)^HOzO52~)aj^wQTpDG*;75od0#!)u(H{-cMK!Ip_RGIWf2gLGXF6p0TTcJO*e5pP{*Mooe>Ry@~J8Y3rUC z^C$;wIiMU11Pr#+$hA^C;#BBKfSu>aM9ak=N!!1N&)<4#CbBy(Y~YyJ@&Wb9IY8nB zeBM{O_kCV{3+l_Ge;JtOuHVMj`j9i&uWR)P%1uu-f0B0E5)!zaM<7;evfR{Ckd#9hRj1!BvY%o1RQ`rjEL9Pp-$WYdaw0PwC{`w1`VI z3}g7awg{)?$GZ*3>Ql-uxQXX60-CC91ygKJ4;~|O*_8+`NG*kt4{<{HVNPSWrc*6i zpI0$RP@*3OhFRZ~204wEo$(I(U8P5kQ?6@bqG3Ix+0@^2XAmoubQnw@Qrqb)VZNyB z^^pfqRy^rbaw5;5sHT{i48CEEukWBCQb#mmT(EV8wQ$Ftjw@$-*up?eV3X}Q2o)$yz zQ=1FPU3D_p)pHldRblikj=|DPq$%bGO)GQ0g#Z0i1h^@nwKwu*=gcEV^9QczRJ+Lj zY#Z2($|e;7Dr_5;8P&aCb~GYCptRm}?#oVQd_4s!WxbAhjV&lBKXFyfrCua)1+h}O zQ8aw8Rm$Y=djpEsLxzzi8-H&*ul3=3l3lw2X{0dglAj8k*sO6o3=}J*A6$2Xa-S4_ zY2Y{*-cudjKvIuYfT=yWVVu$v!>;&^;NV(jx}&NoDT#)hUfg*nHWS*-VmPP_!-)U+ z9S_GiPTM<^mJv-88CWY<qm>-5I>KzK2$g45`m@} z6iJ^Q0VDwM@*@<~hK7a!+OtxJvaPA*N!e#rd*zB34sRI*x|1JYotH|;f<*0Wk)}Ya zxI~nzh-)9A)DL{D(9cWpis_}PCAY$;UF-sH)3ig7(4+~+bFKe$JaY}dG1liwO(Gr; z{bG|^nmXDATC-%klD&`*=MCXzjEe@eW|}spEOeN@HhIi8dM7{M$<@HgJ;4~j(S5<# z66ciCz2lg^9DVCBfM+lHXJD~Wl5bs4Pe=DDOD7$asJ%x5>w%z#3XtdloDsY8-qjPM zpYGQOR4Rtih}gFyg7G^e^#KdL=D{t59|OprM^B!lg9%}i;Jr0QM#gWO(O1>X)YLGk zaK83kktkj=Gt3Qqpui{-2@g*J|A(yCib*@G$*8%&bH-A8t#fzTtht;x^Vl~TeU6rJ z*pr!1O9nC@5sL2(RA9hf_cb0>!wNa&Lj%`Fk6-Hb%+mXF zT7yCFtfVA1GHR9@piN`a~)oA7-aB$?nlz8&`llqR zb~1R~76bAv+tT--bFP#bIH_3PUpYR(E!F;uUqmQ%q|k|3es__$JKG@Be`6kSe%w!y z^o}caQ<(<4ADD36+pYDB^-{LqO1iQd!kHV|UL7xALMm^C6l=}F%E=jg15R^{iK!Nl z#9#r1j_-0K&pXp{F`%Gy;)8K945%rz?pd6EKgJUx?{bl-glv--q|k5jg{E%IfLTEm z45yGv!P-i&3#}mi3<+C`*3uc6h#aE}iKY$v5g~H;%u3CXsS&}4veY0`(o~2&hYLi? z(v?~FBxFkp#9LJ6{4=m60Icjg&Hyr=YnhtK0Lq<}6%M}nL(pe_2~>+9Z1Dv_x4}m3 z1E9M{=9%Eb*JFO#IZWhBhND#o8%x_Q#Z`E*!Vg8^hdM zK|bb1&@OKz$n$&+ynsUS2;wr`-CG^(;|+dTAnF9!6D_b^g78;nnYjPFq{=vmy3!fl@$T#m0MQ z9S|?xt<%%WpNW)~U}RNqYI>n>UKZPZpDfy-@B(01`Cs@^zCSw+5&0#vQ&8#e4JngZ zV+6cI=LF7$0?mte8@0?T87R@+Nrs~n)On{U)<&=^kV6K(5Kt#B&JD&?M$$12hQyh$=aO zC=QWuK-?!GH{2s02;XtbBVqFb=l@mRoHQ0U3; zBc&M_w(?bIV>Y_9(A#CWh7Y$Q{sG`g+n-L=V`Hj)xzb9U_C>fYZ~pPc24MS5c4_mz`1;no%_Lvta8FaFrZ0AR+d$v&<)$&v54XaR3Y8Rf|L!a&4UannC#trYS5Hr?4O zgY!cl1NJ<%Mz}hs+U&rHNMc1)xe5V;2Dn`IrT`nuA-pPap6*!16b__YbmeU>(xPfKC}7t zO{-;d{e|hSBUMlTuiCymoXYQO{}?i5mdr$jBJ(^`p$L`8T$FjtJX47>Rm3TCN-1MF z%6KxCIYeZZA!Nurzw6QPt%N)9ryoVW{m#*3 zeq=wNT&{Yyv(V4;S{p(V6e(Pf_S9Rf&KSvzV9;yEme4Fq%#zQNnR|KLJU=Jd9G;$6C2qJEsvU}LHQ%HdU;uKyLpP%1>9QuZlE0@Lq+bZAkwPm{0kbx)5^SFc zRy|c_{Sc1F8`UU{FO+U9jCZ!h$ z0CyG@8(V7w#7=ehc#yD&dG-E*9P^85dD}zVsBt{QAW8W(?|qF4trf-v1@(Fo<8EJv zsngMMU)&^h8mgx{wrH)J5czElmHp%F$-3FbF5@kMbHlZ4t34lb&F)J$ae%LX?7O}s zv-&9tMD`?~_+}kcikx!8O=xTTjtn*IgfO)`tcgVLF-U;6ytemPT@@ko!qYAR~h{ld$(<3ZfnhMmSB z;kuKRk%sQLz`Z2iM}tbc@+{auSsTb%WD#G9M@TN<&{sk&3Q+Be@~ySW43qN2k`fsq zA)%GE$!yJ!@%1+|=dwGU>)o+>*dX-hFDXErvaT1aUAil;t78km4BWAIFKx0o$~nqg zH`Q120PX&-kq>k|6I!=Bekq{1*pVtv)H9+_TYk7A-o2Dc6}NkSGHX6?z`WyeZn2*9 zvuwQ~H-?cbMH%G;eAQFlvldLdf&_kjjPKm2Kx$>={e=YBvIA{sH>{;c8?Pn>NTHo` zp5w-@%6p;Sjrfp>=kbLfVe!^-7~JmROEuJpDWJ#;GnJUt98eAw4Lbb$U3cILqH6T`XxqOEiR10nqYBLBVv}>|8tIum<$IYIALOE~ zzEG@~6h4Z3{T_cSST9=+AAR!elU4kq9Z#+#pLAUku|E}?zU@*RedYP)NURKj49Nzu zbC^DN66HYJh4QSULBF1&5kgDQwE7|>lZ{&*p_8O8n3H72w1kIFD;~1)ligUCsPl8a zS(|ZOe)kGb8ZqgvP=a3NG4UEgmoYDKUfdcSo`CEC;oc)E@LUHTK?NKkxAa_IajOM# zH0Z=?=Ma?6D69&*Y7)fIShn{i?n@$|bs%%W=|-IjEK(3{pt2tE9m{4kzqP&_8%^z} z35*lj_B7<@GL99v^7QaMZXh=m3s`4yJU!FLtJL_4?6r^4ujyAvgdE^5z-HJ?M@sTF z0Zl*M)|^{Yyims%p}Hv>UqwA^YvGU^LK9N13DX^0rOdS_LX6eG;xh_j4P*9{n;F893^L$>-ka- z-446YsLdd}SXR6}=}(~3#mn3_!1u~pg5`<4+3LOeY@$4z9vk%Vs`AAL&MmHXQ7?W1 z5TIO)WXI~V&endRR;sv$K0QYvP+UWYlfD(Hui%{wszD2{GfXsVY+VZ1pGJO`rkC2X=1TT4zJ5bUU?#M_H?JJ~4(hnj;{EGE9a35wO$ku`qvjA$@l2?@ z1t4?h_N2*#0l`8ubU` z0&zj8Vcsz=^dAlj-7h8H39Lg(C-MM8h%ykEd`39bpr14n+ow<+ul~Syu?(+yI!qQ@ zjTYzYBh*;(&;9LYzt9kU7X~`rnh_PChu&W~T@hcHNHd@hK(!Ap=4=nP9Fzl+x+Fga zBr4cl;N)^>QtT`M9+F=$B2e5RmZlA03QXE8_dkqnF3NcDJaU4Z7(14xl%~55cq?H|Z}IsmfHeD#iMzEt ztx+f7Kz~UUA_+1?3}g|yu8 z+eBI|;mZ_(YTTaAUQ&{Wtg2?Z7;?s(>%1Bs%mbNFliGZ2;BV#v4iJ_+PL7Lq;qcF(|*Q>Xq&TB#X>^{0Gvg?x%R2l3rdP zsS7?Q>KeH`T@HR;u{T}Ao|V)Q95%fAfTRsJdAw?c;`RqqQnwppu1U7zG@;f|zJWE5 zv$7U6I>K~urB>yORwk{rYdT>v12Mo)G|f0#lvrdZlME7^-yhHmeW)*n%uo-7;t)D< zOi|k(m#paEEI020tumr{G;#{2v0A(b%YkX{*I}*Ra$((L=+lNRPw|whqWVHgj`6&-( zN{V)giREB<^6gbeX9w@Y(f^zUKggN-w=5{*K9`nTA~Hf>$H&V%cW8kST*Z$a?FUW| zIHza;jd=Nmn!L6TZ^+FVr$Tgy->G zxL=z!qqh7$KlTCX#M#lr?Y@+M8$1O6@p}bot>@PxBS8X$DR6h7Z&UO^B&j__4~qw9 z82G<`3CH6R4b@ywbd<4@A0?eVr!Y({v*p-9C}7hnj$*@eWqB^(^ZUT0T^b%Bno5w& z2l`J0ELVqJ#TFm**%-_i4qOhHhA=UbynXNV9~VA|&h~<}vXVcMt-aIJ#WKHxsmx_A zTZ|v^&zFnBjne}D!EerYu<$)srD!)dbs3LiR;y?IuKRvm@)xqmRq57LsZ zwxR;$yKI5NhfJKO@j5W}jrG`JtSZ#SB_|pKFD27ClAC|=%)U|*1IrQU9{;%<-^`D; z`@~%loUEv~-Zi>0-{k4pK-en4)Q~W^y*U`^rEq5TB(3~E-QRaMV`|11FJ1uu_Gq|X zeMqP#>A3TEwNxBm4X1uxfN1(R+#h+I%anI?od=r~P;T0!0U$oA+=D-=kB?GNJ8BnB zEmz;ALU#rCyk06CPru*Y*!P6tvC(I$2ASsG&yv1R*zpn5lBBE~;%a zDUNs}E4n!op@I56bl5>ni*_0UWtrk-;@iRZU-Z%DJ@--@@Ke9$w|mrZp;hcpTQSTS zDw`&JIXpjtGl60XXBD1*LMGucgka@amw^L7f3$8^*iLnfxlr)p{d=@KyYCl=ZrD%% zqvra1pxBZX!LmbQf~pa%NICu&i;aA10r)0Pc)G*w zKNU;lt#Oyy=>GM|cYfs90it6d`V9K9s+MnlO)Ci|H}_g7xDUBOCQZ_A_P8?jX~QWu zhKq`N-}q90{8D$szW_BM*J8++5T=d4jY?j!>+e%U>t4Y(qM|=fk@tzhaFF@y{DMx| zzgr8bkb;294v;cjZtczA@1tdW?=TWg?M%{thFpN%*9vP#^0QCJY3!1D$UPyAL zWd5ms!C@ySClR~u=*jHz#H&|vndDoE5#`*a?vZz%J-?|f|2nJxQH^8+E@GRGjKN9@ z`Ae%`M+}{+X$@W9pX1eg@#)P@HF9gMv;1oflf&!^bX!6LI!Y%~zdDG{ST&Zge~PqUT&SUAlJq-SY7B2Eb zntqMq{g*%nVh^iXXm2*ro?cax=+V7wvc0CeG}GSY1(8$Q` z#)XxqPq~f#Rxe}hd+VY!bw@n-DSty{1U?-LTb3-z>?M<-}>GR`Xkxk#i8lsM&pEc-fC&{Ep7 zUpe&4A-(=d+Zz^JONHeL3r;@}3({=VbaekJtPT8^!dkb?rnp<7xV+~7Utw+L6=A^t zd9%M79jeJ^N-!pjUQyAM<6LJ(%4msa2D1=;!2d&gK)jz+p;A*WMSw3NQ`Q@|#cIO? zw8@V^&Uc^Fqz`cGh)5UE8sX8~;acp8oX8P?|l`&{72kEdqDk^8~x`dz@)cC+A3nv-E zhTL+ivj~I!vC~vWNgh{^rz$&f^F@F|eVXgY?#$+ghVohKe=Dpp>yWaLD5OTpab%yo zx8I`L?O*BxDqDeV7dm)v9oD%N>YuJ!OH8xkIiP?vqJx@*i06!KOawiS0x(8%03XMx zu_TADep{Ym3p1KPbX@hN?^J!Ki3JqZP+CBl5A+8A3-N)XdVcZFspr{u`wDoVzDu6IRYGrE)3!kLGHcBPpgxr*T+T` zneLJ|^3Hs_{-yA(ZF@*Uf;7$G{JX`uT0NO72ZD%+wT((208_#l_Ga6j50`34$saw= zO*IflHF*pX14KdC^V~Bn&t9pCro={|gc0hTs%1NS8~p~uDy!b7Dc?9TnYF)AU-Yh+ zL)u`&eO@Lp(g0|0)c{6ZgC=Q;0CFgo7V%{5d*Ws{v0Zf3SW%cMMuUhTvQ#q;Os`#i^3 zh}Z$D5EM$s&mkkzQ_@)CIZwG~&vm1POtTXmgO;L9|rnZ|-$fn6Qoh4f@ z+g!M`vZbH)Y~CsVModJf^7YqRTOV)RJx%L3=&iarD}Q#%actPK_S0r?iP_h!qkV#& z+w5cL#p!9Hrpg9dwyRHob}mlf4kRQ?CCB-Bo_|VuZE4WgIetrnTtz_ zf%6w^JU{5OVq&f(j9i3&K6t}E$FRVjoRg#FCb?3NI4N4SQ` zG+lT3TKa??S^x0@X(SKuwcNB#ABiZn7?9bqIy~V8i=7w1c|{fZW~;x!--i}s*z;d5 z%540Xi|XQkzIdfz8Q*f;%3i3;n`Gi$4?d|){xWYo-alUHnc4o*OLmOx*K?K+SXx?I zSt+Wj5&_uh@=Q7<64##?92*O+t2+t!xr)BCDeM8eU0ytkiPf3+bA#wc8z?%Rpd6IMC zH7-6rzC!A|ef<2{^ekYHaAniw=`uJ_W(!O zqYs1z%e_TU$dmlm(mmsfilhMIc{IDT^5aLQ^z?Lr`|7tgZTrhu6crUi!o#cY3tKb{ z2L%PmvJM0AN=(+hi{Rs0cY%%SsZ+dthlWd|>dtDRbeZbOlNB)PT+ey)mqkSn^Yi!c z@lkBc^NNbnf|N@@dAWQHzez+@)v+)J;Tj)bH#afR)YwzIw6uiTBK~+sRG;6ZjG2s# zOu}bHz{l4&*P@vfjz0KfO$kSBZFx}`f;D^J`O#C=BM|Ge!%g|iRvvotN=h<_JM zRK)4-?!JBZ?p0kAUs@1%!9n%BbixA&L`9kJy4JaKLDA8B3`;yt=l0jf^258}sR>g~ zEzmei3$|{3tTsMAt^iszN=J{L*3r2E#~Be(QFdWr8kB+iiyAO^VZR>}4h|0Ykt65A z@)r9u-mSHy9EEd7Wi6Y3n56S)a%m~c<;#~xCk+e?a=jLG!8*QDtmkT zQM(8>c~He5s?H@PxQ6_0#yhXAja7PiKN*d92^@HBz?4TlKh|6eUm6%2tL->u%@y+0 z096G){hZ#Q$R*taB<8$Jjg*wGB4-otUUPs#4uLdY#U-Jepi_3**tl=fDYz&Q)A8Ab zLtI=}aCCThKY%Ih%NjaP#>CIV>7p8R)EZ*>jZ3&uA$b1Y9*pM zIKJM3J6#mIUj>NFP)?<4OFLC*7AboJR@2avB)>CLHoZlzL=+Uk zv9WY%>i4gnh~hjU$K;y#NED?WCq}luJfmU+JXlgvIk1}Z^YbrsWay3BeJJv`?=K4n zN68^7`d&uI(2&%@!J(?QHm-KGDPcDq9o@W|-~Rpksc30;@$>V;?lS-p?_D`}l{TZv z>7G(=IypJH(R$lnf7t?d2-PU4xiZmem0nA2$po<^JlBbvySw=1!DB1XaLPzadv*!-hO>)$qnk? zhza=k$WaC~>4~M=!2zn7vv0Ny`%65}q9|EeS%>DE<>~173@JfaPbu|X2q-AAU#Ems ziW@LHH%C?M_Jw+^If>4pWbTHevoi<>(%#yPQ|CObtK0d-NhOS)-9;*4Xl!k9QcTvw zn^Dz<{rc1FY`U8_Z{h|xy1LSOl^O%klEP@Ixr0Ksd+l_R&)1vuqSnzb{kO|@aK)w- z*NKfVEV5qtA6{GzHJxfokbd~;m7ZY7s`_wh0@tYrA}^y(zY5IvYo96K=3DS8{eZDE zh@JefmJ*mhJw8qe*l|1@Wr}Y~*2t|xrW>RdEm)ZTdR-c4~hSD&GE0Z#bsjsZBhr;@DeLKQ_h0f%-|RdAyArU+`@tjyb=`3)XZ#dI8xpew$C?j z-U!TSK4}BD;p&|MTM(}ESkt+%UHJItH{;vu?slMNLA)?IB?YH8k}Wtpn;&AkoJ()1 zAs;jNmMckQCNlF#p-m?rI0T-)$$qxdQkC4NPgUo58$nZ&&?PM8VRpy4bLWt~6mZ6O zVq!W*yf7F;_e|L2!fI-iGmMHc%^&j$Mv82DJ(ITjyl3wB^CToBgk!M^%o2_$5Yrm9 zD=jJU>RH~}T&H4XwNJe-oj9$(n>`11CHin>7>MHY_olG$&jW8~Q=;r{e}DgZjP0pM z8W2?Iz3J?v%gV|!GUi}`@A?sYkKviPf0S7}F;hA>KY!HHa(th~)Jt9~-;p|6pUuTi zZXObnfK4M5Y7&!j92d?-)N=EjDWkx?eSje`TS>^3$w zc>DZSRo~oRN0kLmq`N)=$klGxk!td-+HxR{_nS$|WSdA|yh+2Q1#8gV-M!iakZiy- zeFEfrM309ik>O@FVS}5C@?Pu3a#ubPO-xQUvKhaGEE%M2 z_Hl6$&XlcZ9bdPw;aG==_$DvtH{N&~#WnnOr2f|2ty?7cyLREAu3o!FMM>E*Db_Bo zrY|loju$Z0HivToXfj7FY@YhNP50b<`BLJTiV9B+>{Pd4(ro~n>1ZxE*yPkyJYr(v z7f&P)EVMJOtlyv4%QI((6$-F>qpg~Be`tgUddL)Tiw6%LR+h4wTBvEm1p(6ZSbPeM>)4PC zH#ZST(LPO27c}Wi(&fo$Th7URD4OYjC=Bv@6|fBnnm5qH zE4p7^=naZ)dz(`+7)yuTIE(a#$#NhKv>XJ(=L?2nvNumoA+j-gB;!F_Y);;i`wnL$ANH+yk2}$Kk`z>N~R* zZ)EDC3jy^>t)Hob7_~UEyIVikwo6D59xbJF^7zSM>Km90n8}C3^5cj8OYy%4ZP8`=bmZfyz>i+;}x03q+ diff --git a/tomography/EP2020/images/ep2020_original_spatial_distribution.png b/tomography/EP2020/images/ep2020_original_spatial_distribution.png deleted file mode 100644 index 148f232900b3284cede90324e7c7778cc1cdbc47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 66503 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

^f}Vn2LSAu^>$Pz_{zgL)zM61#nYgvE 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 Date: Thu, 18 Dec 2025 12:54:25 +1300 Subject: [PATCH 21/44] update tomography models --- tomography/EP2010/ep2010.h5 | 4 ++-- tomography/EP2020/ep2020.h5 | 4 ++-- tomography/EP2025/ep2025.h5 | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tomography/EP2010/ep2010.h5 b/tomography/EP2010/ep2010.h5 index 9e23a5c..b5835ad 100644 --- a/tomography/EP2010/ep2010.h5 +++ b/tomography/EP2010/ep2010.h5 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37d1eac30bbdedeb7a75985d701cd43e2cdc4a5f530140d69c6d3556ee44e7d2 -size 221080274 +oid sha256:f16409bb32d5ddbfbaa1e67b7718468acaa589d75a95830585f5143578e2ac1d +size 498347472 diff --git a/tomography/EP2020/ep2020.h5 b/tomography/EP2020/ep2020.h5 index 9643709..97abb1b 100644 --- a/tomography/EP2020/ep2020.h5 +++ b/tomography/EP2020/ep2020.h5 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56d28ab8c13a295573b2086eee24501f2e0be5aa1c8b6d1f5698091865f27ca9 -size 306468708 +oid sha256:d22aa31d0b1100c821262e1565d989ea885f4829d03bb440de28893e2d0408d9 +size 691979068 diff --git a/tomography/EP2025/ep2025.h5 b/tomography/EP2025/ep2025.h5 index 6f0d9c8..f18f410 100644 --- a/tomography/EP2025/ep2025.h5 +++ b/tomography/EP2025/ep2025.h5 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac2f5f9e1b8af08f5423aebaf83d1d516ae3ea4277282c57eb3376a92ffcbb97 -size 284439990 +oid sha256:79cacdd2bd1f5cb04a65af4b4f8d5ee35e79b31d46adf0e33bbe993b752d00b3 +size 643563147 From 73b6965760702bf734ae88f05b10dc3fe7681498 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Thu, 18 Dec 2025 13:05:01 +1300 Subject: [PATCH 22/44] EP2017 --- tomography/EP2017/EP2017.h5 | 3 + tomography/EP2020/README.md | 29 --- .../interpolate_to_uniform_nzcvm_grid.py | 200 ------------------ tomography/EP2022/EP2022.h5 | 3 + 4 files changed, 6 insertions(+), 229 deletions(-) create mode 100644 tomography/EP2017/EP2017.h5 delete mode 100644 tomography/EP2020/README.md delete mode 100644 tomography/EP2020/tools/interpolate_to_uniform_nzcvm_grid.py create mode 100644 tomography/EP2022/EP2022.h5 diff --git a/tomography/EP2017/EP2017.h5 b/tomography/EP2017/EP2017.h5 new file mode 100644 index 0000000..780dd4e --- /dev/null +++ b/tomography/EP2017/EP2017.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60a9d8558b8e59383229bc87ffabc7ce89563dc5f157cc791b99551fefd2fb81 +size 686263471 diff --git a/tomography/EP2020/README.md b/tomography/EP2020/README.md deleted file mode 100644 index 02ccaaa..0000000 --- a/tomography/EP2020/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# EP2020 Tomography Model - -The EP2020 tomography model, as defined in the NZCVM registry, provides a foundational background velocity structure for New Zealand. It is an essential component for integrating more detailed regional and basin models. - -## Model Details - -This model is explicitly defined in the `nzcvm_registry.yaml` file, which acts as the central source of truth for its properties: - -* **Name:** `EP2020` -* **Author:** Eberhart-Phillips et al. (2020) -* **Title:** New Zealand Wide model 2.2 seismic velocity and Q structure -* **Data Path:** `global/tomography/ep2020.h5` -* **Elevation Layers (`elev`):** The model is defined on 25 discrete elevation layers, specified in kilometers. These layers range from 15 km above sea level down to 750 km below, providing a deep velocity profile. - -## Data Integration and Visualization - -The raw data for the EP2020 model consists of scattered points of seismic velocity measurements. For use within the 3D velocity model, this data is processed by interpolating these points onto a regular, uniform grid. The interpolated data forms a series of smooth velocity planes at different elevations. - -| Original Data (TXT) | Interpolated Data (HDF5) | -|---------------------|--------------------------| -| Original Spatial Distribution | Interpolated Spatial Distribution | - -The left panel shows the original tomography dataset in EP2020 ASCII format. Grid points follow the model's rotated coordinate system, producing an irregular pattern of longitudes that cross the dateline. - -The right panel shows the same model after interpolation onto a uniform rectilinear latitude–longitude grid. The interpolated dataset contains 1,120,000 points arranged as 1,400 unique latitudes × 800 unique longitudes, spanning 48°S to 33°S in latitude and 165°E to 180°E in longitude. This regularized grid provides a contiguous New Zealand domain that is directly compatible with 3D visualization and numerical simulations. - -## References - -* Eberhart-Phillips, D., & Reyners, M. et al. (2020). *New Zealand Wide model 2.2 seismic velocity and Q structure*. New Zealand Journal of Geology and Geophysics. diff --git a/tomography/EP2020/tools/interpolate_to_uniform_nzcvm_grid.py b/tomography/EP2020/tools/interpolate_to_uniform_nzcvm_grid.py deleted file mode 100644 index 6f28ee2..0000000 --- a/tomography/EP2020/tools/interpolate_to_uniform_nzcvm_grid.py +++ /dev/null @@ -1,200 +0,0 @@ -""" -Interpolate EP2020 tomography data onto a uniform NZCVM grid. - -This script interpolates scattered EP2020 tomography points onto a uniform -rectilinear grid suitable for use in the NZCVM framework. -""" - -from pathlib import Path - -import h5py -import numpy as np -import pandas as pd -from scipy.interpolate import griddata - - -def read_ep_txt(txt_path: str | Path) -> pd.DataFrame: - """ - Read EP-style TXT tomography data. - - Parameters - ---------- - txt_path : str or Path - Path to the EP TXT file. - - Returns - ------- - pd.DataFrame - DataFrame containing tomography data with depth inverted to negative values. - """ - col_names = [ - "vp", "vp_o_vs", "vs", "rho", "sf_vp", "sf_vp_o_vs", - "x", "y", "depth", "lat", "lon" - ] - df = pd.read_csv( - txt_path, - sep=r"\s+", - skiprows=2, - names=col_names, - engine="python" - ) - df['depth']=-1*df['depth'] - - return df - - -def load_nzcvm_grid(h5_path: str | Path) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """ - Load NZCVM grid coordinates from reference HDF5 file. - - Parameters - ---------- - h5_path : str or Path - Path to the reference HDF5 file. - - Returns - ------- - tuple[np.ndarray, np.ndarray, np.ndarray] - Tuple containing (latitudes, longitudes, depths). - """ - with h5py.File(h5_path, "r") as f: - depth_keys = sorted(f.keys(), key=lambda x: float(x)) - depth = np.array([float(k) for k in depth_keys]) - group0 = f[depth_keys[0]] - lat = group0["latitudes"][:] - lon = group0["longitudes"][:] - return lat, lon, depth - - -def interpolate_property_to_grid( - df: pd.DataFrame, - lat: np.ndarray, - lon: np.ndarray, - depth: np.ndarray, - field_name: str -) -> np.ndarray: - """ - Interpolate a property from scattered points to a regular grid. - - Parameters - ---------- - df : pd.DataFrame - Input DataFrame containing scattered data points. - lat : np.ndarray - Target latitude grid points. - lon : np.ndarray - Target longitude grid points. - depth : np.ndarray - Target depth levels. - field_name : str - Name of the field to interpolate. - - Returns - ------- - np.ndarray - Interpolated data on regular grid with shape (ndepth, nlat, nlon). - """ - nlat, nlon, ndepth = len(lat), len(lon), len(depth) - out = np.full((ndepth, nlat, nlon), np.nan, dtype=np.float32) - - for iz, d in enumerate(depth): - df_d = df[np.isclose(df["depth"], d, atol=1e-3)] - if df_d.empty: - print(f"⚠️ No data at depth={d:.2f} km") - continue - - points = df_d[["lon", "lat"]].values - values = df_d[field_name].values - lon_grid, lat_grid = np.meshgrid(lon, lat) - - interp = griddata(points, values, (lon_grid, lat_grid), method="linear", fill_value=0.0) - out[iz] = interp - - return out - - -def write_epstyle_hdf5( - output_path: str | Path, - lats: np.ndarray, - lons: np.ndarray, - depth_list: np.ndarray, - vp_stack: np.ndarray, - vs_stack: np.ndarray, - rho_stack: np.ndarray -) -> None: - """ - Write EP-style tomography HDF5 file. - - Each depth slice becomes a group named by depth in km. - - Parameters - ---------- - output_path : str or Path - Output HDF5 file path. - lats : np.ndarray - Latitude values (nlat,). - lons : np.ndarray - Longitude values (nlon,). - depth_list : np.ndarray - Depths in km (nz,). - vp_stack : np.ndarray - P-wave velocity data (nz, nlat, nlon). - vs_stack : np.ndarray - S-wave velocity data (nz, nlat, nlon). - rho_stack : np.ndarray - Density data (nz, nlat, nlon). - """ - lon_grid, lat_grid = np.meshgrid(lons, lats) - coords = np.stack([lat_grid, lon_grid], axis=-1) # shape: (nlat, nlon, 2) - - vp_stack[np.isnan(vp_stack)]=-999.0 - vs_stack[np.isnan(vs_stack)]=-999.0 - rho_stack[np.isnan(rho_stack)]=-999.0 - - with h5py.File(output_path, "w") as f: - for iz, depth in enumerate(depth_list): - grp = f.create_group(f"{depth:.0f}") - grp.create_dataset("latitudes", data=lats) - grp.create_dataset("longitudes", data=lons) - grp.create_dataset("coords", data=coords) - grp.create_dataset("vp", data=vp_stack[iz]) - grp.create_dataset("vs", data=vs_stack[iz]) - grp.create_dataset("rho", data=rho_stack[iz]) - - print(f"✅ Saved EP-style HDF5 to {output_path}") - - -def main() -> None: - """ - Main function to run the interpolation process. - - This function orchestrates the entire interpolation workflow: - 1. Read EP2020 data - 2. Load NZCVM grid - 3. Interpolate data to grid - 4. Save results to HDF5 - """ - input_txt = Path("vlnzw2p2dnxyzltln.tbl.txt") - ref_h5 = Path("../../EP2010/ep2010.h5") - out_h5 = Path("../ep2020_uniform.h5") - - print("📥 Reading EP-style TXT...") - df = read_ep_txt(input_txt) - print(set(df['depth'])) - - print("📐 Loading NZCVM grid...") - lat, lon, depth = load_nzcvm_grid(ref_h5) - - print("📊 Interpolating vp...") - vp = interpolate_property_to_grid(df, lat, lon, depth, "vp") - print("📊 Interpolating vs...") - vs = interpolate_property_to_grid(df, lat, lon, depth, "vs") - print("📊 Interpolating rho...") - rho = interpolate_property_to_grid(df, lat, lon, depth, "rho") - - print("💾 Writing to output HDF5...") - write_epstyle_hdf5(out_h5, lat, lon, depth, vp, vs, rho) - - -if __name__ == "__main__": - main() diff --git a/tomography/EP2022/EP2022.h5 b/tomography/EP2022/EP2022.h5 new file mode 100644 index 0000000..379fbeb --- /dev/null +++ b/tomography/EP2022/EP2022.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3fb2d96fe6dfcf889bf9a0c63fa265f66b72a0795652a979d1ae2792b7619b5 +size 692517900 From c23e66220d4ee6b3d61868b089c730144c647036 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Thu, 18 Dec 2025 13:12:20 +1300 Subject: [PATCH 23/44] Added updated tomography models --- tomography/EP2010/{ep2010.h5 => EP2010.h5} | 0 tomography/EP2020/{ep2020.h5 => EP2020.h5} | 0 tomography/EP2025/{ep2025.h5 => EP2025.h5} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tomography/EP2010/{ep2010.h5 => EP2010.h5} (100%) rename tomography/EP2020/{ep2020.h5 => EP2020.h5} (100%) rename tomography/EP2025/{ep2025.h5 => EP2025.h5} (100%) diff --git a/tomography/EP2010/ep2010.h5 b/tomography/EP2010/EP2010.h5 similarity index 100% rename from tomography/EP2010/ep2010.h5 rename to tomography/EP2010/EP2010.h5 diff --git a/tomography/EP2020/ep2020.h5 b/tomography/EP2020/EP2020.h5 similarity index 100% rename from tomography/EP2020/ep2020.h5 rename to tomography/EP2020/EP2020.h5 diff --git a/tomography/EP2025/ep2025.h5 b/tomography/EP2025/EP2025.h5 similarity index 100% rename from tomography/EP2025/ep2025.h5 rename to tomography/EP2025/EP2025.h5 From 8fdcc2496d962f3f3170d1d7b8118c882ebc8473 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Thu, 18 Dec 2025 13:15:02 +1300 Subject: [PATCH 24/44] Remove EP2022 from repository --- tomography/EP2022/EP2022.h5 | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 tomography/EP2022/EP2022.h5 diff --git a/tomography/EP2022/EP2022.h5 b/tomography/EP2022/EP2022.h5 deleted file mode 100644 index 379fbeb..0000000 --- a/tomography/EP2022/EP2022.h5 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f3fb2d96fe6dfcf889bf9a0c63fa265f66b72a0795652a979d1ae2792b7619b5 -size 692517900 From ec98543cf17903fe7cace1027f95152cc8e35e3c Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Thu, 18 Dec 2025 13:37:01 +1300 Subject: [PATCH 25/44] Reset repository tracking for clean re-add --- tomography/BASSETT2025/README.md | 5 - tomography/EP2010/EP2010.h5 | 3 - tomography/EP2017/EP2017.h5 | 3 - tomography/EP2020/EP2020.h5 | 3 - tomography/EP2025/EP2025.h5 | 3 - tomography/README.md | 60 -- tomography/tools/compare_tomo_h5.py | 372 ------------ tomography/tools/tomo_analysis.py | 422 ------------- tomography/tools/tomo_h5_downcast_to_f32.py | 133 ----- tomography/tools/tomo_in2h5.py | 275 --------- .../tools/visualisation/tomo_3dviewer.py | 561 ------------------ .../tools/visualisation/tomo_boundary.py | 100 ---- 12 files changed, 1940 deletions(-) delete mode 100644 tomography/BASSETT2025/README.md delete mode 100644 tomography/EP2010/EP2010.h5 delete mode 100644 tomography/EP2017/EP2017.h5 delete mode 100644 tomography/EP2020/EP2020.h5 delete mode 100644 tomography/EP2025/EP2025.h5 delete mode 100644 tomography/README.md delete mode 100644 tomography/tools/compare_tomo_h5.py delete mode 100644 tomography/tools/tomo_analysis.py delete mode 100644 tomography/tools/tomo_h5_downcast_to_f32.py delete mode 100644 tomography/tools/tomo_in2h5.py delete mode 100644 tomography/tools/visualisation/tomo_3dviewer.py delete mode 100644 tomography/tools/visualisation/tomo_boundary.py diff --git a/tomography/BASSETT2025/README.md b/tomography/BASSETT2025/README.md deleted file mode 100644 index 117a71e..0000000 --- a/tomography/BASSETT2025/README.md +++ /dev/null @@ -1,5 +0,0 @@ -This is currently in progress. - ----- -Bassett, D., Henrys, S., Tozer, B., van Avendonk, H., Gase, A., Bangs, N., et al. (2025). Crustal structure of the Hikurangi subduction zone revealed by four decades of Onshore-Offshore seismic data: Implications for the dimensions and slip behavior of the seismogenic zone. Journal of Geophysical Research: Solid Earth, 130, e2024JB030268. https://doi.org/10.1029/2024JB030268 - diff --git a/tomography/EP2010/EP2010.h5 b/tomography/EP2010/EP2010.h5 deleted file mode 100644 index b5835ad..0000000 --- a/tomography/EP2010/EP2010.h5 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f16409bb32d5ddbfbaa1e67b7718468acaa589d75a95830585f5143578e2ac1d -size 498347472 diff --git a/tomography/EP2017/EP2017.h5 b/tomography/EP2017/EP2017.h5 deleted file mode 100644 index 780dd4e..0000000 --- a/tomography/EP2017/EP2017.h5 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:60a9d8558b8e59383229bc87ffabc7ce89563dc5f157cc791b99551fefd2fb81 -size 686263471 diff --git a/tomography/EP2020/EP2020.h5 b/tomography/EP2020/EP2020.h5 deleted file mode 100644 index 97abb1b..0000000 --- a/tomography/EP2020/EP2020.h5 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d22aa31d0b1100c821262e1565d989ea885f4829d03bb440de28893e2d0408d9 -size 691979068 diff --git a/tomography/EP2025/EP2025.h5 b/tomography/EP2025/EP2025.h5 deleted file mode 100644 index f18f410..0000000 --- a/tomography/EP2025/EP2025.h5 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:79cacdd2bd1f5cb04a65af4b4f8d5ee35e79b31d46adf0e33bbe993b752d00b3 -size 643563147 diff --git a/tomography/README.md b/tomography/README.md deleted file mode 100644 index 2804d55..0000000 --- a/tomography/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Tomography Models in NZCVM - -The tomography models in the NZCVM provide the background velocity structure for New Zealand. These models are derived from seismic travel-time data and offer a lower-resolution (~10 km) representation of the subsurface velocity structure. - -## Available Tomography Models - -The NZCVM currently supports the following tomography models: - -1. EP2010: Based on New Zealand-wide model 1.0 by Eberhart-Phillips et al. (2010). -2. EP2020: Based on NZWide 2.2 model by Eberhart-Phillips et al. (2020). -3. EP2025: Based on NZWide 3.1 model by Eberhart-Phillips et al. (2025). -4. CHOW2020_EP2020_MIX: Combination of the CHOW2020 model (Chow et al. 2020) in North Island and the EP2020 model for the rest - -## Tomography Model Definition - -Tomography models are defined in the `nzcvm_registry.yaml` file. Here's an example of a tomography model definition: - -```yaml -tomography: - - name: EP2020 - elev: [ 15, 1, -3, -8, -15, -23, -30, -38, -48, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] - path: global/tomography/ep2020.h5 - author: Eberhart-Phillips et al. (2020) - title: New Zealand Wide model 2.2 seismic velocity and Qs and Qp models for New Zealand - url: https://10.5281/zenodo.3779523 -``` - -The key components of a tomography model definition are: - -- **name**: Identifier for the tomography model. -- **elev**: Array of elevation values (in kilometers) for the model. -- **path**: Path to the tomography model data file (in HDF5 format). -- **author**: Name of the author(s) or organization that created the model. -- **title**: Title or description of the model. -- **url**: URL where the model can be accessed or downloaded. - - -## Tomography Submodels - -Tomography submodels are used to compute velocity values at specific locations based on the tomography model data. These submodels are defined in the `nzcvm_registry.yaml` file and are associated with surfaces in the model version configuration. - -```yaml -submodel: - - name: ep_tomography_submod_v2010 - type: tomography - module: ep_tomography_submod_v2010 -``` - -The `module` specifies the name of the accompanying Python code that prescribes how to calculate velocity at locations within the region below the `surface`. - - -## Data Format - -Tomography model data is stored in HDF5 format. For details on the structure and contents of these files, see the [Data Formats](DataFormats.md) page. - -## References - -[//]: # (- Donna Eberhart-Phillips, Martin Reyners, Stephen Bannister, Mark Chadwick, Susan Ellis; Establishing a Versatile 3-D Seismic Velocity Model for New Zealand. *Seismological Research Letters* 2010; 81 (6): 992–1000. doi: [https://doi.org/10.1785/gssrl.81.6.992](https://doi.org/10.1785/gssrl.81.6.992).) -- Donna Eberhart-Phillips, Stephen Bannister, Martin Reyners, and Stuart Henrys. "New Zealand Wide Model 2.2 Seismic Velocity and Qs and Qp Models for New Zealand". *Zenodo*, May 1, 2020. [https://doi.org/10.5281/zenodo.3779523](https://doi.org/10.5281/zenodo.3779523). -- Bryant Chow, Yoshihiro Kaneko, Carl Tape, Ryan Modrak, John Townend, An automated workflow for adjoint tomography—waveform misfits and synthetic inversions for the North Island, New Zealand, Geophysical Journal International, Volume 223, Issue 3, December 2020, Pages 1461–1480, https://doi.org/10.1093/gji/ggaa381 diff --git a/tomography/tools/compare_tomo_h5.py b/tomography/tools/compare_tomo_h5.py deleted file mode 100644 index 77fec5c..0000000 --- a/tomography/tools/compare_tomo_h5.py +++ /dev/null @@ -1,372 +0,0 @@ -#!/usr/bin/env python3 -""" -Compare two HDF5 tomography files using statistical sampling. - -This script performs detailed comparison of HDF5 tomography files by sampling -points from each depth level and computing various error metrics including -MAE, relative MAE, RMSE, and maximum absolute difference. -""" - -import sys -import traceback -from pathlib import Path -from typing import Annotated - -import h5py -import numpy as np -import typer - -app = typer.Typer(pretty_exceptions_enable=False) - -VARS_DEFAULT = ("vp", "vs", "rho") - - -def sort_key_level(s: str): - """ - Sort key function for depth level strings. - - Parameters - ---------- - s : str - Level string to convert for sorting. - - Returns - ------- - float or str - Numeric value if convertible, otherwise original string. - """ - try: - return float(s) - except ValueError: - return s - - -def list_root_groups(h5: h5py.File) -> list[str]: - """ - List all root groups in an HDF5 file. - - Parameters - ---------- - h5 : h5py.File - Open HDF5 file handle. - - Returns - ------- - list[str] - List of root group names. - """ - return [k for k in h5.keys() if isinstance(h5[k], h5py.Group)] - - -def summarize_file(path: str) -> list[str]: - """ - Summarize the structure of an HDF5 file. - - Parameters - ---------- - path : str - Path to the HDF5 file. - - Returns - ------- - list[str] - List of root group names in the file. - """ - with h5py.File(path, "r") as f: - grps = list_root_groups(f) - print(f"- {path}: {len(grps)} root groups") - preview = ", ".join(sorted(grps, key=sort_key_level)[:8]) - if len(grps) > 8: - preview += ", …" - print(f" groups: {preview}") - # peek first group for shapes/dtypes - if grps: - gname = sorted(grps, key=sort_key_level)[0] - g = f[gname] - for ds in ("vp", "vs", "rho", "latitudes", "longitudes"): - if ds in g: - print(f" {gname}/{ds}: shape={g[ds].shape}, dtype={g[ds].dtype}") - return grps - - -def build_row_plan( - ny: int, nx: int, n_samples: int, rng: np.random.Generator, replace: bool -) -> tuple[dict[int, np.ndarray], int]: - """ - Build a sampling plan for selecting points from a 2D grid. - - Parameters - ---------- - ny : int - Number of rows in the grid. - nx : int - Number of columns in the grid. - n_samples : int - Target number of samples to select. - rng : np.random.Generator - Random number generator. - replace : bool - Whether to sample with replacement. - - Returns - ------- - tuple[dict[int, np.ndarray], int] - A mapping from row indices to column indices, and total unique points count. - """ - total = ny * nx - n = min(n_samples, total) - lin = rng.choice(total, size=n, replace=replace) - lin = np.unique(lin) # drop duplicates to keep point selection valid - jj, ii = np.divmod(lin, nx) - - plan: dict[int, list] = {} - for r, c in zip(jj, ii): - plan.setdefault(int(r), []).append(int(c)) - - # sort & unique columns per row - unique_points = 0 - plan_sorted: dict[int, np.ndarray] = {} - for r, cols in plan.items(): - arr = np.array(cols, dtype=np.int64) - arr = np.unique(arr) # increasing order - plan_sorted[r] = arr - unique_points += arr.size - return plan_sorted, unique_points - - -def compare_levels( - f1: h5py.File, - f2: h5py.File, - levels: list[str], - vars_to_check: list[str], - n_samples: int, - rng: np.random.Generator, - with_replacement: bool, - eps: float, -): - """ - Compare data between two HDF5 files across multiple depth levels. - - Parameters - ---------- - f1 : h5py.File - First HDF5 file handle. - f2 : h5py.File - Second HDF5 file handle. - levels : list[str] - List of depth levels to compare. - vars_to_check : list[str] - List of variable names to compare. - n_samples : int - Number of sample points to use for comparison. - rng : np.random.Generator - Random number generator. - with_replacement : bool - Whether to sample with replacement. - eps : float - Epsilon value for relative error calculations. - """ - totals_abs = {v: 0.0 for v in vars_to_check} - totals_rel = {v: 0.0 for v in vars_to_check} - totals_rmse_sq = {v: 0.0 for v in vars_to_check} - totals_cnt = {v: 0 for v in vars_to_check} - totals_max = {v: 0.0 for v in vars_to_check} - - for lvl in levels: - print(f"\nLevel {lvl}:") - g1, g2 = f1[lvl], f2[lvl] - ref = next((v for v in vars_to_check if v in g1 and v in g2), None) - if ref is None: - print(" [skip] none of requested datasets present in both files here") - continue - if g1[ref].shape != g2[ref].shape: - print( - f" [skip] shape mismatch for {ref}: {g1[ref].shape} vs {g2[ref].shape}" - ) - continue - - ny, nx = g1[ref].shape - row_plan, n_eff = build_row_plan(ny, nx, n_samples, rng, with_replacement) - print(f" sampling {n_eff} unique points across {len(row_plan)} rows") - - for name in vars_to_check: - if name not in g1 or name not in g2: - print(f" {name}: [missing in one file] skip") - continue - - # stream metrics row-by-row to respect monotonic index rule - abs_sum = 0.0 - rel_sum = 0.0 - sq_sum = 0.0 - n_count = 0 - max_abs = 0.0 - - ds1, ds2 = g1[name], g2[name] - for r, cols in row_plan.items(): - a = ds1[r, cols].astype(np.float64, copy=False) - b = ds2[r, cols].astype(np.float64, copy=False) - diff = a - b - absdiff = np.abs(diff) - denom = np.maximum(eps, np.abs(a)) - - abs_sum += float(absdiff.sum()) - rel_sum += float((absdiff / denom).sum()) - sq_sum += float((diff * diff).sum()) - n_count += int(a.size) - if a.size: - max_abs = max(max_abs, float(absdiff.max())) - - if n_count == 0: - print(f" {name}: no comparable samples") - continue - - mae = abs_sum / n_count - relmae = rel_sum / n_count - rmse = np.sqrt(sq_sum / n_count) - print( - f" {name}: MAE={mae:.3e} relMAE={relmae:.3e} RMSE={rmse:.3e} max|Δ|={max_abs:.3e}" - ) - - totals_abs[name] += abs_sum - totals_rel[name] += rel_sum - totals_rmse_sq[name] += sq_sum - totals_cnt[name] += n_count - totals_max[name] = max(totals_max[name], max_abs) - - print("\n=== Overall (sample-weighted across processed levels) ===") - for name in vars_to_check: - if totals_cnt[name] == 0: - print(f" {name}: no comparable samples") - continue - overall_mae = totals_abs[name] / totals_cnt[name] - overall_rel = totals_rel[name] / totals_cnt[name] - overall_rmse = np.sqrt(totals_rmse_sq[name] / totals_cnt[name]) - print( - f" {name}: MAE={overall_mae:.3e} relMAE={overall_rel:.3e} RMSE={overall_rmse:.3e} max|Δ|={totals_max[name]:.3e}" - ) - - -@app.command() -def main( - file_a: Annotated[ - Path, - typer.Argument(exists=True, dir_okay=False, help="First HDF5 file to compare"), - ], - file_b: Annotated[ - Path, - typer.Argument(exists=True, dir_okay=False, help="Second HDF5 file to compare"), - ], - vars: Annotated[ - list[str], - typer.Option( - "-v", - "--vars", - help=f"Datasets to compare (default: {', '.join(VARS_DEFAULT)})", - ), - ] = list(VARS_DEFAULT), - samples: Annotated[ - int, - typer.Option( - "-n", - "--samples", - min=1, - help="Target random points per level (unique after de-dup)", - ), - ] = 200_000, - seed: Annotated[int, typer.Option(help="RNG seed")] = 0, - first_level_only: Annotated[ - bool, - typer.Option( - "--first-level-only", help="Compare only the shallowest common level" - ), - ] = False, - with_replacement: Annotated[ - bool, - typer.Option( - "--with-replacement", help="Sample with replacement before unique()" - ), - ] = False, - eps: Annotated[ - float, - typer.Option(min=0.0, help="Epsilon for relative error denominator"), - ] = 1e-9, - debug: Annotated[ - bool, - typer.Option("--debug", help="Show tracebacks on errors"), - ] = False, -) -> None: - """ - Compare two HDF5 tomography files (row-grouped random sampling per level). - - Parameters - ---------- - file_a : Path - First HDF5 file to compare. - file_b : Path - Second HDF5 file to compare. - vars : list[str], optional - Datasets to compare. - samples : int, optional - Target random points per level. - seed : int, optional - RNG seed. - first_level_only : bool, optional - Compare only the shallowest common level. - with_replacement : bool, optional - Sample with replacement before unique(). - eps : float, optional - Epsilon for relative error denominator. - debug : bool, optional - Show tracebacks on errors. - """ - rng = np.random.default_rng(seed) - - print("=== Structure check ===") - try: - grps_a = summarize_file(str(file_a)) - grps_b = summarize_file(str(file_b)) - except (OSError, IOError, KeyError) as e: - if debug: - traceback.print_exc() - else: - print(f"Error reading HDF5 files: {e}", file=sys.stderr) - raise typer.Exit(2) - - common = sorted(set(grps_a) & set(grps_b), key=sort_key_level) - only_a = sorted(set(grps_a) - set(grps_b), key=sort_key_level) - only_b = sorted(set(grps_b) - set(grps_a), key=sort_key_level) - - print("\n=== Level set comparison ===") - print(f"Common levels: {len(common)}") - if only_a: - print( - f"Only in A ({len(only_a)}): {only_a[:8]}{' …' if len(only_a) > 8 else ''}" - ) - if only_b: - print( - f"Only in B ({len(only_b)}): {only_b[:8]}{' …' if len(only_b) > 8 else ''}" - ) - if not common: - print("No common levels — nothing to compare.") - raise typer.Exit(1) - - levels = common[:1] if first_level_only else common - print( - f"\nWill compare {len(levels)} level(s): {levels[:8]}{' …' if len(levels) > 8 else ''}" - ) - - with h5py.File(str(file_a), "r") as f1, h5py.File(str(file_b), "r") as f2: - compare_levels( - f1, - f2, - levels, - vars, - samples, - rng, - with_replacement, - eps, - ) - - -if __name__ == "__main__": - app() diff --git a/tomography/tools/tomo_analysis.py b/tomography/tools/tomo_analysis.py deleted file mode 100644 index 4ff8abf..0000000 --- a/tomography/tools/tomo_analysis.py +++ /dev/null @@ -1,422 +0,0 @@ -""" -Analyze and plot tomography data from TXT or HDF5 files. - -This script provides functionality to analyze tomography data, generate spatial -distribution plots, check grid consistency, and create various visualizations. -""" - -from pathlib import Path -from typing import Annotated - -import cartopy.crs as ccrs -import cartopy.feature as cfeature -import h5py -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -import seaborn as sns -import typer - -app = typer.Typer(pretty_exceptions_enable=False) - - -def load_txt_ep_format(txt_file: str) -> pd.DataFrame: - """ - Load EP-style TXT tomography grid. Keeps longitudes as provided - (may include values >180 or <0). - - Parameters - ---------- - txt_file : str - Path to the EP-style TXT tomography file. - - Returns - ------- - pd.DataFrame - DataFrame containing tomography data with columns for coordinates and properties. - """ - col_names = [ - "vp", "vp_o_vs", "vs", "rho", "sf_vp", "sf_vp_o_vs", - "x", "y", "depth", "lat", "lon" - ] - df = pd.read_csv( - txt_file, - sep=r"\s+", - skiprows=2, - names=col_names, - engine="python" - ) - # NOTE: do NOT force-convert lon here; plotting handles 0–360 or -180..180 - return df - - -def load_hdf5_data(h5_file: str) -> pd.DataFrame: - """ - Load a single depth slice from the HDF5 tomography file into a flat DataFrame. - - Parameters - ---------- - h5_file : str - Path to the HDF5 tomography file. - - Returns - ------- - pd.DataFrame - DataFrame containing flattened tomography data from the shallowest depth slice. - """ - with h5py.File(h5_file, "r") as f: - depth_keys = sorted(f.keys(), key=lambda x: float(x)) - group = f[depth_keys[0]] - - lat = group["latitudes"][:] - lon = group["longitudes"][:] - vp = group["vp"][:] # shape (nlat, nlon) - lon_grid, lat_grid = np.meshgrid(lon, lat) - - df = pd.DataFrame({ - "lat": lat_grid.ravel().astype(np.float32), - "lon": lon_grid.ravel().astype(np.float32), - "vp": vp.ravel().astype(np.float32), - "depth": float(depth_keys[0]) - }) - return df - - -def check_latlon_consistency(h5_file: str) -> None: - """ - Verify that all depth groups share identical latitude/longitude arrays. - - Parameters - ---------- - h5_file : str - Path to the HDF5 tomography file to check. - """ - with h5py.File(h5_file, "r") as f: - first_group = next(iter(f.keys())) - lat_ref = f[first_group]["latitudes"][:] - lon_ref = f[first_group]["longitudes"][:] - - all_good = True - for depth in f.keys(): - lat = f[depth]["latitudes"][:] - lon = f[depth]["longitudes"][:] - - if not (np.allclose(lat, lat_ref) and np.allclose(lon, lon_ref)): - print(f"Mismatch in lat/lon at depth: {depth}") - all_good = False - - if all_good: - print("✅ All latitudes and longitudes are consistent across depths.") - else: - print("❌ Inconsistencies found in lat/lon arrays.") - - -def check_grid_spacing(lat: np.ndarray, lon: np.ndarray, tol: float = 1e-6) -> None: - """ - Report unique spacings for lat/lon and whether they are regular. - Prints approximate km for spacings. - - Parameters - ---------- - lat : np.ndarray - Array of latitude values in degrees. - lon : np.ndarray - Array of longitude values in degrees. - tol : float, optional - Tolerance for determining regularity. Default is 1e-6. - """ - lat_spacing = np.diff(lat) - lon_spacing = np.diff(lon) - - unique_lat_spacings = np.unique(np.round(lat_spacing, decimals=5)) - unique_lon_spacings = np.unique(np.round(lon_spacing, decimals=5)) - - is_lat_regular = len(unique_lat_spacings) == 1 - is_lon_regular = len(unique_lon_spacings) == 1 - - # Latitude conversion (constant) - lat_km_spacing = np.round(unique_lat_spacings * 111.32, 3) - - # Longitude spacing in km varies with latitude - latitudes_for_conversion = np.linspace(lat.min(), lat.max(), num=1000) - lon_km_spacings = unique_lon_spacings[:, np.newaxis] * 111.32 * np.cos( - np.radians(latitudes_for_conversion) - ) - lon_km_min = np.min(lon_km_spacings, axis=1) - lon_km_max = np.max(lon_km_spacings, axis=1) - - print( - f"Latitude spacings (unique): {unique_lat_spacings} deg ≈ {lat_km_spacing} km | " - f"Regular: {is_lat_regular}" - ) - print( - f"Longitude spacings (unique): {unique_lon_spacings} deg ≈ " - f"[{lon_km_min.min():.3f} – {lon_km_max.max():.3f}] km | Regular: {is_lon_regular}" - ) - - -def lons_centered_for_display(lon: np.ndarray, center: float = 180.0) -> np.ndarray: - """ - Shift longitudes into a continuous range [center-180, center+180) - so there's a single seam at 'center' (default 180°). - - Parameters - ---------- - lon : np.ndarray - Array of longitude values. - center : float, optional - Center longitude for the display range. Default is 180.0. - - Returns - ------- - np.ndarray - Shifted longitude values. - """ - lon = np.asarray(lon, dtype=float) - # wrap to [-180, 180) after subtracting the center, then shift back - return ((lon - center + 180.0) % 360.0) - 180.0 + center - - -def lons_centered_180(lon: np.ndarray) -> np.ndarray: - """ - Convert arbitrary longitudes to the native range of - PlateCarree(central_longitude=180), i.e. [-180, 180). - - Parameters - ---------- - lon : np.ndarray - Array of longitude values. - - Returns - ------- - np.ndarray - Converted longitude values in range [-180, 180). - """ - lon = np.asarray(lon, dtype=float) - # shift by -180, wrap to [-180,180), done - return ((lon - 180.0 + 180.0) % 360.0) - 180.0 - - -def choose_projection_and_extent(lats: np.ndarray, lons: np.ndarray): - """ - Decide projection and compute a tight extent. - - Parameters - ---------- - lats : np.ndarray - Array of latitude values in degrees. - lons : np.ndarray - Array of longitude values in degrees. - - Returns - ------- - ax_crs : cartopy.crs.CRS - Axes projection for plotting. - data_crs : cartopy.crs.CRS - CRS of input data (always standard PlateCarree lon/lat). - extent : tuple - Extent as (minlon, maxlon, minlat, maxlat) in the CRS given by extent_crs. - extent_crs : cartopy.crs.CRS - CRS that 'extent' is expressed in. - """ - lats = np.asarray(lats, dtype=float) - lons = np.asarray(lons, dtype=float) - - data_crs = ccrs.PlateCarree() # input data CRS - - pad_lon = 0.5 - pad_lat = 0.5 - minlat = max(-90.0, float(lats.min()) - pad_lat) - maxlat = min( 90.0, float(lats.max()) + pad_lat) - - has_over_180 = np.nanmax(lons) > 180.0 - has_negative = np.nanmin(lons) < 0.0 - crosses_dl = has_over_180 or has_negative - - if crosses_dl: - # Use 180-centered axes; compute extent in that axes' native range [-180,180) - ax_crs = ccrs.PlateCarree(central_longitude=180) - lons_c = lons_centered_180(lons) # <<< key fix (NOT 0..360) - minlon = max(-180.0, float(lons_c.min()) - pad_lon) - maxlon = min( 180.0, float(lons_c.max()) + pad_lon) - extent = (minlon, maxlon, minlat, maxlat) - extent_crs = ax_crs # extent expressed in axes CRS - else: - # Standard case: compute extent in data CRS and pass with data_crs - ax_crs = ccrs.PlateCarree() - minlon = max(-180.0, float(lons.min()) - pad_lon) - maxlon = min( 180.0, float(lons.max()) + pad_lon) - extent = (minlon, maxlon, minlat, maxlat) - extent_crs = data_crs - - return ax_crs, data_crs, extent, extent_crs - - -def plot_spatial_distribution(df: pd.DataFrame, title_suffix: str = "") -> None: - """ - Map the grid points without clipping across the dateline. - Uses a 180°-centered axes when needed and sets extent in the matching CRS. - - Parameters - ---------- - df : pd.DataFrame - DataFrame containing latitude and longitude columns. - title_suffix : str, optional - Suffix to append to the plot title. Default is empty string. - """ - lats = df["lat"].values - lons = df["lon"].values - - ax_crs, data_crs, extent, extent_crs = choose_projection_and_extent(lats, lons) - - plt.figure(figsize=(10, 8)) - ax = plt.axes(projection=ax_crs) - - # Set extent in the CRS it was computed in - ax.set_extent(extent, crs=extent_crs) - - ax.add_feature(cfeature.COASTLINE, linewidth=0.8) - ax.add_feature(cfeature.BORDERS, linestyle=":", linewidth=0.6) - ax.add_feature(cfeature.LAND, facecolor="lightgray", edgecolor="none") - ax.add_feature(cfeature.OCEAN, facecolor="lightsteelblue", edgecolor="none") - ax.add_feature(cfeature.LAKES, alpha=0.5) - ax.add_feature(cfeature.RIVERS, linewidth=0.5) - - ax.scatter( - df["lon"], df["lat"], - s=1, c="red", alpha=0.5, - transform=data_crs, # input data CRS is always standard lon/lat - label="Model Grid" - ) - gl = ax.gridlines(draw_labels=True, linestyle="--", linewidth=0.5) - try: - gl.right_labels = False - gl.top_labels = False - except AttributeError: - pass - - plt.title(f"Spatial Domain of Tomography Model {title_suffix}") - plt.legend() - plt.tight_layout() - plt.show() - - -def plot_spacing_histograms(df: pd.DataFrame) -> None: - """ - Plot histograms of latitude and longitude spacing and density map. - - Parameters - ---------- - df : pd.DataFrame - DataFrame containing lat/lon data. - """ - lat_spacing = np.diff(np.sort(np.unique(df["lat"]))) - lon_spacing = np.diff(np.sort(np.unique(df["lon"]))) - - # Histogram: latitude spacing - plt.figure(figsize=(10, 4)) - sns.histplot(lat_spacing, bins=100, kde=True) - plt.title("Latitude Spacing Distribution") - plt.xlabel("Δlat (degrees)") - plt.ylabel("Count") - plt.tight_layout() - plt.show() - - # Histogram: longitude spacing - plt.figure(figsize=(10, 4)) - sns.histplot(lon_spacing, bins=100, kde=True) - plt.title("Longitude Spacing Distribution") - plt.xlabel("Δlon (degrees)") - plt.ylabel("Count") - plt.tight_layout() - plt.show() - - # Density map (data space) — shift longitudes so the seam is at 180° - lon_disp = lons_centered_for_display(df["lon"].values, center=180.0) - plt.figure(figsize=(8, 6)) - plt.hexbin(lon_disp, df["lat"], gridsize=100, cmap="Reds", mincnt=1) - plt.colorbar(label="Number of Points") - plt.title("Grid Point Density (centered at 180°)") - plt.xlabel("Longitude") - plt.ylabel("Latitude") - plt.tight_layout() - plt.show() - - -def plot_density_map(df: pd.DataFrame) -> None: - """ - Plot a density map of grid points. - - Parameters - ---------- - df : pd.DataFrame - DataFrame containing lat/lon data. - """ - lon_disp = lons_centered_for_display(df["lon"].values, center=180.0) - plt.figure(figsize=(10, 6)) - plt.hexbin(lon_disp, df["lat"], gridsize=100, cmap="Reds", mincnt=1) - plt.colorbar(label="Number of Points") - plt.title("Grid Point Density (centered at 180°)") - plt.xlabel("Longitude") - plt.ylabel("Latitude") - plt.tight_layout() - plt.show() - - -@app.command() -def analyze( - input_file: Annotated[ - Path, - typer.Argument( - exists=True, - dir_okay=False, - help="Input tomography file (.txt or .h5)" - ), - ], -) -> None: - """ - Analyze and plot tomography data from TXT or HDF5. - - Parameters - ---------- - input_file : Path - Input tomography file (.txt or .h5). - """ - ext = input_file.suffix.lower() - if ext == ".h5": - df = load_hdf5_data(str(input_file)) - check_latlon_consistency(str(input_file)) - suffix = "(HDF5)" - elif ext == ".txt": - df = load_txt_ep_format(str(input_file)) - suffix = "(TXT)" - else: - raise typer.BadParameter("Unsupported file type. Use .txt or .h5") - - print(f"Loaded {len(df)} points.") - print("Unique latitudes:", len(np.unique(df["lat"]))) - print("Unique longitudes:", len(np.unique(df["lon"]))) - - lat_vals = np.sort(np.unique(df["lat"])) - lon_vals = np.sort(np.unique(df["lon"])) - - print(f"Lat range: {df['lat'].min()} {df['lat'].max()}") - print(f"Lon range: {df['lon'].min()} {df['lon'].max()}") - - check_grid_spacing(lat_vals, lon_vals) - - # Map view that handles dateline properly - plot_spatial_distribution(df, suffix) - - # Spacing + data-space density - plot_spacing_histograms(df) - - # If a depth column exists, show surface slice density - if "depth" in df.columns: - df_surface = df[df["depth"] == df["depth"].min()] - if not df_surface.empty: - plot_density_map(df_surface) - - -if __name__ == "__main__": - app() diff --git a/tomography/tools/tomo_h5_downcast_to_f32.py b/tomography/tools/tomo_h5_downcast_to_f32.py deleted file mode 100644 index 8e80b8a..0000000 --- a/tomography/tools/tomo_h5_downcast_to_f32.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 -""" -Downcast HDF5 tomography data from float64 to float32 with compression. - -This script converts vp, vs, and rho datasets from float64 to float32 -while recompressing the entire file with gzip compression and shuffle filter. -""" - -from pathlib import Path -from typing import Annotated - -import h5py -import numpy as np -import typer - -app = typer.Typer(pretty_exceptions_enable=False) - -TARGETS = {"vp", "vs", "rho"} # downcast only these - - -def copy_attrs(src: h5py.Group | h5py.Dataset, dst: h5py.Group | h5py.Dataset) -> None: - """ - Copy all attributes from source to destination object. - - Parameters - ---------- - src : h5py.Group or h5py.Dataset - Source HDF5 object. - dst : h5py.Group or h5py.Dataset - Destination HDF5 object. - """ - for k, v in src.attrs.items(): - dst.attrs[k] = v - - -def chunk_guess(shape: tuple) -> tuple | bool: - """ - Guess appropriate chunk size for HDF5 dataset. - - Parameters - ---------- - shape : tuple - Shape of the dataset. - - Returns - ------- - tuple or True - Chunk shape for 2D datasets, True for automatic chunking for 1D. - """ - # Good default for your 2D (1400x800) datasets; let h5py decide for 1D. - if len(shape) == 2: - return (256, 256) - return True - - -def downcast_hdf5(inp: str, outp: str, gzip_level: int = 4) -> None: - """ - Convert HDF5 file with float64 to float32 downcast and recompression. - - Parameters - ---------- - inp : str - Input HDF5 file path. - outp : str - Output HDF5 file path. - gzip_level : int, optional - Gzip compression level (1-9). Default is 4. - """ - with h5py.File(inp, "r") as fin, h5py.File(outp, "w") as fout: - copy_attrs(fin, fout) - - def visit(name: str, obj: h5py.Group | h5py.Dataset) -> None: - if isinstance(obj, h5py.Group): - if name != "/": - g = fout.require_group(name) - copy_attrs(obj, g) - elif isinstance(obj, h5py.Dataset): - base = name.split("/")[-1] - data = obj[()] # load to memory (fast path for recompress + retype) - dtype = obj.dtype - if ( - base in TARGETS - and np.issubdtype(dtype, np.floating) - and dtype != np.float32 - ): - data = data.astype(np.float32, copy=False) - dtype = np.float32 - - ds = fout.create_dataset( - name, - data=data, - compression="gzip", - compression_opts=gzip_level, - shuffle=True, - chunks=chunk_guess(obj.shape), - ) - copy_attrs(obj, ds) - - fin.visititems(lambda n, o: visit(n, o)) - - -@app.command() -def main( - input_file: Annotated[ - Path, - typer.Argument(exists=True, dir_okay=False, help="Input HDF5 file path"), - ], - output_file: Annotated[ - Path, - typer.Argument(dir_okay=False, help="Output HDF5 file path"), - ], - gzip: Annotated[ - int, - typer.Option(min=1, max=9, help="Gzip compression level (1-9)"), - ] = 4, -) -> None: - """ - Downcast vp/vs/rho to float32 and recompress with gzip+shuffle. - - Parameters - ---------- - input_file : Path - Input HDF5 file path. - output_file : Path - Output HDF5 file path. - gzip : int, optional - Gzip compression level (1-9). Default is 4. - """ - downcast_hdf5(str(input_file), str(output_file), gzip_level=gzip) - - -if __name__ == "__main__": - app() diff --git a/tomography/tools/tomo_in2h5.py b/tomography/tools/tomo_in2h5.py deleted file mode 100644 index 01a4867..0000000 --- a/tomography/tools/tomo_in2h5.py +++ /dev/null @@ -1,275 +0,0 @@ -""" -Convert ASCII tomography files to HDF5 format (with dtype/compression options). - -This script converts ASCII tomography files to HDF5 format. The input directory contains -files with names like "surf_tomography_vp_elev0p25.in", "surf_tomography_vs_elev0p25.in" etc, where -"vp", "vs", and "rho" are the velocity types and "0p25" is the elevation. The script reads the -elevation values from the file names and ensures that they match across all velocity types. The -output HDF5 file will contain groups for each elevation, with datasets for latitudes, longitudes, -and the velocity types. - -Example usage: -python tomo_in2h5.py /path/to/tomography_files output_name --out-dir /path/to/output_dir --dtype f32 --gzip 6 - -This will convert the tomography files in /path/to/tomography_files to a file named output_name.h5 -in the same directory. If --out-dir is specified, the output file will be saved in that directory -instead. -""" - -import re -from datetime import datetime -from pathlib import Path -from typing import Annotated - -import h5py -import numpy as np -import typer - -app = typer.Typer(pretty_exceptions_enable=False) - - -def get_elevations_from_files(input_dir: Path) -> set[float]: - """ - Extract unique elevation values from file names in input_dir. - - Parameters - ---------- - input_dir : Path - Path to directory containing tomography files. - - Returns - ------- - set[float] - Set of unique elevation values found in file names. - - Raises - ------ - ValueError - If no elevation files are found for any velocity type, or if elevations do not match - across all velocity types. - - """ - vtypes = ["vp", "vs", "rho"] - elev_pattern = re.compile(r"surf_tomography_(vp|vs|rho)_elev(-?\d+(?:p\d+)?)\.in") - elevations_by_type: dict[str, set[float]] = {v: set() for v in vtypes} - for filename in input_dir.glob("surf_tomography_*.in"): - match = elev_pattern.match(filename.name) - if match: - vtype, elev_str = match.groups() - elev = float(elev_str.replace("p", ".")) - elevations_by_type[vtype].add(elev) - print(f"Found file: {filename.name}, vtype: {vtype}, elev: {elev}") - if ( - not elevations_by_type["vp"] - or not elevations_by_type["vs"] - or not elevations_by_type["rho"] - ): - missing = [vtype for vtype, elevs in elevations_by_type.items() if not elevs] - raise ValueError( - f"No elevation files found for {', '.join(missing)} in {input_dir}" - ) - if ( - elevations_by_type["vp"] != elevations_by_type["vs"] - or elevations_by_type["vp"] != elevations_by_type["rho"] - ): - raise ValueError( - "Elevations do not match across vp, vs, and rho:\n" - f"vp: {sorted(elevations_by_type['vp'])}\n" - f"vs: {sorted(elevations_by_type['vs'])}\n" - f"rho: {sorted(elevations_by_type['rho'])}" - ) - return elevations_by_type["vp"] - - -def convert_ascii_to_hdf5( - input_dir: Path, - name: str, - out_dir: Path | None = None, - dtype_opt: str = "f64", # "f64" or "f32" for vp/vs/rho - gzip_level: int = 4, # 1..9 - shuffle: bool = True, -): - """ - Convert ASCII tomography files to HDF5 format. - - Parameters - ---------- - input_dir : Path - Path to directory containing ASCII tomography files - name : str - Name for the output HDF5 file (without extension) - - out_dir : Path, optional - Output directory for the HDF5 file (if unspecified, saves to input_dir/name.h5) - dtype_opt : str, optional - Data dtype for vp/vs/rho: "f64" (default) or "f32". - gzip_level : int, optional - Gzip compression level 1..9 (default 4). - shuffle : bool, optional - Enable shuffle filter (default is True). - - """ - - vtypes = ["vp", "vs", "rho"] - elevations = sorted(get_elevations_from_files(input_dir)) - if not elevations: - raise ValueError(f"No valid tomography files found in {input_dir}") - input_path = Path(input_dir) - - # Output path - if out_dir is None: - output_path = input_path / f"{name}.h5" - else: - Path(out_dir).mkdir(parents=True, exist_ok=True) - output_path = Path(out_dir) / f"{name}.h5" - - # Dtype policy - data_dtype = np.float64 if dtype_opt.lower() == "f64" else np.float32 - coord_dtype = np.float64 # keep coords precise - - with h5py.File(output_path, "w") as h5f: - # file-level metadata to advertise decisions - h5f.attrs["created"] = datetime.utcnow().isoformat() + "Z" - h5f.attrs["generator"] = "tomo_in2h5.py" - h5f.attrs["schema"] = "NZTomographyLevelStacked v1" - h5f.attrs["data_dtype_vp_vs_rho"] = ( - "float32" if data_dtype == np.float32 else "float64" - ) - h5f.attrs["coord_dtype_lat_lon"] = "float64" - h5f.attrs["compression"] = f"gzip:{gzip_level}" - h5f.attrs["shuffle"] = bool(shuffle) - - for elev in elevations: - elev_file_str = ( - str(int(elev)) if elev == int(elev) else f"{elev:.2f}".replace(".", "p") - ) - elev_group_name = str(int(elev)) if elev == int(elev) else f"{elev:.2f}" - print( - f"Processing elevation {elev_group_name} (files suffix: {elev_file_str})" - ) - g = h5f.create_group(elev_group_name) - - # Load coords from rho file - ref_file = input_path / f"surf_tomography_rho_elev{elev_file_str}.in" - if not ref_file.exists(): - print(f"Warning: missing {ref_file}, skipping elevation {elev}") - continue - - # Read header and coordinates - with open(ref_file, "r") as f: - nlat, nlon = map(int, f.readline().split()) - latitudes = np.array( - [float(x) for x in f.readline().split()], dtype=coord_dtype - ) - longitudes = np.array( - [float(x) for x in f.readline().split()], dtype=coord_dtype - ) - - # coords - let h5py auto-chunk 1D arrays - g.create_dataset( - "latitudes", - data=latitudes, - compression="gzip", - compression_opts=gzip_level, - shuffle=shuffle, - ) - g.create_dataset( - "longitudes", - data=longitudes, - compression="gzip", - compression_opts=gzip_level, - shuffle=shuffle, - ) - - # fields - for vtype in vtypes: - filename = ( - input_path / f"surf_tomography_{vtype}_elev{elev_file_str}.in" - ) - if not filename.exists(): - print(f"Warning: missing {filename}") - continue - - # Use genfromtxt to skip header lines and load data directly - arr = np.genfromtxt(filename, skip_header=3, dtype=data_dtype) - arr = arr.reshape(nlat, nlon) - - dset = g.create_dataset( - vtype, - data=arr, - compression="gzip", - compression_opts=gzip_level, - shuffle=shuffle, - chunks=True, - ) - dset.attrs["units"] = ( - "km/s" if vtype in ("vp", "vs") else "g/cm^3" - ) # adjust if needed - dset.attrs["dtype"] = ( - "float32" if data_dtype == np.float32 else "float64" - ) - - print(f"Wrote {vtype} at elevation {elev_group_name} to {output_path}") - print(f"Done: {output_path}") - - -@app.command() -def convert_tomo_to_h5( - input_dir: Annotated[ - Path, - typer.Argument( - exists=True, - file_okay=False, - help="Input directory containing ASCII tomography files", - ), - ], - name: Annotated[ - str, - typer.Argument(help="Base name for the output HDF5 file (without extension)"), - ], - out_dir: Annotated[ - Path | None, - typer.Option(file_okay=False, help="Output directory for the HDF5 file"), - ] = None, - dtype: Annotated[ - str, typer.Option(help="Data dtype for vp/vs/rho: f32 (default) or f64") - ] = "f32", - gzip: Annotated[int, typer.Option(help="gzip level 1..9 (default 4)")] = 4, - shuffle: Annotated[bool, typer.Option(help="Enable shuffle filter")] = True, -): - """ - Convert ASCII tomography files to HDF5 format. - - This tool consolidates multiple ASCII tomography files into a single, - structured HDF5 file. It handles files with various elevation values - and velocity parameters (vp, vs, rho) and ensures they are consistent. - - Parameters - ---------- - input_dir : Path - Path to directory containing ASCII tomography files. - name : str - Name for the output HDF5 file (without extension). - out_dir : Path, optional - Output directory for the HDF5 file (if unspecified, saves to input_dir/name.h5). - dtype : str, optional - Data dtype for vp/vs/rho: "f64" (default) or "f32". - gzip : int, optional - Gzip compression level 1..9 (default 4). - shuffle : bool, optional - Enable shuffle filter (default is True). - - """ - - convert_ascii_to_hdf5( - input_dir, - name, - out_dir, - dtype_opt=dtype, - gzip_level=gzip, - shuffle=shuffle, - ) - - -if __name__ == "__main__": - app() diff --git a/tomography/tools/visualisation/tomo_3dviewer.py b/tomography/tools/visualisation/tomo_3dviewer.py deleted file mode 100644 index dbe6319..0000000 --- a/tomography/tools/visualisation/tomo_3dviewer.py +++ /dev/null @@ -1,561 +0,0 @@ -""" -3D Tomography Viewer for HDF5-based velocity models. - -This script provides an interactive 3D visualization of tomography slices -stored in an HDF5 file. It uses PyVista and PyQt for the user interface -and rendering. Users can toggle the visibility of different elevation layers, -overlay original data points from a text file, and inspect the model from -various angles. - -The script is run from the command line and accepts various options to -customize the visualization, such as selecting the scalar field, defining -latitude/longitude ranges, and setting color map limits. -""" - -import sys -from pathlib import Path -from typing import Annotated, Optional - -import h5py -import numpy as np -import pandas as pd -import pyvista as pv -import typer -from pyvista.plotting.camera import Camera -from pyvistaqt import QtInteractor -from qtpy.QtCore import Qt -from qtpy.QtWidgets import ( - QApplication, - QCheckBox, - QDockWidget, - QLabel, - QMainWindow, - QScrollArea, - QVBoxLayout, - QWidget, -) - -from qcore import cli - -app = typer.Typer(pretty_exceptions_enable=False) - -DEFAULT_CAMERA_ANGLE = (-0.018774705983602088, 0.8058896333404005, 0.5917680367252902) - - -def is_number(s: str) -> bool: - """Check if a string can be converted to a float. - - Parameters - ---------- - s : str - The input string. - - Returns - ------- - bool - True if the string can be converted to a float, False otherwise. - """ - try: - float(s) - return True - except ValueError: - return False - - -def read_ep_txt(txt_path: Path) -> pd.DataFrame: - """Read an EP-style tomography text file into a pandas DataFrame. - - The file is expected to have a specific format with columns for velocity - parameters and coordinates. This function handles file parsing, column - naming, and coordinate adjustments (elevation sign and longitude range). - - Parameters - ---------- - txt_path : Path - Path to the input text file. - - Returns - ------- - pd.DataFrame - A DataFrame containing the tomography data with standardized column names. - """ - col_names = [ - "vp", - "vp_o_vs", - "vs", - "rho", - "sf_vp", - "sf_vp_o_vs", - "x", - "y", - "elevation", - "lat", - "lon", - ] - df = pd.read_csv(txt_path, sep=r"\s+", skiprows=2, names=col_names, engine="python") - df["elevation"] = -1 * df["elevation"] - df["lon"] = np.where(df["lon"] < 0, df["lon"] + 360, df["lon"]) - return df - - -def load_stacked_slices( - h5_path: Path, - scalar: str = "vp", - lat_range: Optional[tuple[float, float]] = None, - lon_range: Optional[tuple[float, float]] = None, -) -> tuple[list[str], np.ndarray, np.ndarray, np.ndarray]: - """Load and stack tomography slices from an HDF5 file. - - Reads data for specified elevation layers from an HDF5 file, optionally - cropping it to a given latitude and longitude range. - - Parameters - ---------- - h5_path : Path - Path to the HDF5 file. - scalar : str, optional - The scalar field to load (e.g., "vp", "vs"), by default "vp". - lat_range : tuple of float, optional - The latitude range (min, max) to crop the data. If None, all latitudes - are used. By default None. - lon_range : tuple of float, optional - The longitude range (min, max) to crop the data. If None, all latitudes - are used. By default None. - - Returns - ------- - tuple - A tuple containing: - - keys (list[str]): Sorted list of elevation keys. - - lon (np.ndarray): Array of longitudes within the specified range. - - lat (np.ndarray): Array of latitudes within the specified range. - - scalar_values np.ndarray: A 3d array containing scalar data of shape (n_elevations, n_latitudes, n_longitudes). - """ - with h5py.File(h5_path, "r") as f: - keys = sorted((k for k in f.keys() if is_number(k)), key=float, reverse=True) - sample = f[keys[0]] - lat_full = sample["latitudes"][:] - lon_full = sample["longitudes"][:] - - lat_mask = ( - (lat_full >= lat_range[0]) & (lat_full <= lat_range[1]) - if lat_range - else np.ones_like(lat_full, bool) - ) - lon_mask = ( - (lon_full >= lon_range[0]) & (lon_full <= lon_range[1]) - if lon_range - else np.ones_like(lon_full, bool) - ) - - lat = lat_full[lat_mask] - lon = lon_full[lon_mask] - - scalar_values = [] - for k in keys: - data = f[k][scalar][:][np.ix_(lat_mask, lon_mask)] - scalar_values.append(data) - return keys, lon, lat, np.array(scalar_values) - - -def make_flat_surfaces( - elevations: list[str], - lon: np.ndarray, - lat: np.ndarray, - scalar_values: np.ndarray, - gap: float = 0.1, -) -> list[tuple[pv.StructuredGrid, float, float, float]]: - """Create a list of PyVista surfaces from stacked 2D data. - - Each slice in the data cube is converted into a flat surface mesh, - vertically stacked with a specified gap. - - Parameters - ---------- - elevations : list[str] - List of elevation values for each slice. - lon : np.ndarray - 1D array of longitudes. - lat : np.ndarray - 1D array of latitudes. - scalar_values : np.ndarray - 3D data array with shape (elevation, lat, lon). - gap : float, optional - Vertical gap between the stacked surfaces, by default 0.1. - - - Returns - ------- - tuple - A tuple containing: - - grids (dict): A dictionary mapping elevations to their corresponding PyVista surface meshes. - Each key is a float elevation value, and the value is a PyVista StructuredGrid object. - - global_min (float): The minimum value in the entire data cube, used for consistent color mapping. - - global_max (float): The maximum value in the entire data cube, used for consistent color mapping. - - """ - - grid_dict = {} - global_min = np.min(scalar_values) - global_max = np.max(scalar_values) - - for iz in range(len(elevations)): - z = -gap * iz - xs, ys = np.meshgrid(lon, lat, indexing="xy") - zs = np.full_like(xs, z) - surf = pv.StructuredGrid(xs, ys, zs).extract_surface() - surf["values"] = scalar_values[iz].ravel(order="C") - grid_dict[float(elevations[iz])] = surf - - return grid_dict, global_min, global_max - - -class TomoApp(QMainWindow): - """Main application window for the 3D tomography viewer. - - This class encapsulates the Qt-based GUI, including the PyVista plotter - and layer visibility controls. - - - Parameters - ---------- - title : str - Title for the window. - scalar_name : str - Name of the scalar field being displayed. - grid_dict : dict[pv.StructuredGrid] - Dictionary mapping elevations to PyVista StructuredGrid objects. - clim : tuple, optional - Color map limits (min, max) to override the defaults. By default None. - points_by_elevation : dict, optional - Dictionary mapping elevations to point data to be overlaid. By default None. - debug : bool, optional - Flag to enable debug printing. By default False. - - """ - - def __init__( - self, - title: str, - scalar_name: str, - grid_dict: dict[pv.StructuredGrid], - clim: tuple[float, float], - points_by_elevation: Optional[dict[float, np.ndarray]] = None, - debug: bool = False, - ): - """Initialize the Tomography 3D Viewer application.""" - - super().__init__() - self.debug = debug - - # Find the topmost grid (highest elevation) - top_grid = grid_dict[max(grid_dict.keys())] - # Dynamically determine focal point from the center of the top grid - self.focal_point = top_grid.center - - # Store camera settings relative to the focal point - self.camera_position = ( - self.focal_point[0] - 1.63, - self.focal_point[1] - 46.57, - self.focal_point[2] + 63.37, - ) - self.view_up = DEFAULT_CAMERA_ANGLE - - self.plotter_widget = QtInteractor(self) - - self.setWindowTitle(f"Tomography 3D Viewer - {title}") - - title_label = QLabel(f"{title}- {scalar_name}") - title_label.setAlignment(Qt.AlignCenter) - title_label.setStyleSheet("font-weight: bold; font-size: 14px;") - - main_layout = QVBoxLayout() - main_layout.addWidget(title_label) - main_layout.addWidget(self.plotter_widget) - central_widget = QWidget() - central_widget.setLayout(main_layout) - self.setCentralWidget(central_widget) - self.actors = [] - self.slice_actor = None - self.point_actors = [] - self.points_by_elevation = points_by_elevation - - self.toggle_panel = QWidget() - layout = QVBoxLayout(self.toggle_panel) - # layout.addWidget(QLabel(self.windowTitle())) - layout.addWidget(QLabel("Layer Visibility")) - - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setWidget(self.toggle_panel) - - dock = QDockWidget("Layers", self) - dock.setWidget(scroll) - dock.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) - self.addDockWidget(Qt.LeftDockWidgetArea, dock) - - for i, elevation in enumerate( - sorted(grid_dict.keys())[::-1] - ): # reverse order for top-down view - grid = grid_dict[elevation] - clim = clim - actor = self.plotter_widget.add_mesh( - grid, - scalars="values", - cmap="hot_r", - opacity=1.0, - clim=clim, - show_scalar_bar=False, - name=f"{elevation}km", - lighting=False, - ) - self.actors.append(actor) - - cb = QCheckBox(f"{elevation:.0f} km") - cb.setChecked(True) - - # Add optional point overlay for this elevation if provided - if self.points_by_elevation and elevation in self.points_by_elevation: - z = -i * 0.1 + 0.01 # slightly above the surface - pts = self.points_by_elevation[elevation] - pdata = pv.PolyData( - np.column_stack((pts[:, 1], pts[:, 0], np.full(len(pts), z))) - ) - pdata["values"] = pts[:, 2] if pts.shape[1] > 2 else np.zeros(len(pts)) - if self.debug: - print(pdata["values"]) - - p_actor = self.plotter_widget.add_mesh( - pdata, - scalars="values", - cmap="hot_r", - point_size=4, - render_points_as_spheres=True, - show_scalar_bar=False, - clim=clim, - ) - self.point_actors.append((elevation, p_actor)) - if elevation == max(self.points_by_elevation): - p_actor.SetVisibility(True) - else: - p_actor.SetVisibility(False) - - cb.stateChanged.connect( - lambda state, a=actor: self.set_visibility(a, state) - ) - layout.addWidget(cb) - - self.plotter_widget.add_scalar_bar(title=scalar_name.upper(), n_labels=5) - self.plotter_widget.add_axes( - xlabel="Longitude (°E)", - ylabel="Latitude (°N)", - zlabel="Elevation", - label_size=(0.4, 0.4), - ) - self.plotter_widget.camera_position = "iso" - self.plotter_widget.set_scale(xscale=1, yscale=1, zscale=0.05) - self.plotter_widget.renderer.interpolate_before_map = True - - # 🧭 Set custom camera view - self.reset_camera() - - self.plotter_widget.add_key_event("v", self.toggle_all) - self.plotter_widget.add_key_event("s", self.toggle_slice) - self.plotter_widget.add_key_event("r", self.reset_camera) - - self.plotter_widget.renderer.GetActiveCamera().AddObserver( - "ModifiedEvent", self.on_camera_modified - ) - - def set_topmost_point_visibility(self) -> None: - """Control visibility of point overlays. - - Ensures that only the points corresponding to the topmost visible - elevation layer are displayed. - """ - if not self.points_by_elevation: - return - checkboxes = self.toggle_panel.findChildren(QCheckBox) - visible_elevations = [ - float(cb.text().split()[0]) for cb in checkboxes if cb.isChecked() - ] - if self.debug: - print(visible_elevations) - if visible_elevations: - top = max(visible_elevations) # - for d, a in self.point_actors: - is_visible = d == top - a.SetVisibility(is_visible) - if is_visible: - if self.debug: - print(f"👁️ Top visible elevation: {top} km") - pdata = a.GetMapper().GetInputAsDataSet() - if "values" in pdata.point_data: - if self.debug: - print("📊 Scalar values:", pdata["values"]) - else: - if self.debug: - print("⚠️ 'values' not found in point data") - - def set_visibility(self, actor: pv.Actor, state: int) -> None: - """Set the visibility of a layer and update point overlays. - - This is a slot connected to the layer visibility checkboxes. - - Parameters - ---------- - actor : pv.Actor - The main surface actor for the layer. - - state : int - The state of the checkbox (Qt.Checked or Qt.Unchecked). - - """ - actor.SetVisibility(state == Qt.Checked) - self.set_topmost_point_visibility() - self.plotter_widget.render() - - def toggle_all(self) -> None: - """Toggle the visibility of all layers.""" - for actor in self.actors: - actor.SetVisibility(not actor.GetVisibility()) - self.plotter_widget.render() - - def toggle_slice(self) -> None: - """Toggle a cross-section slice view.""" - if self.slice_actor: - self.plotter_widget.remove_actor(self.slice_actor) - self.slice_actor = None - else: - grid = self.actors[len(self.actors) // 2].GetMapper().GetInputAsDataSet() - bounds = grid.bounds - x_center = 0.5 * (bounds[0] + bounds[1]) - sliced = grid.slice(normal="x", origin=(x_center, 0, 0)) - self.slice_actor = self.plotter_widget.add_mesh( - sliced, color="white", line_width=2 - ) - self.plotter_widget.render() - - def reset_camera(self) -> None: - """Reset the camera to a predefined position and orientation.""" - camera = self.plotter_widget.renderer.GetActiveCamera() - camera.SetPosition(self.camera_position) - camera.SetFocalPoint(self.focal_point) - camera.SetViewUp(self.view_up) - self.plotter_widget.renderer.ResetCameraClippingRange() - self.plotter_widget.render() - - def on_camera_modified(self, caller: Camera, event: str) -> None: - """Print camera parameters when modified (for debugging). - - Parameters - ---------- - caller : Camera - The camera object that triggered the event. - event : str - The event name (e.g., "ModifiedEvent"). - """ - if not self.debug: - return - - cam = self.plotter_widget.renderer.GetActiveCamera() - print("📸 Camera changed:") - print(" Position :", cam.GetPosition()) - print(" FocalPoint :", cam.GetFocalPoint()) - print(" ViewUp :", cam.GetViewUp()) - - -@cli.from_docstring(app) -def launch_viewer( - h5file: Annotated[Path, typer.Argument(help="HDF5 file with tomography slices")], - scalar: Annotated[ - str, typer.Option(help="Scalar field to show", show_default=True) - ] = "vp", - gap: Annotated[float, typer.Option(help="Vertical gap between slices")] = 0.2, - lat_range: Annotated[ - Optional[tuple[float, float]], typer.Option(help="Latitude range [min max]") - ] = None, - lon_range: Annotated[ - Optional[tuple[float, float]], typer.Option(help="Longitude range [min max]") - ] = None, - clim: Annotated[ - Optional[tuple[float, float]], typer.Option(help="Color range [min max]") - ] = None, - txt: Annotated[ - Optional[Path], - typer.Option(help="Optional EP-style TXT input file for original points"), - ] = None, - debug: Annotated[bool, typer.Option(help="Enable debug print statements")] = False, -) -> None: - """Launch the interactive tomography viewer with optional overlays and controls. - - This function serves as the main entry point for the command-line application. - It parses arguments, loads data, and initializes the Qt application and - the TomoApp viewer window. - - Parameters - ---------- - h5file : Path - Path to the HDF5 file with tomography slices. - scalar : str, optional - Scalar field to show (e.g., "vp", "vs"), by default "vp". - gap : float, optional - Vertical gap between slices, by default 0.2. - lat_range : tuple of float, optional - Latitude range (min, max) to display, by default None. - lon_range : tuple of float, optional - Longitude range (min, max) to display, by default None. - clim : tuple of float, optional - Color range (min, max) for the scalar bar, by default None. - txt : Path, optional - Optional EP-style TXT input file for original points overlay, by default None. - debug : bool, optional - Enable debug print statements, by default False. - """ - elevations, lon, lat, scalar_values = load_stacked_slices( - h5file, scalar=scalar, lat_range=lat_range, lon_range=lon_range - ) - grid_dict, vmin, vmax = make_flat_surfaces( - elevations, lon, lat, scalar_values, gap=gap - ) - - points_by_elevation = None - if txt: - df = read_ep_txt(txt) - df_filtered = df[np.any(np.isclose(df['elevation'].values[:, None], [float(d) for d in elevations], atol=1e-3), axis=1)] - if debug: - print(df_filtered.head()) - points_by_elevation = { - float(d): df_filtered[ - np.isclose(df_filtered["elevation"], float(d), atol=1e-3) - ][["lat", "lon", scalar]].values - for d in elevations - } - - app_qt = QApplication.instance() or QApplication(sys.argv) - - # validate clim input - if clim is None: - clim = (vmin, vmax) - else: - if len(clim) != 2 or not all(isinstance(c, (int, float)) for c in clim): - raise ValueError("clim must be a tuple of two numeric values (min, max).") - clim = tuple(clim) - - window = TomoApp( - h5file.stem, - scalar, - grid_dict, - clim, - points_by_elevation=points_by_elevation, - debug=debug, - ) - window.show() - # Ensure clean shutdown - app_qt.lastWindowClosed.connect(app_qt.quit) - - sys.exit(app_qt.exec()) - - -if __name__ == "__main__": - app() diff --git a/tomography/tools/visualisation/tomo_boundary.py b/tomography/tools/visualisation/tomo_boundary.py deleted file mode 100644 index 909b3a5..0000000 --- a/tomography/tools/visualisation/tomo_boundary.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -This script extracts the geographical boundary from a tomography HDF5 file -and saves it as a GeoJSON file. It computes the convex hull of the grid points -defined by the latitude and longitude arrays in the HDF5 file to determine -the boundary polygon. - -The script is intended to be run from the command line, taking the path to an -HDF5 file as input. -""" - -from pathlib import Path -from typing import Annotated - -import geojson -import h5py -import numpy as np -import typer -from shapely.geometry import MultiPoint - -from qcore import cli - -app = typer.Typer(pretty_exceptions_enable=False) - - -@cli.from_docstring(app) -def extract_single_boundary_geojson( - hdf5_file_path: Annotated[ - Path, - typer.Argument( - help="Path to the HDF5 file (e.g., 2020_NZ.h5)", - exists=True, - file_okay=True, - dir_okay=False, - resolve_path=True, - ), - ], -): - """Extracts the convex hull boundary from an HDF5 file and saves it as GeoJSON. - - This function reads latitude and longitude grids from the first group in the - HDF5 file, calculates the convex hull of the grid's edge points, and - writes the resulting polygon to a GeoJSON file. The output file will have - the same name as the input file but with a .geojson extension. - - Parameters - ---------- - hdf5_file_path : Path - The path to the input HDF5 file containing latitude and longitude grids. - - Raises - ------ - RuntimeError - If the convex hull operation does not result in a Polygon. - """ - with h5py.File(hdf5_file_path, "r") as f: - # Use the first available group to extract lat/lon - first_group_name = next(iter(f.keys())) - group = None - for key in f.keys(): - if 'latitudes' in f[key] and 'longitudes' in f[key]: - group = f[key] - break - if group is None: - raise RuntimeError("No group with 'latitudes' and 'longitudes' found in HDF5 file.") - - lats = group["latitudes"][:] - lons = group["longitudes"][:] - - # Construct edge points only - lon_grid, lat_grid = np.meshgrid(lons, lats) - edge_coords = [] - - # Top and bottom rows - edge_coords += list(zip(lon_grid[0, :], lat_grid[0, :])) # top - edge_coords += list(zip(lon_grid[-1, :], lat_grid[-1, :])) # bottom - - # Left and right columns (excluding corners) - edge_coords += list(zip(lon_grid[1:-1, 0], lat_grid[1:-1, 0])) # left - edge_coords += list(zip(lon_grid[1:-1, -1], lat_grid[1:-1, -1])) # right - - # Compute convex hull - hull = MultiPoint(edge_coords).convex_hull - - if hull.geom_type != "Polygon": - raise RuntimeError("Convex hull did not produce a polygon.") - - coords = [list(p) for p in hull.exterior.coords] - polygon = geojson.Polygon([coords]) - feature = geojson.Feature(geometry=polygon, properties={}) - feature_collection = geojson.FeatureCollection([feature]) - - output_path = hdf5_file_path.with_suffix(".geojson") - with open(output_path, "w") as geojson_file: - geojson.dump(feature_collection, geojson_file, indent=2) - - print(f"Boundary GeoJSON saved to: {output_path}") - - -if __name__ == "__main__": - app() From a14dfbd8f7e27f12097b1ae73b3de249438033a8 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Thu, 18 Dec 2025 13:40:49 +1300 Subject: [PATCH 26/44] Add tomography models for EP simulations --- tomography/EP2010/EP2010.h5 | 3 +++ tomography/EP2017/EP2017.h5 | 3 +++ tomography/EP2020/EP2020.h5 | 3 +++ tomography/EP2022/EP2022.h5 | 3 +++ tomography/EP2025/EP2025.h5 | 3 +++ 5 files changed, 15 insertions(+) create mode 100644 tomography/EP2010/EP2010.h5 create mode 100644 tomography/EP2017/EP2017.h5 create mode 100644 tomography/EP2020/EP2020.h5 create mode 100644 tomography/EP2022/EP2022.h5 create mode 100644 tomography/EP2025/EP2025.h5 diff --git a/tomography/EP2010/EP2010.h5 b/tomography/EP2010/EP2010.h5 new file mode 100644 index 0000000..b5835ad --- /dev/null +++ b/tomography/EP2010/EP2010.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f16409bb32d5ddbfbaa1e67b7718468acaa589d75a95830585f5143578e2ac1d +size 498347472 diff --git a/tomography/EP2017/EP2017.h5 b/tomography/EP2017/EP2017.h5 new file mode 100644 index 0000000..780dd4e --- /dev/null +++ b/tomography/EP2017/EP2017.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60a9d8558b8e59383229bc87ffabc7ce89563dc5f157cc791b99551fefd2fb81 +size 686263471 diff --git a/tomography/EP2020/EP2020.h5 b/tomography/EP2020/EP2020.h5 new file mode 100644 index 0000000..97abb1b --- /dev/null +++ b/tomography/EP2020/EP2020.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d22aa31d0b1100c821262e1565d989ea885f4829d03bb440de28893e2d0408d9 +size 691979068 diff --git a/tomography/EP2022/EP2022.h5 b/tomography/EP2022/EP2022.h5 new file mode 100644 index 0000000..379fbeb --- /dev/null +++ b/tomography/EP2022/EP2022.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3fb2d96fe6dfcf889bf9a0c63fa265f66b72a0795652a979d1ae2792b7619b5 +size 692517900 diff --git a/tomography/EP2025/EP2025.h5 b/tomography/EP2025/EP2025.h5 new file mode 100644 index 0000000..f18f410 --- /dev/null +++ b/tomography/EP2025/EP2025.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79cacdd2bd1f5cb04a65af4b4f8d5ee35e79b31d46adf0e33bbe993b752d00b3 +size 643563147 From 9eaaf033896937be4dfd4077bb0b3cad4acf700c Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 13:45:35 +1300 Subject: [PATCH 27/44] fix(registry): resolve merge artifacts in nzcvm_registry.yaml tomography section - Remove leftover conflict markers from EP2022 and EP2025 entries - Ensure consistent formatting for elevation arrays and paths - Clean up whitespace for improved readability --- nzcvm_registry.yaml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index 39bb29e..347d2db 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -69,7 +69,7 @@ tomography: author: Eberhart-Phillips et al. (2020) title: New Zealand Wide model 2.2 seismic velocity and Qs and Qp models for New Zealand url: https://zenodo.org/records/3779523 - + - name: EP2022 elev: [15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750] path: tomography/EP2022/EP2022.h5 @@ -320,16 +320,13 @@ tomography: -750, ] path: tomography/EP2022/EP2022_New.h5 ->>>>>>> d1a69463e70ec8effc4ee5a391ce6537eca23e1d author: Eberhart-Phillips et al. (2022) title: New Zealand Wide model 2.3 seismic velocity and Qs and Qp models for New Zealand url: https://zenodo.org/records/5098356 - name: EP2025 -<<<<<<< HEAD elev: [15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750] path: tomography/EP2025/EP2025.h5 -======= elev: [ 15, @@ -358,8 +355,6 @@ tomography: -620, -750, ] - path: tomography/EP2025/EP2025_New.h5 ->>>>>>> d1a69463e70ec8effc4ee5a391ce6537eca23e1d author: Eberhart-Phillips et al. (2025) title: New Zealand Wide model 3.1 seismic velocity and Qs and Qp models for New Zealand url: Personal communication (pending publication) From 5b8a294e5745ba46d919bcbbf00de23a2d72df0a Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 13:45:51 +1300 Subject: [PATCH 28/44] fix(registry): update EP2022 tomography model path to EP2022.h5 --- nzcvm_registry.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index 347d2db..ce685ad 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -319,7 +319,7 @@ tomography: -620, -750, ] - path: tomography/EP2022/EP2022_New.h5 + path: tomography/EP2022/EP2022.h5 author: Eberhart-Phillips et al. (2022) title: New Zealand Wide model 2.3 seismic velocity and Qs and Qp models for New Zealand url: https://zenodo.org/records/5098356 From e7d903cb6c00ab9a090bd412133e7d0d4d898e34 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 13:47:51 +1300 Subject: [PATCH 29/44] chore(registry): remove CHOW2020_EP2020_MIX model entry from registry --- nzcvm_registry.yaml | 185 -------------------------------------------- 1 file changed, 185 deletions(-) diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index ce685ad..6f10c5c 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -105,191 +105,6 @@ tomography: title: New Zealand Wide model 2.2 seismic velocity and Qs and Qp models for New Zealand url: https://zenodo.org/records/3779523 - - name: CHOW2020_EP2020_MIX - elev: - [ - 15.0, - 2.25, - 2.0, - 1.75, - 1.5, - 1.25, - 1.0, - 0.75, - 0.5, - 0.25, - 0.0, - -0.25, - -0.5, - -0.75, - -1.0, - -1.25, - -1.5, - -1.75, - -2.0, - -2.25, - -2.5, - -2.75, - -3.0, - -3.25, - -3.5, - -3.75, - -4.0, - -4.25, - -4.5, - -4.75, - -5.0, - -5.25, - -5.5, - -5.75, - -6.0, - -6.25, - -6.5, - -6.75, - -7.0, - -7.25, - -7.5, - -7.75, - -8.0, - -9.0, - -10.0, - -11.0, - -12.0, - -13.0, - -14.0, - -15.0, - -16.0, - -17.0, - -18.0, - -19.0, - -20.0, - -21.0, - -22.0, - -23.0, - -24.0, - -25.0, - -26.0, - -27.0, - -28.0, - -29.0, - -30.0, - -31.0, - -32.0, - -33.0, - -34.0, - -35.0, - -36.0, - -37.0, - -38.0, - -39.0, - -40.0, - -41.0, - -42.0, - -43.0, - -44.0, - -45.0, - -46.0, - -47.0, - -48.0, - -49.0, - -50.0, - -52.0, - -56.0, - -60.0, - -64.0, - -68.0, - -72.0, - -76.0, - -80.0, - -84.0, - -88.0, - -92.0, - -96.0, - -100.0, - -104.0, - -108.0, - -112.0, - -116.0, - -120.0, - -124.0, - -128.0, - -132.0, - -136.0, - -140.0, - -144.0, - -148.0, - -152.0, - -156.0, - -160.0, - -164.0, - -168.0, - -172.0, - -176.0, - -180.0, - -184.0, - -188.0, - -192.0, - -196.0, - -200.0, - -204.0, - -208.0, - -212.0, - -216.0, - -220.0, - -224.0, - -228.0, - -232.0, - -236.0, - -240.0, - -244.0, - -248.0, - -252.0, - -256.0, - -260.0, - -264.0, - -268.0, - -272.0, - -276.0, - -280.0, - -284.0, - -288.0, - -292.0, - -296.0, - -300.0, - -304.0, - -308.0, - -312.0, - -316.0, - -320.0, - -324.0, - -328.0, - -332.0, - -336.0, - -340.0, - -344.0, - -348.0, - -352.0, - -356.0, - -360.0, - -364.0, - -368.0, - -372.0, - -376.0, - -380.0, - -384.0, - -388.0, - -392.0, - -396.0, - -400.0, - -620.0, - -750.0, - ] - path: tomography/CHOW2020_EP2020_MIX/chow2020_ep2020_mix.h5 - author: Chow et al. (2020) - url: - - https://doi.org/10.1093/gji/ggaa381 - - https://core.geo.vuw.ac.nz/d/feae69f61ea54f81bee1 - - name: EP2022 elev: [ From b1611b71b9435c9d39388f3dc4f99346b665d279 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 13:49:25 +1300 Subject: [PATCH 30/44] build: require Python 3.11 or higher in pyproject.toml --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 223a1da..91ed9f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [project] name = "nzcvm_data" +requires-python = ">=3.11" version = "0.1.0" description = "Integrity checks for NZCVM data registry" dependencies = ["numpy>=2.3.5", "pytest-subtests>=0.15.0", "schema>=0.7.8"] From 209c25d713c107982f4dd08fe1c20a628a1354f8 Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Thu, 18 Dec 2025 14:08:01 +1300 Subject: [PATCH 31/44] test: improve boundary geometry assertions in basin smoothing test - Check now asserts the basin *boundary* contains the smoothing boundary, instead of the basin *polygon* (which includes the interior). --- tests/test_registry.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_registry.py b/tests/test_registry.py index 4d6942a..a5fe59a 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -424,7 +424,10 @@ def test_basin_smoothing_contained_in_boundaries( boundary_path = nzcvm_root / boundary_relative_path geojson_str = boundary_path.read_text() geom_collection = shapely.from_geojson(geojson_str) - boundaries.append(geom_collection) + assert isinstance(geom_collection, shapely.GeometryCollection) + for geom in geom_collection.geoms: + assert isinstance(geom, shapely.Polygon) + boundaries.append(geom.exterior) # Add a small smoothing boundary buffer to account for the fact # that smoothing boundary is not perfectly contained in the basin From dd82647ad51a19bb243ab5119b6a4526ff11402f Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Thu, 18 Dec 2025 14:27:32 +1300 Subject: [PATCH 32/44] Removed the top right smoothing --- regional/Dunedin/Dunedin_smoothing.txt | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/regional/Dunedin/Dunedin_smoothing.txt b/regional/Dunedin/Dunedin_smoothing.txt index 9499c95..9faae13 100755 --- a/regional/Dunedin/Dunedin_smoothing.txt +++ b/regional/Dunedin/Dunedin_smoothing.txt @@ -131,21 +131,3 @@ 170.45759988 -45.92441961 170.45719142 -45.92356637 170.45689042 -45.92269490 -170.56320143 -45.86537325 -170.56356933 -45.86624843 -170.56393725 -45.86712360 -170.56430518 -45.86799877 -170.56467313 -45.86887395 -170.56504108 -45.86974911 -170.56540905 -45.87062428 -170.56577703 -45.87149945 -170.56614502 -45.87237461 -170.56651302 -45.87324977 -170.56688103 -45.87412493 -170.56724906 -45.87500009 -170.56761709 -45.87587525 -170.56798514 -45.87675041 -170.56835320 -45.87762556 -170.56872127 -45.87850071 -170.56908935 -45.87937587 -170.56945745 -45.88025101 From 6a27cd3c7478981538dbdf8bd16f0662cf700a63 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Thu, 18 Dec 2025 19:10:40 +1300 Subject: [PATCH 33/44] Updating registry formatting --- nzcvm_registry.yaml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index 6f10c5c..b48022f 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -64,15 +64,7 @@ tomography: url: https://zenodo.org/record/1043558 - name: EP2020 - elev: [ 15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] path: tomography/EP2020/EP2020.h5 - author: Eberhart-Phillips et al. (2020) - title: New Zealand Wide model 2.2 seismic velocity and Qs and Qp models for New Zealand - url: https://zenodo.org/records/3779523 - - - name: EP2022 - elev: [15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750] - path: tomography/EP2022/EP2022.h5 elev: [ 15, @@ -106,6 +98,7 @@ tomography: url: https://zenodo.org/records/3779523 - name: EP2022 + path: tomography/EP2022/EP2022.h5 elev: [ 15, @@ -134,13 +127,11 @@ tomography: -620, -750, ] - path: tomography/EP2022/EP2022.h5 author: Eberhart-Phillips et al. (2022) title: New Zealand Wide model 2.3 seismic velocity and Qs and Qp models for New Zealand url: https://zenodo.org/records/5098356 - name: EP2025 - elev: [15, 1, -1, -3, -5, -8, -15, -23, -30, -34, -38, -42, -48, -55, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750] path: tomography/EP2025/EP2025.h5 elev: [ From 9821247574b13adff3f3354a485151463e5f13f4 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Thu, 18 Dec 2025 19:12:19 +1300 Subject: [PATCH 34/44] Updating registry formatting --- nzcvm_registry.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index b48022f..c475647 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -27,7 +27,7 @@ tomography: ] author: Eberhart-Phillips et al. (2010) title: Establishing a Versatile 3-d seismic Velocity Model for New Zealand - url: https://10.1785/gssrl.81.6.992 + url: https://www.seismosoc.org/Publications/srl/SRL_81/srl_81-6_eberhart-phillips_et_al-esupp/Table_S1.txt - name: EP2017 path: tomography/EP2017/EP2017.h5 From 641dfea5a16757fcded6040bd95d7c18c83d747b Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Sun, 28 Dec 2025 17:16:52 +1300 Subject: [PATCH 35/44] Adding Wu et al. (2025) with its initial background model EP2022 using hard boundary merge --- nzcvm_registry.yaml | 11 ++++++++++- tomography/SW2025/SW2025_EP2022_Merge.h5 | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 tomography/SW2025/SW2025_EP2022_Merge.h5 diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index c475647..7eaf192 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -164,7 +164,16 @@ tomography: author: Eberhart-Phillips et al. (2025) title: New Zealand Wide model 3.1 seismic velocity and Qs and Qp models for New Zealand url: Personal communication (pending publication) - + + - name: SW2025 + path: tomography/SW2025/SW2025_EP2022_Merge.h5 + elev: [ 15, 5, 4, 3, 2, 1, 0, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16, -17, -18, -19, -20, -21, -22, -23, -24, -25, -26, -27, -28, -29, -30, -31, -32, -33, -34, -35, -36, -37, -38, -39, -40, -41, -42, -43, -44, -45, -46, -47, -48, -49, -50, -51, -52, -53, -54, -55, -56, -57, -58, -59, -60, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] + author: Wu et al. (2025) + title: Seismic azimuthal anisotropy of New Zealand revealed by adjoint-state traveltime tomography + url: Full Model Data - Personal Communication, Downsampled data published in https://doi.org/10.21979/N9/QKU80S + + + basin: - name: Canterbury_v18p1 boundaries: diff --git a/tomography/SW2025/SW2025_EP2022_Merge.h5 b/tomography/SW2025/SW2025_EP2022_Merge.h5 new file mode 100644 index 0000000..139f294 --- /dev/null +++ b/tomography/SW2025/SW2025_EP2022_Merge.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a8021970301632ba496a22e8a67a8f6c0a663bb41822a37adbc648352cece5b +size 2618899313 From 1b818e97059509a74721a2956c8ceff1cdd8c478 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Tue, 30 Dec 2025 11:26:44 +1300 Subject: [PATCH 36/44] Wu model registry formatting --- nzcvm_registry.yaml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index 7eaf192..620b244 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -167,7 +167,19 @@ tomography: - name: SW2025 path: tomography/SW2025/SW2025_EP2022_Merge.h5 - elev: [ 15, 5, 4, 3, 2, 1, 0, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16, -17, -18, -19, -20, -21, -22, -23, -24, -25, -26, -27, -28, -29, -30, -31, -32, -33, -34, -35, -36, -37, -38, -39, -40, -41, -42, -43, -44, -45, -46, -47, -48, -49, -50, -51, -52, -53, -54, -55, -56, -57, -58, -59, -60, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] + elev: [ 15, 5, 4, 3, 2, 1, 0, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16, -17, -18, -19, -20, -21, -22, -23, -24, -25, -26, -27, -28, -29, -30, + -31, -32, -33, -34, -35, -36, -37, -38, -39, -40, -41, -42, -43, -44, -45, -46, -47, -48, -49, -50, -51, -52, -53, -54, -55, -56, -57, -58, -59, -60, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] + elev: + [ + 15, 5, 4, 3, 2, 1, 0, -1, -2, -3, -4, -5, -6, -7, -8, + -9, -10, -11, -12, -13, -14, -15, -16, -17, -18, -19, + -20, -21, -22, -23, -24, -25, -26, -27, -28, -29, + -30, -31, -32, -33, -34, -35, -36, -37, -38, -39, + -40, -41, -42, -43, -44, -45, -46, -47, -48, -49, + -50, -51, -52, -53, -54, -55, -56, -57, -58, -59, + -60, -65, -85, -105, -130, -155, -185, -225, -275, + -370, -620, -750 + ] author: Wu et al. (2025) title: Seismic azimuthal anisotropy of New Zealand revealed by adjoint-state traveltime tomography url: Full Model Data - Personal Communication, Downsampled data published in https://doi.org/10.21979/N9/QKU80S From e3bf40b25fbc19434bdeacf48c3477f711649b8a Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Tue, 30 Dec 2025 11:33:08 +1300 Subject: [PATCH 37/44] Wu model registry formatting --- nzcvm_registry.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index 620b244..e080932 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -167,8 +167,6 @@ tomography: - name: SW2025 path: tomography/SW2025/SW2025_EP2022_Merge.h5 - elev: [ 15, 5, 4, 3, 2, 1, 0, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -11, -12, -13, -14, -15, -16, -17, -18, -19, -20, -21, -22, -23, -24, -25, -26, -27, -28, -29, -30, - -31, -32, -33, -34, -35, -36, -37, -38, -39, -40, -41, -42, -43, -44, -45, -46, -47, -48, -49, -50, -51, -52, -53, -54, -55, -56, -57, -58, -59, -60, -65, -85, -105, -130, -155, -185, -225, -275, -370, -620, -750 ] elev: [ 15, 5, 4, 3, 2, 1, 0, -1, -2, -3, -4, -5, -6, -7, -8, From b9c2d0d778a1314280d7c6c1151780e0cb645f75 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Wed, 31 Dec 2025 17:48:40 +1300 Subject: [PATCH 38/44] Adding Dan Bassett tomography model hard boundary merged with EP2025 --- nzcvm_registry.yaml | 30 ++++++++++++++++++++++++ tomography/DB2025/DB2025_EP2025_Merge.h5 | 3 +++ 2 files changed, 33 insertions(+) create mode 100644 tomography/DB2025/DB2025_EP2025_Merge.h5 diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index e080932..d98c9a6 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -182,6 +182,36 @@ tomography: title: Seismic azimuthal anisotropy of New Zealand revealed by adjoint-state traveltime tomography url: Full Model Data - Personal Communication, Downsampled data published in https://doi.org/10.21979/N9/QKU80S + - name: DB2025 + path: tomography/DB2025/DB2025_EP2025_Merge.h5 + elev: + [ + 15.0, 3.0, 2.5, 2.0, 1.5, 1.0, 0.5, -0.0, -0.5, -1.0, + -1.5, -2.0, -2.5, -3.0, -3.5, -4.0, -4.5, -5.0, -5.5, -6.0, + -6.5, -7.0, -7.5, -8.0, -8.5, -9.0, -9.5, -10.0, -10.5, -11.0, + -11.5, -12.0, -12.5, -13.0, -13.5, -14.0, -14.5, -15.0, -15.5, -16.0, + -16.5, -17.0, -17.5, -18.0, -18.5, -19.0, -19.5, -20.0, -20.5, -21.0, + -21.5, -22.0, -22.5, -23.0, -23.5, -24.0, -24.5, -25.0, -25.5, -26.0, + -26.5, -27.0, -27.5, -28.0, -28.5, -29.0, -29.5, -30.0, -30.5, -31.0, + -31.5, -32.0, -32.5, -33.0, -33.5, -34.0, -34.5, -35.0, -35.5, -36.0, + -36.5, -37.0, -37.5, -38.0, -38.5, -39.0, -39.5, -40.0, -40.5, -41.0, + -41.5, -42.0, -42.5, -43.0, -43.5, -44.0, -44.5, -45.0, -45.5, -46.0, + -46.5, -47.0, -47.5, -48.0, -48.5, -49.0, -49.5, -50.0, -50.5, -51.0, + -51.5, -52.0, -52.5, -53.0, -53.5, -54.0, -54.5, -55.0, -55.5, -56.0, + -56.5, -57.0, -57.5, -58.0, -58.5, -59.0, -59.5, -60.0, -60.5, -61.0, + -61.5, -62.0, -62.5, -63.0, -63.5, -64.0, -64.5, -65.0, -65.5, -66.0, + -66.5, -67.0, -67.5, -68.0, -68.5, -69.0, -69.5, -70.0, -70.5, -71.0, + -71.5, -72.0, -72.5, -73.0, -73.5, -74.0, -74.5, -75.0, -75.5, -76.0, + -76.5, -77.0, -77.5, -78.0, -78.5, -79.0, -79.5, -80.0, -80.5, -81.0, + -81.5, -82.0, -82.5, -83.0, -83.5, -84.0, -84.5, -85.0, -85.5, -86.0, + -86.5, -87.0, -87.5, -88.0, -88.5, -89.0, -89.5, -90.0, -90.5, -91.0, + -91.5, -92.0, -92.5, -93.0, -93.5, -94.0, -94.5, -95.0, -95.5, -96.0, + -96.5, -97.0, -97.5, -98.0, -98.5, -99.0, -99.5, -100.0, + -105.0, -130.0, -155.0, -185.0, -225.0, -275.0, -370.0, -620.0, -750.0 + ] + author: Bassett et al. (2025) + title: Crustal Structure of the Hikurangi Subduction Zone Revealed by Four Decades of Onshore-Offshore Seismic Data + url: Latest model data obtained through personal communication (email). Older data published in https://zenodo.org/records/13381669 basin: diff --git a/tomography/DB2025/DB2025_EP2025_Merge.h5 b/tomography/DB2025/DB2025_EP2025_Merge.h5 new file mode 100644 index 0000000..82f081e --- /dev/null +++ b/tomography/DB2025/DB2025_EP2025_Merge.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cd4d2f6f9a0e23f2602278e06c97069376cde9ad476ea9a504de12708ff7510 +size 3224727754 From 8a28da7224ec0368ccd8f4e612f6f41ac2538d1e Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 5 Jan 2026 11:34:44 +1300 Subject: [PATCH 39/44] ci: use big-data runner group for extra space --- .github/workflows/pytest.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 32e365f..e9ba328 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -3,7 +3,8 @@ on: [pull_request] jobs: test: - runs-on: ubuntu-latest + runs-on: + group: big-data steps: - name: Checkout code uses: nschloe/action-cached-lfs-checkout@v1 From d48a9479cfea8ac5c0df200f3eb41dc6af7622ac Mon Sep 17 00:00:00 2001 From: Jake Faulkner Date: Mon, 5 Jan 2026 11:39:43 +1300 Subject: [PATCH 40/44] ci: use ubuntu latest but remove unused junk --- .github/workflows/pytest.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index e9ba328..64937b1 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -3,9 +3,14 @@ on: [pull_request] jobs: test: - runs-on: - group: big-data + runs-on: ubuntu-latest steps: + - name: Remove junk + uses: easimon/maximize-build-space@master + with: + root-reserve-mb: 512 + swap-size-mb: 1024 + remove-dotnet: "true" - name: Checkout code uses: nschloe/action-cached-lfs-checkout@v1 From 17b97b8ac4934ac2ce7ebe97923369300e1832c8 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Fri, 13 Feb 2026 06:49:28 +1300 Subject: [PATCH 41/44] Adding smoothed SCEC SW2025 --- nzcvm_registry.yaml | 2 +- tomography/SW2025/SW2025_EP2022_Merge.h5 | 3 --- tomography/SW2025/SW2025_EP2022_SmoothConvolve.h5 | 3 +++ 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 tomography/SW2025/SW2025_EP2022_Merge.h5 create mode 100644 tomography/SW2025/SW2025_EP2022_SmoothConvolve.h5 diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index d98c9a6..633a79f 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -166,7 +166,7 @@ tomography: url: Personal communication (pending publication) - name: SW2025 - path: tomography/SW2025/SW2025_EP2022_Merge.h5 + path: tomography/SW2025/SW2025_EP2022_SmoothConvolve.h5 elev: [ 15, 5, 4, 3, 2, 1, 0, -1, -2, -3, -4, -5, -6, -7, -8, diff --git a/tomography/SW2025/SW2025_EP2022_Merge.h5 b/tomography/SW2025/SW2025_EP2022_Merge.h5 deleted file mode 100644 index 139f294..0000000 --- a/tomography/SW2025/SW2025_EP2022_Merge.h5 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a8021970301632ba496a22e8a67a8f6c0a663bb41822a37adbc648352cece5b -size 2618899313 diff --git a/tomography/SW2025/SW2025_EP2022_SmoothConvolve.h5 b/tomography/SW2025/SW2025_EP2022_SmoothConvolve.h5 new file mode 100644 index 0000000..8722bd4 --- /dev/null +++ b/tomography/SW2025/SW2025_EP2022_SmoothConvolve.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69176adbcaf959a648a97b978d957aa95330e28cc76b9189a19ad5e6ebf5a39c +size 2628504332 From f7c9a283865dc44a0dc4038df8513c7461995ac7 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Fri, 13 Feb 2026 12:25:27 +1300 Subject: [PATCH 42/44] Adding smoothing-based Dan Bassett model --- nzcvm_registry.yaml | 2 +- tomography/DB2025/DB2025_EP2025_Merge.h5 | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 tomography/DB2025/DB2025_EP2025_Merge.h5 diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index 633a79f..2a1e93b 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -183,7 +183,7 @@ tomography: url: Full Model Data - Personal Communication, Downsampled data published in https://doi.org/10.21979/N9/QKU80S - name: DB2025 - path: tomography/DB2025/DB2025_EP2025_Merge.h5 + path: tomography/DB2025/DB2025_EP2025_SmoothConvolve.h5 elev: [ 15.0, 3.0, 2.5, 2.0, 1.5, 1.0, 0.5, -0.0, -0.5, -1.0, diff --git a/tomography/DB2025/DB2025_EP2025_Merge.h5 b/tomography/DB2025/DB2025_EP2025_Merge.h5 deleted file mode 100644 index 82f081e..0000000 --- a/tomography/DB2025/DB2025_EP2025_Merge.h5 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2cd4d2f6f9a0e23f2602278e06c97069376cde9ad476ea9a504de12708ff7510 -size 3224727754 From e3f4a4e94302c231344e536df4d38f185600e3d3 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Fri, 13 Feb 2026 14:02:46 +1300 Subject: [PATCH 43/44] Adding DB smooth merged model --- tomography/DB2025/DB2025_EP2025_SmoothConvolve.h5 | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 tomography/DB2025/DB2025_EP2025_SmoothConvolve.h5 diff --git a/tomography/DB2025/DB2025_EP2025_SmoothConvolve.h5 b/tomography/DB2025/DB2025_EP2025_SmoothConvolve.h5 new file mode 100644 index 0000000..457f29b --- /dev/null +++ b/tomography/DB2025/DB2025_EP2025_SmoothConvolve.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee162d6b075d9bdceaa334d52ff89e7bfeacc1aef3d788860aaf485c2233ee4d +size 3488267598 From a69bcd10fcff33c0b0ac325b3ed7a3b3364c9e70 Mon Sep 17 00:00:00 2001 From: Ayushi Tiwari Date: Sat, 21 Feb 2026 20:19:04 +1300 Subject: [PATCH 44/44] Adding Chow data --- nzcvm_registry.yaml | 30 ++++++++++++++++++- .../BC2022/BC2022_EP2020_SmoothConvolve.h5 | 3 ++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tomography/BC2022/BC2022_EP2020_SmoothConvolve.h5 diff --git a/nzcvm_registry.yaml b/nzcvm_registry.yaml index 2a1e93b..a50b83b 100644 --- a/nzcvm_registry.yaml +++ b/nzcvm_registry.yaml @@ -211,8 +211,36 @@ tomography: ] author: Bassett et al. (2025) title: Crustal Structure of the Hikurangi Subduction Zone Revealed by Four Decades of Onshore-Offshore Seismic Data - url: Latest model data obtained through personal communication (email). Older data published in https://zenodo.org/records/13381669 + url: Latest model data obtained through personal communication (email). Older data published in https://zenodo.org/records/13381669 + - name: BC2022 + path: tomography/BC2022/BC2022_EP2020_SmoothConvolve.h5 + elev: + [ + 15.0, 2.25, 2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25, + 0.0, -0.25, -0.5, -0.75, -1.0, -1.25, -1.5, -1.75, -2.0, -2.25, + -2.5, -2.75, -3.0, -3.25, -3.5, -3.75, -4.0, -4.25, -4.5, -4.75, + -5.0, -5.25, -5.5, -5.75, -6.0, -6.25, -6.5, -6.75, -7.0, -7.25, + -7.5, -7.75, -8.0, -9.0, -10.0, -11.0, -12.0, -13.0, -14.0, -15.0, + -16.0, -17.0, -18.0, -19.0, -20.0, -21.0, -22.0, -23.0, -24.0, -25.0, + -26.0, -27.0, -28.0, -29.0, -30.0, -31.0, -32.0, -33.0, -34.0, -35.0, + -36.0, -37.0, -38.0, -39.0, -40.0, -41.0, -42.0, -43.0, -44.0, -45.0, + -46.0, -47.0, -48.0, -49.0, -50.0, -52.0, -55.0, -56.0, -60.0, -64.0, + -65.0, -68.0, -72.0, -76.0, -80.0, -84.0, -85.0, -88.0, -92.0, -96.0, + -100.0, -104.0, -105.0, -108.0, -112.0, -116.0, -120.0, -124.0, -128.0, -130.0, + -132.0, -136.0, -140.0, -144.0, -148.0, -152.0, -155.0, -156.0, -160.0, -164.0, + -168.0, -172.0, -176.0, -180.0, -184.0, -185.0, -188.0, -192.0, -196.0, -200.0, + -204.0, -208.0, -212.0, -216.0, -220.0, -224.0, -225.0, -228.0, -232.0, -236.0, + -240.0, -244.0, -248.0, -252.0, -256.0, -260.0, -264.0, -268.0, -272.0, -275.0, + -276.0, -280.0, -284.0, -288.0, -292.0, -296.0, -300.0, -304.0, -308.0, -312.0, + -316.0, -320.0, -324.0, -328.0, -332.0, -336.0, -340.0, -344.0, -348.0, -352.0, + -356.0, -360.0, -364.0, -368.0, -370.0, -372.0, -376.0, -380.0, -384.0, -388.0, + -392.0, -396.0, -400.0, -620.0, -750.0 + ] + author: Chow et al. (2022) + title: Strong Upper-Plate Heterogeneity at the Hikurangi Subduction Margin (North Island, New Zealand) Imaged by Adjoint Tomography + url: Latest model data obtained through https://doi.org/10.17611/dp/emc.2021.nzatomnnorthvpvs.1. + basin: - name: Canterbury_v18p1 diff --git a/tomography/BC2022/BC2022_EP2020_SmoothConvolve.h5 b/tomography/BC2022/BC2022_EP2020_SmoothConvolve.h5 new file mode 100644 index 0000000..8814b08 --- /dev/null +++ b/tomography/BC2022/BC2022_EP2020_SmoothConvolve.h5 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:135af5a0b9486417032eeab170592956e35877a96876e1eaec4603e3813dba47 +size 3004682172