xhycom 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xhycom-0.1.0/LICENSE +21 -0
- xhycom-0.1.0/PKG-INFO +93 -0
- xhycom-0.1.0/README.md +51 -0
- xhycom-0.1.0/pyproject.toml +81 -0
- xhycom-0.1.0/setup.cfg +4 -0
- xhycom-0.1.0/tests/test_postprocess.py +146 -0
- xhycom-0.1.0/tests/test_reader.py +204 -0
- xhycom-0.1.0/tests/test_real_data.py +324 -0
- xhycom-0.1.0/tests/test_regrid.py +603 -0
- xhycom-0.1.0/xhycom/__init__.py +400 -0
- xhycom-0.1.0/xhycom/_abfile.py +433 -0
- xhycom-0.1.0/xhycom/_discovery.py +73 -0
- xhycom-0.1.0/xhycom/_postprocess.py +173 -0
- xhycom-0.1.0/xhycom/_reader.py +961 -0
- xhycom-0.1.0/xhycom/_regrid.py +1303 -0
- xhycom-0.1.0/xhycom/_time.py +129 -0
- xhycom-0.1.0/xhycom.egg-info/PKG-INFO +93 -0
- xhycom-0.1.0/xhycom.egg-info/SOURCES.txt +19 -0
- xhycom-0.1.0/xhycom.egg-info/dependency_links.txt +1 -0
- xhycom-0.1.0/xhycom.egg-info/requires.txt +19 -0
- xhycom-0.1.0/xhycom.egg-info/top_level.txt +1 -0
xhycom-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nora Loose
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
xhycom-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xhycom
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: xarray interface for HYCOM a.b binary output files
|
|
5
|
+
Author-email: Nora Loose <nora.loose@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nansencenter/xhycom
|
|
8
|
+
Project-URL: Documentation, https://xhycom.readthedocs.io
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/nansencenter/xhycom/issues
|
|
10
|
+
Project-URL: Source, https://github.com/nansencenter/xhycom
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy
|
|
25
|
+
Requires-Dist: xarray
|
|
26
|
+
Requires-Dist: cftime
|
|
27
|
+
Requires-Dist: dask
|
|
28
|
+
Requires-Dist: xgcm
|
|
29
|
+
Requires-Dist: numba
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
33
|
+
Requires-Dist: netCDF4; extra == "dev"
|
|
34
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
35
|
+
Provides-Extra: docs
|
|
36
|
+
Requires-Dist: sphinx; extra == "docs"
|
|
37
|
+
Requires-Dist: sphinx-book-theme; extra == "docs"
|
|
38
|
+
Requires-Dist: myst-nb; extra == "docs"
|
|
39
|
+
Requires-Dist: sphinx-copybutton; extra == "docs"
|
|
40
|
+
Requires-Dist: sphinx-design; extra == "docs"
|
|
41
|
+
Dynamic: license-file
|
|
42
|
+
|
|
43
|
+
# xhycom
|
|
44
|
+
|
|
45
|
+
[](https://github.com/nansencenter/xhycom/actions/workflows/tests.yml?query=branch%3Amain)
|
|
46
|
+
[](https://codecov.io/gh/nansencenter/xhycom)
|
|
47
|
+
[](https://xhycom.readthedocs.io/en/latest/?badge=latest)
|
|
48
|
+
|
|
49
|
+
xhycom integrates HYCOM model output with [xarray](https://docs.xarray.dev) —
|
|
50
|
+
giving every field a name, coordinates, units, and lazy out-of-memory access,
|
|
51
|
+
directly from the native `.ab` format.
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install git+https://github.com/nansencenter/xhycom.git
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Dependencies:** `numpy`, `xarray`, `cftime`, `dask`, `xgcm` — no Fortran compiler
|
|
60
|
+
or external binary readers required. This covers reading, lazy/out-of-memory
|
|
61
|
+
loading, and vertical regridding.
|
|
62
|
+
|
|
63
|
+
Horizontal regridding additionally needs [xESMF](https://xesmf.readthedocs.io),
|
|
64
|
+
whose ESMF/esmpy backend is conda-forge only. `ci/environment-regrid.yml` creates
|
|
65
|
+
the environment and `pip install`s xhycom itself (in editable mode) into it in one
|
|
66
|
+
step:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
conda env create -f ci/environment-regrid.yml
|
|
70
|
+
conda activate hycom-analysis-env
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
See [Installation](https://xhycom.readthedocs.io/en/latest/installation.html) for
|
|
74
|
+
details, including setup on the Olivia and Betzy HPC clusters.
|
|
75
|
+
|
|
76
|
+
## Quick example
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import xhycom
|
|
80
|
+
|
|
81
|
+
# Single snapshot — auto-detects file type, attaches lon/lat/time/dens
|
|
82
|
+
ds = xhycom.open_dataset("archv.2020_001_00", grid="regional.grid")
|
|
83
|
+
ds["temp"].isel(time=0, k=0).plot()
|
|
84
|
+
|
|
85
|
+
# Multi-year time series — lazy, out-of-memory, no data loaded until .compute()
|
|
86
|
+
ds = xhycom.open_mfdataset("data/", grid="regional.grid", chunks={"time": 1})
|
|
87
|
+
ds["temp"].isel(k=0).mean("time").compute()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Documentation
|
|
91
|
+
|
|
92
|
+
Full documentation — why xarray, worked examples, API reference, and a
|
|
93
|
+
how-it-works guide — is at **[https://xhycom.readthedocs.io](https://xhycom.readthedocs.io/en/latest/)**.
|
xhycom-0.1.0/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# xhycom
|
|
2
|
+
|
|
3
|
+
[](https://github.com/nansencenter/xhycom/actions/workflows/tests.yml?query=branch%3Amain)
|
|
4
|
+
[](https://codecov.io/gh/nansencenter/xhycom)
|
|
5
|
+
[](https://xhycom.readthedocs.io/en/latest/?badge=latest)
|
|
6
|
+
|
|
7
|
+
xhycom integrates HYCOM model output with [xarray](https://docs.xarray.dev) —
|
|
8
|
+
giving every field a name, coordinates, units, and lazy out-of-memory access,
|
|
9
|
+
directly from the native `.ab` format.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install git+https://github.com/nansencenter/xhycom.git
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Dependencies:** `numpy`, `xarray`, `cftime`, `dask`, `xgcm` — no Fortran compiler
|
|
18
|
+
or external binary readers required. This covers reading, lazy/out-of-memory
|
|
19
|
+
loading, and vertical regridding.
|
|
20
|
+
|
|
21
|
+
Horizontal regridding additionally needs [xESMF](https://xesmf.readthedocs.io),
|
|
22
|
+
whose ESMF/esmpy backend is conda-forge only. `ci/environment-regrid.yml` creates
|
|
23
|
+
the environment and `pip install`s xhycom itself (in editable mode) into it in one
|
|
24
|
+
step:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
conda env create -f ci/environment-regrid.yml
|
|
28
|
+
conda activate hycom-analysis-env
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
See [Installation](https://xhycom.readthedocs.io/en/latest/installation.html) for
|
|
32
|
+
details, including setup on the Olivia and Betzy HPC clusters.
|
|
33
|
+
|
|
34
|
+
## Quick example
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import xhycom
|
|
38
|
+
|
|
39
|
+
# Single snapshot — auto-detects file type, attaches lon/lat/time/dens
|
|
40
|
+
ds = xhycom.open_dataset("archv.2020_001_00", grid="regional.grid")
|
|
41
|
+
ds["temp"].isel(time=0, k=0).plot()
|
|
42
|
+
|
|
43
|
+
# Multi-year time series — lazy, out-of-memory, no data loaded until .compute()
|
|
44
|
+
ds = xhycom.open_mfdataset("data/", grid="regional.grid", chunks={"time": 1})
|
|
45
|
+
ds["temp"].isel(k=0).mean("time").compute()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
Full documentation — why xarray, worked examples, API reference, and a
|
|
51
|
+
how-it-works guide — is at **[https://xhycom.readthedocs.io](https://xhycom.readthedocs.io/en/latest/)**.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xhycom"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "xarray interface for HYCOM a.b binary output files"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Nora Loose", email = "nora.loose@gmail.com" }]
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"numpy",
|
|
15
|
+
"xarray",
|
|
16
|
+
"cftime",
|
|
17
|
+
"dask",
|
|
18
|
+
"xgcm",
|
|
19
|
+
"numba", # required by xgcm's Grid.transform for method="conservative"
|
|
20
|
+
# (regrid_vertical's default)
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Development Status :: 3 - Alpha",
|
|
24
|
+
"Intended Audience :: Science/Research",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Atmospheric Science",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.8",
|
|
29
|
+
"Programming Language :: Python :: 3.9",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
# Lateral regridding (regrid_horizontal / the full regrid wrapper) needs
|
|
37
|
+
# xESMF. Its ESMF/esmpy backend has no PyPI wheels, so it must come from
|
|
38
|
+
# conda-forge -- there is no pip-only path, and it stays optional since it
|
|
39
|
+
# can conflict with other ESMF installs on some platforms. Ready-made env:
|
|
40
|
+
# conda env create -f ci/environment-regrid.yml
|
|
41
|
+
dev = ["pytest", "pytest-cov", "netCDF4", "pre-commit"]
|
|
42
|
+
docs = [
|
|
43
|
+
"sphinx",
|
|
44
|
+
"sphinx-book-theme",
|
|
45
|
+
"myst-nb",
|
|
46
|
+
"sphinx-copybutton",
|
|
47
|
+
"sphinx-design",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Homepage = "https://github.com/nansencenter/xhycom"
|
|
52
|
+
Documentation = "https://xhycom.readthedocs.io"
|
|
53
|
+
"Bug Tracker" = "https://github.com/nansencenter/xhycom/issues"
|
|
54
|
+
Source = "https://github.com/nansencenter/xhycom"
|
|
55
|
+
|
|
56
|
+
[tool.setuptools.packages.find]
|
|
57
|
+
where = ["."]
|
|
58
|
+
include = ["xhycom*"]
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
testpaths = ["tests"]
|
|
62
|
+
# dask and xgcm are core dependencies, so reader/postprocess/vertical-regrid
|
|
63
|
+
# tests run on the base + dev install. The lateral-regrid tests still
|
|
64
|
+
# self-skip when xesmf is unavailable (conda-forge only, see above).
|
|
65
|
+
|
|
66
|
+
[tool.coverage.run]
|
|
67
|
+
source = ["xhycom"]
|
|
68
|
+
omit = ["tests/*"]
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
fix = true
|
|
72
|
+
show-fixes = true
|
|
73
|
+
line-length = 88
|
|
74
|
+
exclude = ["docs"]
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint]
|
|
77
|
+
extend-select = ["I", "E", "W", "F", "D", "RUF", "UP"]
|
|
78
|
+
ignore = ["E501", "D205", "D102", "D100", "D401", "D101", "D105", "D103", "D104", "UP040", "RUF043", "RUF059", "UP031", "RUF001", "RUF002", "RUF003", "RUF012"]
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint.pydocstyle]
|
|
81
|
+
convention = "pep257"
|
xhycom-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Tests for xhycom.postprocess (unit conversions + derived fields)."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
import xarray as xr
|
|
6
|
+
|
|
7
|
+
import xhycom
|
|
8
|
+
from xhycom._postprocess import _G, _ONEM, _reconcile_velocities, postprocess
|
|
9
|
+
from xhycom._regrid import layer_centre_depth
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_ssh_converted_to_metres():
|
|
13
|
+
# srfhgt is geopotential g*eta; 1.5 m of SSH -> stored as g*1.5
|
|
14
|
+
srfhgt = xr.DataArray(
|
|
15
|
+
np.full((2, 3), _G * 1.5), dims=("y", "x"), name="srfhgt", attrs={"units": "Pa"}
|
|
16
|
+
)
|
|
17
|
+
out = postprocess(xr.Dataset({"srfhgt": srfhgt}))
|
|
18
|
+
np.testing.assert_allclose(out["srfhgt"].values, 1.5)
|
|
19
|
+
assert out["srfhgt"].attrs["units"] == "m"
|
|
20
|
+
assert "converted from" in out["srfhgt"].attrs["comment"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_thknss_converted_with_onem_not_g():
|
|
24
|
+
thknss = xr.DataArray(
|
|
25
|
+
np.full((1, 2, 2), 10.0 * _ONEM),
|
|
26
|
+
dims=("k", "y", "x"),
|
|
27
|
+
name="thknss",
|
|
28
|
+
attrs={"units": "Pa"},
|
|
29
|
+
)
|
|
30
|
+
out = postprocess(xr.Dataset({"thknss": thknss}))
|
|
31
|
+
np.testing.assert_allclose(out["thknss"].values, 10.0)
|
|
32
|
+
assert out["thknss"].attrs["units"] == "m"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_area_added_from_scales():
|
|
36
|
+
ds = xr.Dataset(
|
|
37
|
+
{
|
|
38
|
+
"scpx": (("y", "x"), np.full((2, 2), 100.0)),
|
|
39
|
+
"scpy": (("y", "x"), np.full((2, 2), 200.0)),
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
out = postprocess(ds)
|
|
43
|
+
assert "area" in out
|
|
44
|
+
np.testing.assert_allclose(out["area"].values, 20000.0)
|
|
45
|
+
assert out["area"].attrs["units"] == "m2"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_landmask_from_depth():
|
|
49
|
+
depth = xr.DataArray(
|
|
50
|
+
np.array([[10.0, np.nan], [5.0, 3.0]]),
|
|
51
|
+
dims=("y", "x"),
|
|
52
|
+
name="depth",
|
|
53
|
+
attrs={"units": "m"},
|
|
54
|
+
)
|
|
55
|
+
out = postprocess(xr.Dataset({"depth": depth}))
|
|
56
|
+
assert "landmask" in out
|
|
57
|
+
np.testing.assert_array_equal(out["landmask"].values, [[1, 0], [1, 1]])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_idempotent_does_not_double_convert():
|
|
61
|
+
srfhgt = xr.DataArray(
|
|
62
|
+
np.full((2, 2), _G), dims=("y", "x"), name="srfhgt", attrs={"units": "Pa"}
|
|
63
|
+
)
|
|
64
|
+
once = postprocess(xr.Dataset({"srfhgt": srfhgt}))
|
|
65
|
+
twice = postprocess(once)
|
|
66
|
+
np.testing.assert_allclose(twice["srfhgt"].values, once["srfhgt"].values)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_postprocess_exported():
|
|
70
|
+
assert xhycom.postprocess is postprocess
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_regrid_vertical_unit_aware_after_postprocess():
|
|
74
|
+
# thknss in Pa vs converted to m must yield identical layer-centre depths
|
|
75
|
+
thk_pa = xr.DataArray(
|
|
76
|
+
np.full((4, 1, 1), 10.0 * _ONEM),
|
|
77
|
+
dims=("k", "y", "x"),
|
|
78
|
+
name="thknss",
|
|
79
|
+
attrs={"units": "Pa"},
|
|
80
|
+
)
|
|
81
|
+
thk_m = postprocess(xr.Dataset({"thknss": thk_pa}))["thknss"]
|
|
82
|
+
z_pa = layer_centre_depth(thk_pa).values
|
|
83
|
+
z_m = layer_centre_depth(thk_m).values
|
|
84
|
+
np.testing.assert_allclose(z_pa, z_m)
|
|
85
|
+
np.testing.assert_allclose(z_pa[:, 0, 0], [5, 15, 25, 35], atol=1e-3)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Velocity convention: archv stores baroclinic, archm stores total
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
def _vel_ds(archive_type, with_barotropic=True):
|
|
92
|
+
data = {
|
|
93
|
+
"u-vel.": xr.DataArray(np.full((2, 2, 2), 0.3), dims=("k", "y", "x")),
|
|
94
|
+
"v-vel.": xr.DataArray(np.full((2, 2, 2), -0.2), dims=("k", "y", "x")),
|
|
95
|
+
}
|
|
96
|
+
if with_barotropic:
|
|
97
|
+
data["u_btrop"] = xr.DataArray(np.full((2, 2), 0.1), dims=("y", "x"))
|
|
98
|
+
data["v_btrop"] = xr.DataArray(np.full((2, 2), 0.05), dims=("y", "x"))
|
|
99
|
+
return xr.Dataset(data, attrs={"archive_type": archive_type})
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_velocity_total_for_instantaneous_archive():
|
|
103
|
+
out = postprocess(_vel_ds("instantaneous"))
|
|
104
|
+
np.testing.assert_allclose(
|
|
105
|
+
out["u-vel."].values, 0.4
|
|
106
|
+
) # 0.3 baroclinic + 0.1 barotropic
|
|
107
|
+
np.testing.assert_allclose(out["v-vel."].values, -0.15)
|
|
108
|
+
assert out["u-vel."].attrs["hycom_velocity"] == "total"
|
|
109
|
+
assert "barotropic" in out["u-vel."].attrs["comment"]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_velocity_total_for_mean_archive():
|
|
113
|
+
out = postprocess(_vel_ds("mean"))
|
|
114
|
+
np.testing.assert_allclose(out["u-vel."].values, 0.3) # already total, unchanged
|
|
115
|
+
assert out["u-vel."].attrs["hycom_velocity"] == "total"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_velocity_left_baroclinic_without_barotropic():
|
|
119
|
+
with pytest.warns(UserWarning, match="baroclinic"):
|
|
120
|
+
out = postprocess(_vel_ds("instantaneous", with_barotropic=False))
|
|
121
|
+
np.testing.assert_allclose(out["u-vel."].values, 0.3) # unchanged
|
|
122
|
+
assert out["u-vel."].attrs["hycom_velocity"] == "baroclinic"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_velocity_reconcile_idempotent():
|
|
126
|
+
out = postprocess(postprocess(_vel_ds("instantaneous")))
|
|
127
|
+
np.testing.assert_allclose(out["u-vel."].values, 0.4) # not 0.5
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_velocity_untouched_without_archive_type():
|
|
131
|
+
ds = xr.Dataset({"u-vel.": xr.DataArray(np.full((2, 2), 0.3), dims=("y", "x"))})
|
|
132
|
+
out = _reconcile_velocities(ds)
|
|
133
|
+
assert "hycom_velocity" not in out["u-vel."].attrs
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_augment_velocity_vars():
|
|
137
|
+
from xhycom import _augment_velocity_vars
|
|
138
|
+
|
|
139
|
+
aug, auto = _augment_velocity_vars(["temp", "u-vel.", "v-vel."], True)
|
|
140
|
+
assert set(auto) == {"u_btrop", "v_btrop"}
|
|
141
|
+
assert {"temp", "u-vel.", "v-vel.", "u_btrop", "v_btrop"} == set(aug)
|
|
142
|
+
assert _augment_velocity_vars(["u-vel."], False) == (["u-vel."], [])
|
|
143
|
+
assert _augment_velocity_vars(None, True) == (None, [])
|
|
144
|
+
assert _augment_velocity_vars(["temp"], True) == (["temp"], [])
|
|
145
|
+
aug2, auto2 = _augment_velocity_vars(["u-vel.", "u_btrop"], True)
|
|
146
|
+
assert auto2 == ["v_btrop"]
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Reader tests against synthetic .ab fixtures (see conftest.py)."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
import xhycom
|
|
7
|
+
from xhycom._reader import detect_filetype, read_ave
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# ---------------------------------------------------------------------------
|
|
11
|
+
# detect_filetype
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
def test_detect_filetype(grid_file, bathy_file, archive_file):
|
|
14
|
+
assert detect_filetype(grid_file[0]) == "grid"
|
|
15
|
+
assert detect_filetype(bathy_file[0]) == "bathy"
|
|
16
|
+
assert detect_filetype(archive_file) == "archv"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Grid
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
def test_open_grid(grid_file):
|
|
23
|
+
base, fields = grid_file
|
|
24
|
+
ds = xhycom.open_dataset(base)
|
|
25
|
+
for name, arr in fields.items():
|
|
26
|
+
assert name in ds, name
|
|
27
|
+
np.testing.assert_allclose(ds[name].values, arr, rtol=1e-5)
|
|
28
|
+
assert ds["plon"].dims == ("y", "x")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Bathymetry
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
def test_open_bathy_requires_grid(bathy_file):
|
|
35
|
+
with pytest.raises(ValueError, match="grid="):
|
|
36
|
+
xhycom.open_dataset(bathy_file[0])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_open_bathy(bathy_file, grid_file):
|
|
40
|
+
base, depth = bathy_file
|
|
41
|
+
grid = xhycom.open_dataset(grid_file[0])
|
|
42
|
+
ds = xhycom.open_dataset(base, grid=grid)
|
|
43
|
+
wet = depth < 1e30
|
|
44
|
+
np.testing.assert_allclose(ds["depth"].values[wet], depth[wet], rtol=1e-5)
|
|
45
|
+
assert np.isnan(ds["depth"].values[0, 0]) # land point -> NaN
|
|
46
|
+
assert "lon" in ds.coords and "lat" in ds.coords
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Archive
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
def test_open_archive_structure(archive_file, grid_file):
|
|
53
|
+
grid = xhycom.open_dataset(grid_file[0])
|
|
54
|
+
ds = xhycom.open_dataset(archive_file, grid=grid)
|
|
55
|
+
assert set(ds["temp"].dims) == {"time", "k", "y", "x"}
|
|
56
|
+
assert ds["srfhgt"].dims == ("time", "y", "x")
|
|
57
|
+
np.testing.assert_array_equal(ds["k"].values, [1, 2, 3])
|
|
58
|
+
np.testing.assert_allclose(ds["dens"].values, [28.0, 29.0, 30.0])
|
|
59
|
+
# temp[k] == 10 - k
|
|
60
|
+
np.testing.assert_allclose(
|
|
61
|
+
ds["temp"].isel(time=0, y=0, x=0).values,
|
|
62
|
+
[9.0, 8.0, 7.0],
|
|
63
|
+
rtol=1e-5,
|
|
64
|
+
)
|
|
65
|
+
assert ds["time"].size == 1
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_open_archive_variables_filter(archive_file, grid_file):
|
|
69
|
+
grid = xhycom.open_dataset(grid_file[0])
|
|
70
|
+
ds = xhycom.open_dataset(archive_file, grid=grid, variables=["temp", "thknss"])
|
|
71
|
+
assert "temp" in ds and "thknss" in ds
|
|
72
|
+
assert "salin" not in ds
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_open_mfdataset_concats_time(archive_pair, grid_file):
|
|
76
|
+
grid = xhycom.open_dataset(grid_file[0])
|
|
77
|
+
ds = xhycom.open_mfdataset(archive_pair, grid=grid)
|
|
78
|
+
assert ds["time"].size == 2
|
|
79
|
+
assert ds["temp"].sizes["time"] == 2
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_open_mfdataset_lazy_chunks(archive_pair, grid_file):
|
|
83
|
+
pytest.importorskip("dask")
|
|
84
|
+
grid = xhycom.open_dataset(grid_file[0])
|
|
85
|
+
ds = xhycom.open_mfdataset(archive_pair, grid=grid, chunks={"time": 1})
|
|
86
|
+
assert ds["temp"].chunks is not None
|
|
87
|
+
np.testing.assert_allclose(
|
|
88
|
+
ds["temp"].isel(time=0, y=0, x=0).values,
|
|
89
|
+
[9.0, 8.0, 7.0],
|
|
90
|
+
rtol=1e-5,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# postprocess threaded through the real read path
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
def test_postprocess_through_open_archive(archive_file, grid_file):
|
|
98
|
+
grid = xhycom.open_dataset(grid_file[0])
|
|
99
|
+
ds = xhycom.open_dataset(archive_file, grid=grid, postprocess=True)
|
|
100
|
+
assert ds["srfhgt"].attrs["units"] == "m"
|
|
101
|
+
np.testing.assert_allclose(
|
|
102
|
+
ds["srfhgt"].isel(time=0, y=0, x=0).values, 0.5, rtol=1e-4
|
|
103
|
+
)
|
|
104
|
+
assert ds["thknss"].attrs["units"] == "m"
|
|
105
|
+
np.testing.assert_allclose(
|
|
106
|
+
ds["thknss"].isel(time=0, y=0, x=0).values, [10.0, 10.0, 10.0], rtol=1e-4
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_postprocess_through_open_grid(grid_file):
|
|
111
|
+
base, _ = grid_file
|
|
112
|
+
ds = xhycom.open_dataset(base, postprocess=True)
|
|
113
|
+
assert "area" in ds
|
|
114
|
+
np.testing.assert_allclose(ds["area"].values, 100.0 * 200.0, rtol=1e-5)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_postprocess_through_open_bathy(bathy_file, grid_file):
|
|
118
|
+
grid = xhycom.open_dataset(grid_file[0])
|
|
119
|
+
ds = xhycom.open_dataset(bathy_file[0], grid=grid, postprocess=True)
|
|
120
|
+
assert "landmask" in ds
|
|
121
|
+
assert ds["landmask"].values[0, 0] == 0 # land
|
|
122
|
+
assert ds["landmask"].values[-1, -1] == 1 # ocean
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_archive_type_instantaneous(archive_file):
|
|
126
|
+
# The synthetic fixture writes a "... model day" header -> instantaneous archv.
|
|
127
|
+
ds = xhycom.open_dataset(archive_file)
|
|
128
|
+
assert ds.attrs["archive_type"] == "instantaneous"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# AVE (hycave/ensave monthly average)
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
def test_detect_filetype_ave(ave_file: str) -> None:
|
|
135
|
+
"""AVE header (has 'iversn' + 'kdm ') is distinguished from archv."""
|
|
136
|
+
assert detect_filetype(ave_file) == "ave"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_open_ave_structure(ave_file: str, grid_file: tuple) -> None:
|
|
140
|
+
"""2-D and 3-D AVE fields land on the expected dims; archive_type is set."""
|
|
141
|
+
grid = xhycom.open_dataset(grid_file[0])
|
|
142
|
+
ds = xhycom.open_dataset(ave_file, grid=grid)
|
|
143
|
+
assert "ssh" in ds
|
|
144
|
+
assert "temp" in ds
|
|
145
|
+
assert ds["ssh"].dims == ("time", "y", "x")
|
|
146
|
+
assert set(ds["temp"].dims) == {"time", "k", "y", "x"}
|
|
147
|
+
np.testing.assert_array_equal(ds["k"].values, [1, 2, 3])
|
|
148
|
+
assert ds["time"].size == 1
|
|
149
|
+
assert ds.attrs["archive_type"] == "time_average"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_open_ave_time_from_filename(ave_file: str) -> None:
|
|
153
|
+
"""Time coordinate is parsed from _YYYY_MM suffix, not from model-day (always 0)."""
|
|
154
|
+
import cftime
|
|
155
|
+
|
|
156
|
+
ds = xhycom.open_dataset(ave_file)
|
|
157
|
+
t = ds["time"].values[0]
|
|
158
|
+
assert isinstance(t, cftime.datetime)
|
|
159
|
+
assert t.year == 1991
|
|
160
|
+
assert t.month == 1
|
|
161
|
+
assert t.day == 1
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_open_ave_variables_filter(ave_file: str) -> None:
|
|
165
|
+
"""variables= kwarg restricts which AVE fields are loaded."""
|
|
166
|
+
ds = xhycom.open_dataset(ave_file, variables=["temp"])
|
|
167
|
+
assert "temp" in ds
|
|
168
|
+
assert "ssh" not in ds
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_open_ave_no_time_without_pattern(tmp_path, grid_file: tuple) -> None:
|
|
172
|
+
"""Basenames without _YYYY_MM suffix produce a Dataset with no time dim."""
|
|
173
|
+
from conftest import _write_ave
|
|
174
|
+
|
|
175
|
+
base = str(tmp_path / "EXPAVE_nodate")
|
|
176
|
+
_write_ave(base)
|
|
177
|
+
ds = xhycom.open_dataset(base)
|
|
178
|
+
assert "time" not in ds.dims
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_read_ave_public_api(ave_file: str) -> None:
|
|
182
|
+
"""read_ave() is importable from xhycom._reader and returns a Dataset."""
|
|
183
|
+
ds = read_ave(ave_file)
|
|
184
|
+
assert "ssh" in ds and "temp" in ds
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def test_archive_type_mean(tp0):
|
|
188
|
+
# The bundled archm fixture writes a "... mean day" header -> mean archive,
|
|
189
|
+
# whose u-vel./v-vel. are already the total current.
|
|
190
|
+
ds = xhycom.open_dataset(tp0.archive, postprocess=True)
|
|
191
|
+
assert ds.attrs["archive_type"] == "mean"
|
|
192
|
+
assert ds["u-vel."].attrs["hycom_velocity"] == "total"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_open_ave_chunks(ave_file: str) -> None:
|
|
196
|
+
"""read_ave with chunks= loads lazily via Dask."""
|
|
197
|
+
pytest.importorskip("dask")
|
|
198
|
+
ds = read_ave(ave_file, chunks={"k": 1})
|
|
199
|
+
assert ds["temp"].chunks is not None
|
|
200
|
+
np.testing.assert_allclose(
|
|
201
|
+
ds["temp"].isel(time=0, y=0, x=0).values,
|
|
202
|
+
[9.0, 8.0, 7.0],
|
|
203
|
+
rtol=1e-5,
|
|
204
|
+
)
|