xeos
Lightweight, xarray-enabled wrappers for seawater equations of state.
Ocean models (MOM6, MITgcm, MPAS-Ocean, Oceananigans) differ in the equation of
state (EOS) they use, and many let you change it at run time. Python post-processing then
often applies a different EOS than the simulation did, silently corrupting
derived quantities like density, thermal expansion, and water-mass transformation
diagnostics. xeos lets you pick the EOS that matches your run — by the model's
own selector string — and apply it to xarray/dask data through one uniform API.
It stays lightweight on purpose: the polynomial/rational equations of state are
vendored as small numpy kernels, so the core install needs only numpy + xarray.
TEOS-10 (via gsw) is an optional extra.
Install
pip install xeos # core (numpy + xarray): all vendored EOS
pip install xeos[teos10] # adds TEOS-10 via gsw
pip install xeos[complete] # gsw + numba acceleration
Usage
Match your model run by its selector string:
import xeos
# MOM6 run with EQN_OF_STATE = "WRIGHT_FULL"
eos = xeos.from_model("MOM6", "WRIGHT_FULL")
rho = eos.rho(theta, salt, pressure) # xarray DataArrays in, labeled DataArray out
a = eos.alpha(theta, salt, pressure) # thermal expansion
b = eos.beta(theta, salt, pressure) # haline contraction
# MITgcm eosType = 'JMD95Z'
xeos.from_model("MITgcm", "JMD95Z").rho(theta, salt, p)
# MPAS-Ocean config_eos_type = 'jm'
xeos.from_model("MPAS-Ocean", "jm").rho(theta, salt, p)
# Oceananigans TEOS10EquationOfState
xeos.from_model("Oceananigans", "TEOS10EquationOfState").rho(CT, SA, p)
Or address an EOS directly:
xeos.equation_of_state("jmd95").rho(t, s, p)
xeos.rho(t, s, p, eos="wright97-full") # one-off functional form
xeos.list_eos() # what's available
Inputs may be scalars, numpy arrays, or xarray DataArrays (dask-backed arrays
stay lazy). Pressure is sea pressure in dbar by default.
Conventions
xeos does not silently convert inputs. Each EOS declares the temperature and
salinity it expects: TEOS-10 and the Roquet polynomials use conservative
temperature + absolute salinity; the others use potential temperature +
practical salinity (see eos.temperature / eos.salinity). Explicit
conversion helpers live in xeos.conventions (these need the gsw extra).
Supported equations of state
xeos.list_eos() returns the current set. As of now:
- linear — configurable (MOM6/MITgcm/Oceananigans
LINEAR) - wright97-full, wright97-reduced — Wright 1997 (MOM6
WRIGHT_FULL,WRIGHT_RED/WRIGHT_REDUCED). The bare MOM6WRIGHTselector also resolves towright97-reducedwith a warning: it is the corrected reduced fit, whereas MOM6's legacyWRIGHTruns the uncorrected kernel (see "not yet implemented"). - jmd95 — Jackett & McDougall 1995 (MITgcm
JMD95Z/JMD95P; also MOM6UNESCO/JACKETT_MCD, which are this fit — not EOS-80) - unesco — UNESCO/EOS-80, Fofonoff & Millard 1983 (MITgcm
UNESCO) - mdjwf — McDougall et al. 2003 (MITgcm
MDJWF) - teos10-poly55 — Roquet 55-term polynomial / TEOS-10 density form
(Oceananigans
TEOS10EquationOfState, MOM6ROQUET_RHO/NEMO) - roquet-spv — Roquet 55-term specific-volume form (MOM6
ROQUET_SPV) - roquet-{linear,cabbeling,cabbeling-thermobaricity,freezing,second-order,simplest-realistic}
— idealized second-order Roquet forms (Oceananigans
RoquetSeawaterPolynomial(:…)) - mpas-linear, mpas-jm, mpas-wright — MPAS-Ocean / E3SM
config_eos_type=linear/jm/wright;mpas-jmandmpas-wrightreuse thejmd95andwright97-reducedkernels (MPAS-O'sjm/wrightare the same EOS) - teos10 — TEOS-10 via
gsw(its 75-term Roquet polynomial, not the exact Gibbs function; MOM6/MITgcmTEOS10)
Not yet implemented (planned, slot into the same registry): MOM6 JACKETT_06 and
the legacy-buggy WRIGHT kernel (the bare WRIGHT selector currently resolves to
the corrected wright97-reduced with a warning), and MITgcm POLY3 (per-level
runtime coefficients).
Full literature references with DOIs are in the usage docs.
Development
pip install -e .[test]
pytest # validates vendored kernels against frozen fixtures
Test "truth" values are generated from authoritative reference packages in a
pinned, separate environment and frozen into xeos/tests/reference/truth.json;
the test suite reads that file and stays lightweight. See
xeos/tests/reference/README.md to regenerate.
Releasing
The git tag is the version. xeos has no version string checked into the
source tree: hatch-vcs derives it from the tag at build time and writes
xeos/_version.py (gitignored, but shipped inside the sdist and wheel). To cut
a release you tag; there is no file to bump and nothing to keep in sync.
- Make sure
mainis green and has everything you want in the release. - Publish a GitHub Release whose tag is
vX.Y.Z, targeting the commit you want to ship:gh release create vX.Y.Z --target "$(git rev-parse origin/main)" \ --title vX.Y.Z --generate-notes
Publishing it (not merely pushing a tag) is what fires the workflow. - The Publish to PyPI workflow builds from that tag and uploads to PyPI via
Trusted Publishing (OIDC — no token secret). It checks out with
fetch-depth: 0so the tag is visible tohatch-vcs, and asserts that the built version matches the tag before publishing. - Verify: https://pypi.org/project/xeos/.
- conda-forge builds from the PyPI sdist and lags by design — see
conda/README.md. After the first release the autotick bot opens the version-bump PR for you.
To rehearse without publishing, run the workflow manually
(Actions → Publish to PyPI → Run workflow); on workflow_dispatch it builds
and can optionally push to TestPyPI, but never to PyPI.
Release files for xeos 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| xeos-0.2.2.tar.gz | 100.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| xeos-0.2.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 218.0 kB
Release files / xeos-0.2.2.tar.gz
| Download URL | xeos-0.2.2.tar.gz |
|---|---|
| Size | 100.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f042e1080a2e0f51ba5f237820ab7f6ca582706603addf202eeec06ad02cf8e5
|
|
BLAKE2b-256 checksum How to use checksums |
60760c18a7fbb06ae3d88447433d0b3a58486816e5637655289addfe18aab4b1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.
Transparency logRelease files / xeos-0.2.2-py3-none-any.whl
| Download URL | xeos-0.2.2-py3-none-any.whl |
|---|---|
| Size | 117.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
38a1c6cdf54b255fcf155e43249330b3e5f049d6390cd9b419c9bdcdb1c57a3a
|
|
BLAKE2b-256 checksum How to use checksums |
6ecdc3a93b819abf8aa2f97f90108e3eeb66234f52f235e2af136065560ba49d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 28, 2026.
Transparency log