xeos 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.
- xeos-0.1.0/.github/workflows/ci.yml +134 -0
- xeos-0.1.0/.github/workflows/docs.yml +49 -0
- xeos-0.1.0/.github/workflows/python-publish.yml +99 -0
- xeos-0.1.0/.gitignore +23 -0
- xeos-0.1.0/CLAUDE.md +122 -0
- xeos-0.1.0/LICENSE +21 -0
- xeos-0.1.0/PKG-INFO +135 -0
- xeos-0.1.0/README.md +103 -0
- xeos-0.1.0/ci/environment.yml +14 -0
- xeos-0.1.0/conda/README.md +67 -0
- xeos-0.1.0/conda/meta.yaml +66 -0
- xeos-0.1.0/pyproject.toml +59 -0
- xeos-0.1.0/xeos/__init__.py +37 -0
- xeos-0.1.0/xeos/api.py +34 -0
- xeos-0.1.0/xeos/backends/__init__.py +17 -0
- xeos-0.1.0/xeos/backends/_jmd95.py +86 -0
- xeos-0.1.0/xeos/backends/_linear.py +52 -0
- xeos-0.1.0/xeos/backends/_mdjwf.py +59 -0
- xeos-0.1.0/xeos/backends/_mpas.py +103 -0
- xeos-0.1.0/xeos/backends/_roquet.py +162 -0
- xeos-0.1.0/xeos/backends/_roquet_idealized.py +81 -0
- xeos-0.1.0/xeos/backends/_roquet_spv.py +175 -0
- xeos-0.1.0/xeos/backends/_teos10.py +61 -0
- xeos-0.1.0/xeos/backends/_unesco.py +68 -0
- xeos-0.1.0/xeos/backends/_wright.py +87 -0
- xeos-0.1.0/xeos/conventions.py +104 -0
- xeos-0.1.0/xeos/eos.py +147 -0
- xeos-0.1.0/xeos/models.py +123 -0
- xeos-0.1.0/xeos/registry.py +64 -0
- xeos-0.1.0/xeos/tests/reference/.gitignore +18 -0
- xeos-0.1.0/xeos/tests/reference/README.md +84 -0
- xeos-0.1.0/xeos/tests/reference/_build_mitgcm_fortran.py +281 -0
- xeos-0.1.0/xeos/tests/reference/_build_mpas_eos_fortran.py +224 -0
- xeos-0.1.0/xeos/tests/reference/_build_roquet_spv_fortran.py +130 -0
- xeos-0.1.0/xeos/tests/reference/_build_seawaterpolynomials_julia.py +128 -0
- xeos-0.1.0/xeos/tests/reference/_build_unesco_fortran.py +147 -0
- xeos-0.1.0/xeos/tests/reference/_build_wright_fortran.py +157 -0
- xeos-0.1.0/xeos/tests/reference/environment.yml +31 -0
- xeos-0.1.0/xeos/tests/reference/generate_truth.py +199 -0
- xeos-0.1.0/xeos/tests/reference/truth.json +2243 -0
- xeos-0.1.0/xeos/tests/test_api.py +177 -0
- xeos-0.1.0/xeos/tests/test_backends.py +69 -0
- xeos-0.1.0/xeos/tests/test_models.py +66 -0
- xeos-0.1.0/xeos/version.py +3 -0
- xeos-0.1.0/xeos/xarray_utils.py +38 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
|
|
11
|
+
# Modern replacement for styfle/cancel-workflow-action: cancel superseded runs
|
|
12
|
+
# of this workflow on the same ref (e.g. rapid pushes to a PR branch).
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Full cross-validation suite across the supported Python range, using the
|
|
20
|
+
# pinned conda environment (numpy/xarray + the optional gsw + dask extras) so
|
|
21
|
+
# the TEOS-10 backend and the dask-parallelized path are actually exercised.
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
test-conda:
|
|
24
|
+
name: test (conda, py${{ matrix.python-version }})
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
defaults:
|
|
27
|
+
run:
|
|
28
|
+
shell: bash -l {0}
|
|
29
|
+
strategy:
|
|
30
|
+
fail-fast: false
|
|
31
|
+
matrix:
|
|
32
|
+
python-version: ['3.11', '3.12', '3.13', '3.14']
|
|
33
|
+
|
|
34
|
+
steps:
|
|
35
|
+
- name: Checkout source
|
|
36
|
+
uses: actions/checkout@v4
|
|
37
|
+
|
|
38
|
+
- name: Set up conda (miniforge)
|
|
39
|
+
uses: conda-incubator/setup-miniconda@v3
|
|
40
|
+
with:
|
|
41
|
+
miniforge-version: latest
|
|
42
|
+
channels: conda-forge
|
|
43
|
+
channel-priority: strict
|
|
44
|
+
python-version: ${{ matrix.python-version }}
|
|
45
|
+
activate-environment: test_env_xeos
|
|
46
|
+
auto-activate-base: false
|
|
47
|
+
|
|
48
|
+
- name: Update environment from ci/environment.yml
|
|
49
|
+
run: conda env update -f ci/environment.yml
|
|
50
|
+
|
|
51
|
+
- name: Editable install
|
|
52
|
+
run: python -m pip install -e .
|
|
53
|
+
|
|
54
|
+
- name: Environment info
|
|
55
|
+
run: |
|
|
56
|
+
conda env list
|
|
57
|
+
conda list
|
|
58
|
+
|
|
59
|
+
- name: Test with pytest
|
|
60
|
+
run: pytest -ra
|
|
61
|
+
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
# Guard the "core install stays lightweight" invariant: install ONLY the
|
|
64
|
+
# core package (numpy + xarray) via pip, confirm no heavy optional deps leaked
|
|
65
|
+
# in, and run the suite. The gsw/dask-dependent cases skip themselves cleanly
|
|
66
|
+
# (pytest.importorskip / ImportError -> pytest.skip), so this also serves as a
|
|
67
|
+
# non-conda cross-check that the vendored numpy kernels stand on their own.
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
test-pip-core:
|
|
70
|
+
name: test (pip, core-only)
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
steps:
|
|
73
|
+
- name: Checkout source
|
|
74
|
+
uses: actions/checkout@v4
|
|
75
|
+
|
|
76
|
+
- name: Set up Python
|
|
77
|
+
uses: actions/setup-python@v5
|
|
78
|
+
with:
|
|
79
|
+
python-version: '3.12'
|
|
80
|
+
|
|
81
|
+
- name: Install core package only (numpy + xarray)
|
|
82
|
+
run: |
|
|
83
|
+
python -m pip install --upgrade pip
|
|
84
|
+
python -m pip install -e .
|
|
85
|
+
|
|
86
|
+
- name: Assert the core install did not pull heavy optional deps
|
|
87
|
+
run: |
|
|
88
|
+
python -c "import xeos; print('xeos import OK,', len(xeos.list_eos()), 'backends registered')"
|
|
89
|
+
python - <<'PY'
|
|
90
|
+
import importlib.util
|
|
91
|
+
for mod in ("gsw", "dask", "numba"):
|
|
92
|
+
assert importlib.util.find_spec(mod) is None, \
|
|
93
|
+
f"{mod} unexpectedly installed by the core `pip install -e .` (invariant broken)"
|
|
94
|
+
print("lightweight-core invariant holds: gsw/dask/numba absent")
|
|
95
|
+
PY
|
|
96
|
+
|
|
97
|
+
- name: Run tests (pytest added separately so it is not a core dep)
|
|
98
|
+
run: |
|
|
99
|
+
python -m pip install pytest
|
|
100
|
+
pytest -ra
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Style / static-analysis. Advisory (non-blocking): the vendored numeric
|
|
104
|
+
# kernels deliberately use compact, hand-aligned coefficient tables and
|
|
105
|
+
# oceanographic single-letter names (R100, A0, SA, CT, ...), which black would
|
|
106
|
+
# explode line-per-value and pylint flags as naming/format violations. The
|
|
107
|
+
# job still runs so reviewers see the output, but does not gate the workflow.
|
|
108
|
+
# Flip `continue-on-error` to false once the team adopts a [tool.black] /
|
|
109
|
+
# pylint config that codifies those exceptions.
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
lint:
|
|
112
|
+
name: lint / format (advisory)
|
|
113
|
+
runs-on: ubuntu-latest
|
|
114
|
+
steps:
|
|
115
|
+
- name: Checkout source
|
|
116
|
+
uses: actions/checkout@v4
|
|
117
|
+
|
|
118
|
+
- name: Set up Python
|
|
119
|
+
uses: actions/setup-python@v5
|
|
120
|
+
with:
|
|
121
|
+
python-version: '3.12'
|
|
122
|
+
|
|
123
|
+
- name: Install lint tools + package
|
|
124
|
+
run: |
|
|
125
|
+
python -m pip install --upgrade pip
|
|
126
|
+
python -m pip install -e . black pylint
|
|
127
|
+
|
|
128
|
+
- name: black --check
|
|
129
|
+
continue-on-error: true
|
|
130
|
+
run: black --check --diff xeos
|
|
131
|
+
|
|
132
|
+
- name: pylint
|
|
133
|
+
continue-on-error: true
|
|
134
|
+
run: pylint xeos
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Docs
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
# Allow the GITHUB_TOKEN to deploy to GitHub Pages.
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
pages: write
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
# Allow one concurrent deployment; cancel in-progress runs for the same ref.
|
|
15
|
+
concurrency:
|
|
16
|
+
group: pages
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
build:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.12"
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: pip install -r docs/requirements.txt
|
|
31
|
+
|
|
32
|
+
- name: Build HTML docs
|
|
33
|
+
run: sphinx-build -W --keep-going -b html docs docs/_build/html
|
|
34
|
+
|
|
35
|
+
- uses: actions/configure-pages@v5
|
|
36
|
+
|
|
37
|
+
- uses: actions/upload-pages-artifact@v3
|
|
38
|
+
with:
|
|
39
|
+
path: docs/_build/html
|
|
40
|
+
|
|
41
|
+
deploy:
|
|
42
|
+
needs: build
|
|
43
|
+
runs-on: ubuntu-latest
|
|
44
|
+
environment:
|
|
45
|
+
name: github-pages
|
|
46
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
47
|
+
steps:
|
|
48
|
+
- id: deployment
|
|
49
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Build and publish xeos to PyPI using PyPI Trusted Publishing (OIDC).
|
|
2
|
+
#
|
|
3
|
+
# No API token / password secret is used: the `publish` job authenticates to
|
|
4
|
+
# PyPI over OpenID Connect via `pypa/gh-action-pypi-publish`, gated by the
|
|
5
|
+
# GitHub `pypi` environment. See conda/README.md and PyPI's "Trusted Publishers"
|
|
6
|
+
# settings for the one-time configuration the maintainer must do on PyPI before
|
|
7
|
+
# the first publish succeeds.
|
|
8
|
+
#
|
|
9
|
+
# Triggers:
|
|
10
|
+
# * release: published -> build + publish to PyPI
|
|
11
|
+
# * workflow_dispatch -> build (+ optional TestPyPI publish) on demand
|
|
12
|
+
|
|
13
|
+
name: Publish to PyPI
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
release:
|
|
17
|
+
types: [published]
|
|
18
|
+
workflow_dispatch:
|
|
19
|
+
inputs:
|
|
20
|
+
testpypi:
|
|
21
|
+
description: "Also publish the build to TestPyPI"
|
|
22
|
+
type: boolean
|
|
23
|
+
default: false
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
build:
|
|
30
|
+
name: Build sdist + wheel
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
|
|
35
|
+
- name: Set up Python
|
|
36
|
+
uses: actions/setup-python@v5
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.x"
|
|
39
|
+
|
|
40
|
+
- name: Install build tooling
|
|
41
|
+
run: python -m pip install --upgrade pip build twine
|
|
42
|
+
|
|
43
|
+
- name: Build sdist and wheel
|
|
44
|
+
run: python -m build
|
|
45
|
+
|
|
46
|
+
- name: Check distribution metadata
|
|
47
|
+
run: twine check --strict dist/*
|
|
48
|
+
|
|
49
|
+
- name: Upload distributions
|
|
50
|
+
uses: actions/upload-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: python-package-distributions
|
|
53
|
+
path: dist/
|
|
54
|
+
|
|
55
|
+
publish-pypi:
|
|
56
|
+
name: Publish to PyPI
|
|
57
|
+
# Only publish to PyPI on an actual GitHub Release.
|
|
58
|
+
if: github.event_name == 'release'
|
|
59
|
+
needs: [build]
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
environment:
|
|
62
|
+
name: pypi
|
|
63
|
+
url: https://pypi.org/p/xeos
|
|
64
|
+
permissions:
|
|
65
|
+
id-token: write # OIDC token for Trusted Publishing
|
|
66
|
+
steps:
|
|
67
|
+
- name: Download distributions
|
|
68
|
+
uses: actions/download-artifact@v4
|
|
69
|
+
with:
|
|
70
|
+
name: python-package-distributions
|
|
71
|
+
path: dist/
|
|
72
|
+
|
|
73
|
+
- name: Publish to PyPI
|
|
74
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
75
|
+
|
|
76
|
+
publish-testpypi:
|
|
77
|
+
name: Publish to TestPyPI
|
|
78
|
+
# Opt-in dry-run path: trigger via "Run workflow" and tick the `testpypi`
|
|
79
|
+
# box. Requires a separate Trusted Publisher configured on
|
|
80
|
+
# https://test.pypi.org and a GitHub environment named `testpypi`.
|
|
81
|
+
if: github.event_name == 'workflow_dispatch' && inputs.testpypi
|
|
82
|
+
needs: [build]
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
environment:
|
|
85
|
+
name: testpypi
|
|
86
|
+
url: https://test.pypi.org/p/xeos
|
|
87
|
+
permissions:
|
|
88
|
+
id-token: write # OIDC token for Trusted Publishing
|
|
89
|
+
steps:
|
|
90
|
+
- name: Download distributions
|
|
91
|
+
uses: actions/download-artifact@v4
|
|
92
|
+
with:
|
|
93
|
+
name: python-package-distributions
|
|
94
|
+
path: dist/
|
|
95
|
+
|
|
96
|
+
- name: Publish to TestPyPI
|
|
97
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
98
|
+
with:
|
|
99
|
+
repository-url: https://test.pypi.org/legacy/
|
xeos-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
|
|
16
|
+
# OS / editor
|
|
17
|
+
.DS_Store
|
|
18
|
+
|
|
19
|
+
# Sphinx docs build output
|
|
20
|
+
docs/_build/
|
|
21
|
+
|
|
22
|
+
# Claude Code transient worktrees
|
|
23
|
+
.claude/worktrees/
|
xeos-0.1.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`xeos` provides lightweight, xarray/dask-aware wrappers over many seawater
|
|
8
|
+
Equations of State (EOS), so analysts can apply the *same* EOS their ocean-model
|
|
9
|
+
run used (MOM6, Oceananigans, MITgcm) — selected by the model's own selector
|
|
10
|
+
string. The polynomial/rational EOS are vendored as numpy kernels; core runtime
|
|
11
|
+
deps are **numpy + xarray only**. TEOS-10 is delegated to the optional `gsw` extra
|
|
12
|
+
(`pip install xeos[teos10]`) — note `gsw.rho` is itself the Roquet 75-term
|
|
13
|
+
polynomial, not the exact Gibbs function (which is `gsw.rho_t_exact`).
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install -e .[test] # editable install + gsw + pytest
|
|
19
|
+
pytest # full suite
|
|
20
|
+
pytest xeos/tests/test_backends.py # cross-validation vs frozen truth
|
|
21
|
+
pytest xeos/tests/test_models.py::test_selector_resolves # a single test
|
|
22
|
+
pylint xeos && black xeos # lint / format (declared in ci/environment.yml)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
CI (`.github/workflows/ci.yml`) builds a conda env from `ci/environment.yml`,
|
|
26
|
+
does an editable install, and runs `pytest` across Python 3.11–3.14.
|
|
27
|
+
|
|
28
|
+
## Architecture
|
|
29
|
+
|
|
30
|
+
The package is a small registry-and-facade design. Data flows:
|
|
31
|
+
`from_model(model, selector)` → canonical EOS id → registered `EOSBackend`
|
|
32
|
+
→ wrapped in an `EquationOfState` facade → kernels dispatched through xarray.
|
|
33
|
+
|
|
34
|
+
- **`registry.py`** — `EOSBackend` dataclass (a `density` kernel + optional
|
|
35
|
+
analytic `drho_dt`/`drho_ds` + convention metadata) and a global registry.
|
|
36
|
+
Backends self-register at import time.
|
|
37
|
+
- **`backends/`** — one module per EOS; importing `backends/__init__.py`
|
|
38
|
+
registers them all. Vendored kernels: `_linear`, `_wright` (full + reduced
|
|
39
|
+
coefficient sets, native pressure **Pa**), `_jmd95` and `_unesco` (native
|
|
40
|
+
**dbar**; `_unesco` reuses `_jmd95._rho_surface`), `_mdjwf` (rational fit,
|
|
41
|
+
**dbar**), `_roquet` (55-term TEOS-10 density polynomial, **dbar**≈depth),
|
|
42
|
+
`_roquet_spv` (55-term specific-volume form, MOM6 `ROQUET_SPV`), and
|
|
43
|
+
`_roquet_idealized` (6 second-order Roquet forms via one factory, conservative
|
|
44
|
+
temp / absolute salinity, Z = −p), and `_mpas` (MPAS-Ocean: `mpas-linear` plus
|
|
45
|
+
`mpas-jm`/`mpas-wright`, which **reuse** the `_jmd95`/`_wright`-reduced kernels —
|
|
46
|
+
byte-identical EOS). `_teos10` is a thin lazy-`gsw` wrapper.
|
|
47
|
+
- **`eos.py`** — `EquationOfState` facade. Converts user pressure (default dbar)
|
|
48
|
+
to each backend's native unit, dispatches via `xarray_utils.apply_eos`, and
|
|
49
|
+
computes `alpha = -drho_dt/rho`, `beta = drho_ds/rho` from analytic derivatives,
|
|
50
|
+
falling back to centred finite differences when a backend supplies none.
|
|
51
|
+
- **`models.py`** — `MODEL_SELECTORS` alias table mapping each model's selector
|
|
52
|
+
strings (MOM6 `EQN_OF_STATE`, MITgcm `eosType`, Oceananigans EOS types, MPAS-O
|
|
53
|
+
`config_eos_type`) to canonical ids; `from_model()` and `equation_of_state()`
|
|
54
|
+
(the latter handles parameterised schemes like `linear`).
|
|
55
|
+
- **`conventions.py`** — `TemperatureKind`/`SalinityKind`/`PressureUnit` enums
|
|
56
|
+
and optional gsw-backed conversion helpers. **xeos never silently converts
|
|
57
|
+
inputs**; backends declare the kinds they expect. TEOS-10 + Roquet take
|
|
58
|
+
conservative temperature / absolute salinity; everything else potential
|
|
59
|
+
temperature / practical salinity.
|
|
60
|
+
- **`xarray_utils.py`** — `apply_eos` wraps kernels with
|
|
61
|
+
`xr.apply_ufunc(dask="parallelized")` when any input is a DataArray (preserving
|
|
62
|
+
labels/dask, attaching CF attrs); otherwise calls the kernel on numpy arrays.
|
|
63
|
+
|
|
64
|
+
### Adding an EOS
|
|
65
|
+
Drop a `backends/_name.py` that builds an `EOSBackend` and calls `register(...)`,
|
|
66
|
+
import it in `backends/__init__.py`, and add its model selector strings to
|
|
67
|
+
`MODEL_SELECTORS`. Tests iterating the registry / truth fixtures pick it up.
|
|
68
|
+
|
|
69
|
+
### Critical gotchas
|
|
70
|
+
- **Pressure units differ per backend** (Wright = Pa, JMD95/Roquet/gsw = dbar);
|
|
71
|
+
the facade converts, so a kernel always receives its declared native unit.
|
|
72
|
+
- **Wright variants share one formula, differ only in coefficients.** Both
|
|
73
|
+
`wright97-reduced` (MOM6 `WRIGHT`/`WRIGHT_RED`) and `wright97-full`
|
|
74
|
+
(`WRIGHT_FULL`) are validated against MOM6 Fortran (`MOM_EOS_Wright_{red,full}.F90`).
|
|
75
|
+
- **MOM6 `UNESCO`/`JACKETT_MCD` is the JMD95 fit, NOT EOS-80.** Despite the name,
|
|
76
|
+
`MOM_EOS_UNESCO.F90` is the Jackett & McDougall (1995) potential-temp fit —
|
|
77
|
+
byte-for-byte xeos's `jmd95` (verified to machine precision), differing from the
|
|
78
|
+
original Fofonoff & Millard EOS-80 (xeos's `unesco`, = MITgcm `UNESCO`) by up to
|
|
79
|
+
~0.4 kg/m³. So those MOM6 selectors resolve to `jmd95` in `models.py`; the
|
|
80
|
+
`jmd95@mom6` truth case pins the equivalence.
|
|
81
|
+
- **`ROQUET_SPV` uses `deltaS=24`, NOT 32.** The widely-used `polyTEOS10.py`
|
|
82
|
+
reference has a typo in its `polyTEOS10_55t` routine (`deltaS=32`, copied from the
|
|
83
|
+
density form), making its specific-volume output disagree with its own published
|
|
84
|
+
check values. `_roquet_spv.py` uses the correct `24` and is validated against
|
|
85
|
+
MOM6's authoritative Fortran (`MOM_EOS_Roquet_SpV.F90`), not the buggy Python —
|
|
86
|
+
see `xeos/tests/reference/_build_roquet_spv_fortran.py`. (Bug reported upstream.)
|
|
87
|
+
- **MPAS-O `jm`/`wright` are the same EOS as `jmd95`/`wright97-reduced`.** MPAS-O's
|
|
88
|
+
`config_eos_type` offers `linear`, `jm` (Jackett-McDougall 1995) and `wright`
|
|
89
|
+
(Wright 1997, reduced coefficients); the `jm`/`wright` coefficients are
|
|
90
|
+
byte-for-byte identical to xeos's existing kernels, so `_mpas.py` reuses those
|
|
91
|
+
kernel functions rather than re-vendoring them, and the `mpas-jm`/`mpas-wright`
|
|
92
|
+
truth (from MPAS-O's own Fortran, `_build_mpas_eos_fortran.py`) confirms the reuse
|
|
93
|
+
is exact. MPAS-O's T/S clamping and depth→pressure parameterisations are
|
|
94
|
+
documented in `_mpas.py` but **not** applied (the facade takes pressure as input).
|
|
95
|
+
- **Not yet implemented:** MOM6 `JACKETT_06`, MOM6 `WRIGHT` legacy-buggy, MITgcm
|
|
96
|
+
`POLY3` (per-level runtime coefficients), MITgcm `IDEALGAS`. Add as new
|
|
97
|
+
`backends/_*.py` + selector entries when a trustworthy reference is available.
|
|
98
|
+
|
|
99
|
+
## Testing & reference truth
|
|
100
|
+
|
|
101
|
+
`test_backends.py` validates each vendored kernel against frozen values in
|
|
102
|
+
`xeos/tests/reference/truth.json`. **Preferred truth source is the model's own
|
|
103
|
+
source code**, not a third-party Python port: MITgcm Fortran (`jmd95`, `unesco`,
|
|
104
|
+
`mdjwf` — coefficients parsed from `ini_eos.F`, formulas from `find_rho.F`), MOM6
|
|
105
|
+
Fortran (`wright97-*`, `jmd95@mom6`, `roquet-spv`) and MPAS-Ocean Fortran
|
|
106
|
+
(`mpas-linear`/`-jm`/`-wright`, from E3SM) all compiled with gfortran, plus
|
|
107
|
+
Oceananigans' `SeawaterPolynomials.jl` run via Julia (the six idealized `roquet-*`
|
|
108
|
+
forms); `gsw` is the accepted exception for `teos10`. The `_build_*.py` generators
|
|
109
|
+
download/compile/run those sources on demand (each self-checks a value first) and
|
|
110
|
+
emit only numbers, so the committed `truth.json` stays toolchain-free to consume.
|
|
111
|
+
A truth case may set a `"backend"` field to validate one kernel against multiple
|
|
112
|
+
model sources (e.g. `jmd95@mom6` → the `jmd95` backend, which is *also* validated
|
|
113
|
+
against MITgcm's own Fortran; `mpas-jm`/`mpas-wright` reuse the `jmd95`/
|
|
114
|
+
`wright97-reduced` kernels and confirm MPAS-O's `jm`/`wright` are the same EOS).
|
|
115
|
+
Only `polyTEOS10.py` (→ `teos10-poly55`) remains a Python-port reference; `gsw` is
|
|
116
|
+
canonical. All run in a **separate pinned env** (`xeos/tests/reference/environment.yml`:
|
|
117
|
+
gfortran + julia + gsw); the regular suite reads only `truth.json`. Regenerate via
|
|
118
|
+
`xeos/tests/reference/README.md` and commit the updated JSON.
|
|
119
|
+
|
|
120
|
+
## Versioning
|
|
121
|
+
|
|
122
|
+
Single-sourced in `xeos/version.py`, read by hatchling (`[tool.hatch.version]`).
|
xeos-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Henri F. Drake
|
|
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.
|
xeos-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xeos
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: xarray-enabled wrappers for various Equations of State of seawater
|
|
5
|
+
Project-URL: Homepage, https://github.com/hdrake/xeos
|
|
6
|
+
Project-URL: Bugs/Issues/Features, https://github.com/hdrake/xeos/issues
|
|
7
|
+
Author-email: "Henri F. Drake" <hfdrake@uci.edu>
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: ocean modeling,oceanography,water mass transformation
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: numpy
|
|
21
|
+
Requires-Dist: xarray
|
|
22
|
+
Provides-Extra: complete
|
|
23
|
+
Requires-Dist: gsw; extra == 'complete'
|
|
24
|
+
Requires-Dist: numba; extra == 'complete'
|
|
25
|
+
Provides-Extra: teos10
|
|
26
|
+
Requires-Dist: gsw; extra == 'teos10'
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: dask; extra == 'test'
|
|
29
|
+
Requires-Dist: gsw; extra == 'test'
|
|
30
|
+
Requires-Dist: pytest; extra == 'test'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# xeos
|
|
34
|
+
|
|
35
|
+
**Lightweight, xarray-enabled wrappers for seawater equations of state.**
|
|
36
|
+
|
|
37
|
+
Ocean models (MOM6, MITgcm, MPAS-Ocean, Oceananigans) differ in the equation of
|
|
38
|
+
state (EOS) they use, and many let you change it at run time. Python post-processing then
|
|
39
|
+
often applies a *different* EOS than the simulation did, silently corrupting
|
|
40
|
+
derived quantities like density, thermal expansion, and water-mass transformation
|
|
41
|
+
diagnostics. `xeos` lets you pick the EOS that matches your run — **by the model's
|
|
42
|
+
own selector string** — and apply it to xarray/dask data through one uniform API.
|
|
43
|
+
|
|
44
|
+
It stays lightweight on purpose: the polynomial/rational equations of state are
|
|
45
|
+
vendored as small numpy kernels, so the core install needs only **numpy + xarray**.
|
|
46
|
+
TEOS-10 (via `gsw`) is an optional extra.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install xeos # core (numpy + xarray): all vendored EOS
|
|
52
|
+
pip install xeos[teos10] # adds TEOS-10 via gsw
|
|
53
|
+
pip install xeos[complete] # gsw + numba acceleration
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
Match your model run by its selector string:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import xeos
|
|
62
|
+
|
|
63
|
+
# MOM6 run with EQN_OF_STATE = "WRIGHT_FULL"
|
|
64
|
+
eos = xeos.from_model("MOM6", "WRIGHT_FULL")
|
|
65
|
+
rho = eos.rho(theta, salt, pressure) # xarray DataArrays in, labeled DataArray out
|
|
66
|
+
a = eos.alpha(theta, salt, pressure) # thermal expansion
|
|
67
|
+
b = eos.beta(theta, salt, pressure) # haline contraction
|
|
68
|
+
|
|
69
|
+
# MITgcm eosType = 'JMD95Z'
|
|
70
|
+
xeos.from_model("MITgcm", "JMD95Z").rho(theta, salt, p)
|
|
71
|
+
|
|
72
|
+
# MPAS-Ocean config_eos_type = 'jm'
|
|
73
|
+
xeos.from_model("MPAS-Ocean", "jm").rho(theta, salt, p)
|
|
74
|
+
|
|
75
|
+
# Oceananigans TEOS10EquationOfState
|
|
76
|
+
xeos.from_model("Oceananigans", "TEOS10EquationOfState").rho(CT, SA, p)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Or address an EOS directly:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
xeos.equation_of_state("jmd95").rho(t, s, p)
|
|
83
|
+
xeos.rho(t, s, p, eos="wright97-full") # one-off functional form
|
|
84
|
+
xeos.list_eos() # what's available
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Inputs may be scalars, numpy arrays, or xarray `DataArray`s (dask-backed arrays
|
|
88
|
+
stay lazy). Pressure is sea pressure in **dbar** by default.
|
|
89
|
+
|
|
90
|
+
## Conventions
|
|
91
|
+
|
|
92
|
+
`xeos` does **not** silently convert inputs. Each EOS declares the temperature and
|
|
93
|
+
salinity it expects: TEOS-10 and the Roquet polynomials use **conservative
|
|
94
|
+
temperature** + **absolute salinity**; the others use **potential temperature** +
|
|
95
|
+
**practical salinity** (see `eos.temperature` / `eos.salinity`). Explicit
|
|
96
|
+
conversion helpers live in `xeos.conventions` (these need the `gsw` extra).
|
|
97
|
+
|
|
98
|
+
## Supported equations of state
|
|
99
|
+
|
|
100
|
+
`xeos.list_eos()` returns the current set. As of now:
|
|
101
|
+
|
|
102
|
+
- **linear** — configurable (MOM6/MITgcm/Oceananigans `LINEAR`)
|
|
103
|
+
- **wright97-full**, **wright97-reduced** — Wright 1997 (MOM6 `WRIGHT_FULL`, `WRIGHT`/`WRIGHT_RED`)
|
|
104
|
+
- **jmd95** — Jackett & McDougall 1995 (MITgcm `JMD95Z`/`JMD95P`; **also** MOM6
|
|
105
|
+
`UNESCO`/`JACKETT_MCD`, which are this fit — *not* EOS-80)
|
|
106
|
+
- **unesco** — UNESCO/EOS-80, Fofonoff & Millard 1983 (MITgcm `UNESCO`)
|
|
107
|
+
- **mdjwf** — McDougall et al. 2003 (MITgcm `MDJWF`)
|
|
108
|
+
- **teos10-poly55** — Roquet 55-term polynomial / TEOS-10 density form
|
|
109
|
+
(Oceananigans `TEOS10EquationOfState`, MOM6 `ROQUET_RHO`/`NEMO`)
|
|
110
|
+
- **roquet-spv** — Roquet 55-term specific-volume form (MOM6 `ROQUET_SPV`)
|
|
111
|
+
- **roquet-{linear,cabbeling,cabbeling-thermobaricity,freezing,second-order,simplest-realistic}**
|
|
112
|
+
— idealized second-order Roquet forms (Oceananigans `RoquetSeawaterPolynomial(:…)`)
|
|
113
|
+
- **mpas-linear**, **mpas-jm**, **mpas-wright** — MPAS-Ocean / E3SM
|
|
114
|
+
`config_eos_type` = `linear`/`jm`/`wright`; `mpas-jm` and `mpas-wright` reuse the
|
|
115
|
+
`jmd95` and `wright97-reduced` kernels (MPAS-O's `jm`/`wright` are the same EOS)
|
|
116
|
+
- **teos10** — TEOS-10 via `gsw` (its 75-term Roquet polynomial, not the exact
|
|
117
|
+
Gibbs function; MOM6/MITgcm `TEOS10`)
|
|
118
|
+
|
|
119
|
+
Not yet implemented (planned, slot into the same registry): MOM6 `JACKETT_06` and
|
|
120
|
+
`WRIGHT` legacy-buggy, and MITgcm `POLY3` (per-level runtime coefficients).
|
|
121
|
+
|
|
122
|
+
Full literature references with DOIs are in the
|
|
123
|
+
[usage docs](docs/usage.md#references).
|
|
124
|
+
|
|
125
|
+
## Development
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
pip install -e .[test]
|
|
129
|
+
pytest # validates vendored kernels against frozen fixtures
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Test "truth" values are generated from authoritative reference packages in a
|
|
133
|
+
pinned, separate environment and frozen into `xeos/tests/reference/truth.json`;
|
|
134
|
+
the test suite reads that file and stays lightweight. See
|
|
135
|
+
[`xeos/tests/reference/README.md`](xeos/tests/reference/README.md) to regenerate.
|