xeos 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ """xeos: lightweight, xarray-enabled wrappers for seawater equations of state.
2
+
3
+ Pick the EOS that matches your ocean-model run — by the model's own selector
4
+ string — and apply it to xarray/dask data with a uniform API::
5
+
6
+ import xeos
7
+ eos = xeos.from_model("MOM6", "WRIGHT_FULL")
8
+ density = eos.rho(theta, salt, pressure) # theta, salt, pressure as DataArrays
9
+
10
+ See :func:`xeos.list_eos` for the available equations of state.
11
+ """
12
+
13
+ from . import backends # noqa: F401 (registers all built-in EOS at import time)
14
+
15
+ from .eos import EquationOfState
16
+ from .models import from_model, equation_of_state, MODEL_SELECTORS
17
+ from .api import rho, alpha, beta, specific_volume
18
+ from .registry import list_eos, get_backend
19
+ from .conventions import TemperatureKind, SalinityKind, PressureUnit
20
+ from .version import __version__
21
+
22
+ __all__ = [
23
+ "EquationOfState",
24
+ "from_model",
25
+ "equation_of_state",
26
+ "MODEL_SELECTORS",
27
+ "rho",
28
+ "alpha",
29
+ "beta",
30
+ "specific_volume",
31
+ "list_eos",
32
+ "get_backend",
33
+ "TemperatureKind",
34
+ "SalinityKind",
35
+ "PressureUnit",
36
+ "__version__",
37
+ ]
xeos/api.py ADDED
@@ -0,0 +1,34 @@
1
+ """Functional shims: ``xeos.rho(t, s, p, eos=...)`` and friends.
2
+
3
+ Thin wrappers over :class:`~xeos.eos.EquationOfState` for one-off calls. For
4
+ repeated use, build an ``EquationOfState`` once (via :func:`xeos.equation_of_state`
5
+ or :func:`xeos.from_model`) and call its methods.
6
+ """
7
+
8
+ from .models import equation_of_state
9
+
10
+ __all__ = ["rho", "alpha", "beta", "specific_volume"]
11
+
12
+
13
+ def _eos(eos, pressure_input_unit, params):
14
+ return equation_of_state(eos, pressure_input_unit=pressure_input_unit, **params)
15
+
16
+
17
+ def rho(t, s, p, eos, pressure_input_unit="dbar", **params):
18
+ """In-situ density [kg m-3] from the named ``eos``."""
19
+ return _eos(eos, pressure_input_unit, params).rho(t, s, p)
20
+
21
+
22
+ def alpha(t, s, p, eos, pressure_input_unit="dbar", **params):
23
+ """Thermal expansion coefficient [degC-1] from the named ``eos``."""
24
+ return _eos(eos, pressure_input_unit, params).alpha(t, s, p)
25
+
26
+
27
+ def beta(t, s, p, eos, pressure_input_unit="dbar", **params):
28
+ """Haline contraction coefficient [(salt unit)-1] from the named ``eos``."""
29
+ return _eos(eos, pressure_input_unit, params).beta(t, s, p)
30
+
31
+
32
+ def specific_volume(t, s, p, eos, pressure_input_unit="dbar", **params):
33
+ """Specific volume [m3 kg-1] from the named ``eos``."""
34
+ return _eos(eos, pressure_input_unit, params).specific_volume(t, s, p)
@@ -0,0 +1,17 @@
1
+ """Importing this package registers all built-in EOS backends.
2
+
3
+ Each module calls :func:`xeos.registry.register` at import time, so importing
4
+ the package populates the global registry. New schemes are added by dropping a
5
+ module here and importing it below.
6
+ """
7
+
8
+ from . import _linear # noqa: F401
9
+ from . import _wright # noqa: F401
10
+ from . import _jmd95 # noqa: F401
11
+ from . import _unesco # noqa: F401
12
+ from . import _mdjwf # noqa: F401
13
+ from . import _roquet # noqa: F401
14
+ from . import _roquet_spv # noqa: F401
15
+ from . import _roquet_idealized # noqa: F401
16
+ from . import _mpas # noqa: F401
17
+ from . import _teos10 # noqa: F401
@@ -0,0 +1,86 @@
1
+ """Jackett & McDougall (1995) equation of state (the MITgcm / ECCO standard).
2
+
3
+ Jackett, D.R. & T.J. McDougall (1995): "Minimal adjustment of hydrographic
4
+ profiles to achieve static stability." J. Atmos. Oceanic Technol., 12, 381-389.
5
+
6
+ Coefficients ported verbatim from the MITgcm reference implementation (also used
7
+ by ``fastjmd95``). This same fit is what MOM6 calls ``UNESCO`` / ``JACKETT_MCD``
8
+ (``MOM_EOS_UNESCO.F90``), so those MOM6 selectors resolve here too. State
9
+ variables: potential temperature [degC], practical
10
+ salinity [PSU], sea pressure [dbar]. Density derivatives are left to the
11
+ facade's centred finite-difference fallback (the reference analytic derivatives
12
+ carry a known typo); the fallback is accurate to ~1e-6 (O(h^2) truncation).
13
+ """
14
+
15
+ import numpy as np
16
+
17
+ from ..conventions import TemperatureKind, SalinityKind, PressureUnit
18
+ from ..registry import EOSBackend, register
19
+
20
+ # Density of fresh / sea water at p = 0.
21
+ _CFW = np.array([999.842594, 6.793952e-02, -9.095290e-03,
22
+ 1.001685e-04, -1.120083e-06, 6.536332e-09])
23
+ _CSW = np.array([8.244930e-01, -4.089900e-03, 7.643800e-05, -8.246700e-07,
24
+ 5.387500e-09, -5.724660e-03, 1.022700e-04, -1.654600e-06,
25
+ 4.831400e-04])
26
+ # Secant bulk modulus coefficients.
27
+ _CKFW = np.array([1.965933e04, 1.444304e02, -1.706103e00,
28
+ 9.648704e-03, -4.190253e-05])
29
+ _CKSW = np.array([5.284855e01, -3.101089e-01, 6.283263e-03, -5.084188e-05,
30
+ 3.886640e-01, 9.085835e-03, -4.619924e-04])
31
+ _CKP = np.array([3.186519e00, 2.212276e-02, -2.984642e-04, 1.956415e-06,
32
+ 6.704388e-03, -1.847318e-04, 2.059331e-07, 1.480266e-04,
33
+ 2.102898e-04, -1.202016e-05, 1.394680e-07, -2.040237e-06,
34
+ 6.128773e-08, 6.207323e-10])
35
+
36
+
37
+ def _rho_surface(s, t):
38
+ t2 = t * t
39
+ t3 = t2 * t
40
+ t4 = t3 * t
41
+ s3o2 = s * np.sqrt(s)
42
+ rho_fw = (_CFW[0] + _CFW[1] * t + _CFW[2] * t2 + _CFW[3] * t3
43
+ + _CFW[4] * t4 + _CFW[5] * t4 * t)
44
+ return (rho_fw
45
+ + s * (_CSW[0] + _CSW[1] * t + _CSW[2] * t2 + _CSW[3] * t3 + _CSW[4] * t4)
46
+ + s3o2 * (_CSW[5] + _CSW[6] * t + _CSW[7] * t2)
47
+ + _CSW[8] * s * s)
48
+
49
+
50
+ def _bulk_modulus(s, t, p_bar):
51
+ t2 = t * t
52
+ t3 = t2 * t
53
+ t4 = t3 * t
54
+ s3o2 = s * np.sqrt(s)
55
+ p = p_bar
56
+ p2 = p * p
57
+ bm = _CKFW[0] + _CKFW[1] * t + _CKFW[2] * t2 + _CKFW[3] * t3 + _CKFW[4] * t4
58
+ bm = (bm
59
+ + s * (_CKSW[0] + _CKSW[1] * t + _CKSW[2] * t2 + _CKSW[3] * t3)
60
+ + s3o2 * (_CKSW[4] + _CKSW[5] * t + _CKSW[6] * t2))
61
+ bm = (bm
62
+ + p * (_CKP[0] + _CKP[1] * t + _CKP[2] * t2 + _CKP[3] * t3)
63
+ + p * s * (_CKP[4] + _CKP[5] * t + _CKP[6] * t2)
64
+ + p * s3o2 * _CKP[7]
65
+ + p2 * (_CKP[8] + _CKP[9] * t + _CKP[10] * t2)
66
+ + p2 * s * (_CKP[11] + _CKP[12] * t + _CKP[13] * t2))
67
+ return bm
68
+
69
+
70
+ def density(t, s, p_dbar):
71
+ """In-situ density [kg m-3] from potential temp, practical salinity, dbar."""
72
+ p_bar = 0.1 * p_dbar
73
+ rho_s = _rho_surface(s, t)
74
+ bulk = _bulk_modulus(s, t, p_bar)
75
+ return rho_s / (1.0 - p_bar / bulk)
76
+
77
+
78
+ register(EOSBackend(
79
+ id="jmd95",
80
+ density=density,
81
+ temperature=TemperatureKind.POTENTIAL,
82
+ salinity=SalinityKind.PRACTICAL,
83
+ pressure_unit=PressureUnit.DBAR,
84
+ reference="Jackett & McDougall (1995), J. Atmos. Oceanic Technol., 12, 381-389.",
85
+ description="UNESCO/EOS-80 refit; MITgcm JMD95Z/JMD95P, ECCO standard.",
86
+ ))
@@ -0,0 +1,52 @@
1
+ """Linear equation of state: rho = rho0 + drho_dT * T + drho_dS * S.
2
+
3
+ Used by all three target models (MOM6 ``LINEAR``, MITgcm ``LINEAR``,
4
+ Oceananigans ``LinearEquationOfState``). The coefficients are configurable;
5
+ the registered default uses MITgcm-style values.
6
+ """
7
+
8
+ from ..conventions import TemperatureKind, SalinityKind, PressureUnit
9
+ from ..registry import EOSBackend, register
10
+
11
+ # MITgcm-style defaults (rhoNil, tAlpha, sBeta).
12
+ DEFAULT_RHO0 = 999.8
13
+ DEFAULT_TALPHA = 2.0e-4 # thermal expansion [degC-1]
14
+ DEFAULT_SBETA = 7.4e-4 # haline contraction [PSU-1]
15
+
16
+
17
+ def make_linear(rho0=DEFAULT_RHO0, talpha=DEFAULT_TALPHA, sbeta=DEFAULT_SBETA,
18
+ eos_id="linear"):
19
+ """Build a linear :class:`EOSBackend` from expansion/contraction coefficients.
20
+
21
+ ``rho = rho0 * (1 - talpha * T + sbeta * S)``, so ``drho/dT = -rho0*talpha``
22
+ and ``drho/dS = rho0*sbeta`` are exact constants.
23
+ """
24
+ drdt = -rho0 * talpha
25
+ drds = rho0 * sbeta
26
+
27
+ def density(t, s, p):
28
+ return rho0 + drdt * t + drds * s
29
+
30
+ def drho_dt(t, s, p):
31
+ return drdt + 0.0 * t
32
+
33
+ def drho_ds(t, s, p):
34
+ return drds + 0.0 * s
35
+
36
+ return EOSBackend(
37
+ id=eos_id,
38
+ density=density,
39
+ drho_dt=drho_dt,
40
+ drho_ds=drho_ds,
41
+ temperature=TemperatureKind.POTENTIAL,
42
+ salinity=SalinityKind.PRACTICAL,
43
+ pressure_unit=PressureUnit.DBAR,
44
+ reference="Linear EOS (configurable thermal/haline coefficients).",
45
+ description=(
46
+ f"rho0={rho0}, talpha={talpha}, sbeta={sbeta}; "
47
+ "pressure-independent (incompressible)."
48
+ ),
49
+ )
50
+
51
+
52
+ register(make_linear())
@@ -0,0 +1,59 @@
1
+ """McDougall, Jackett, Wright & Feistel (2003) equation of state.
2
+
3
+ McDougall, T.J., D.R. Jackett, D.G. Wright & R. Feistel (2003): "Accurate and
4
+ computationally efficient algorithms for potential temperature and density of
5
+ seawater." J. Atmos. Oceanic Technol., 20, 730-741.
6
+
7
+ A rational-function (numerator / denominator) fit; MITgcm ``eosType='MDJWF'``.
8
+ Coefficients ported from the MITgcm reference (``MITgcmutils.density.mdjwf``).
9
+
10
+ State variables: potential temperature [degC], practical salinity [PSU], sea
11
+ pressure [dbar].
12
+ """
13
+
14
+ import numpy as np
15
+
16
+ from ..conventions import TemperatureKind, SalinityKind, PressureUnit
17
+ from ..registry import EOSBackend, register
18
+
19
+ _NUM = np.array([7.35212840e+00, -5.45928211e-02, 3.98476704e-04, 2.96938239e+00,
20
+ -7.23268813e-03, 2.12382341e-03, 1.04004591e-02, 1.03970529e-07,
21
+ 5.18761880e-06, -3.24041825e-08, -1.23869360e-11, 9.99843699e+02])
22
+ _DEN = np.array([7.28606739e-03, -4.60835542e-05, 3.68390573e-07, 1.80809186e-10,
23
+ 2.14691708e-03, -9.27062484e-06, -1.78343643e-10, 4.76534122e-06,
24
+ 1.63410736e-09, 5.30848875e-06, -3.03175128e-16, -1.27934137e-17,
25
+ 1.00000000e+00])
26
+
27
+
28
+ def density(t, s, p_dbar):
29
+ """In-situ density [kg m-3] from potential temp, practical salinity, dbar."""
30
+ t1 = t
31
+ t2 = t1 * t1
32
+ s1 = s
33
+ p1 = p_dbar
34
+ sp5 = np.sqrt(s1)
35
+ p1t1 = p1 * t1
36
+
37
+ num = (_NUM[11]
38
+ + t1 * (_NUM[0] + t1 * (_NUM[1] + _NUM[2] * t1))
39
+ + s1 * (_NUM[3] + _NUM[4] * t1 + _NUM[5] * s1)
40
+ + p1 * (_NUM[6] + _NUM[7] * t2 + _NUM[8] * s1
41
+ + p1 * (_NUM[9] + _NUM[10] * t2)))
42
+ den = (_DEN[12]
43
+ + t1 * (_DEN[0] + t1 * (_DEN[1] + t1 * (_DEN[2] + t1 * _DEN[3])))
44
+ + s1 * (_DEN[4] + t1 * (_DEN[5] + _DEN[6] * t2)
45
+ + sp5 * (_DEN[7] + _DEN[8] * t2))
46
+ + p1 * (_DEN[9] + p1t1 * (_DEN[10] * t2 + _DEN[11] * p1)))
47
+ return num / den
48
+
49
+
50
+ register(EOSBackend(
51
+ id="mdjwf",
52
+ density=density,
53
+ temperature=TemperatureKind.POTENTIAL,
54
+ salinity=SalinityKind.PRACTICAL,
55
+ pressure_unit=PressureUnit.DBAR,
56
+ reference="McDougall, Jackett, Wright & Feistel (2003), "
57
+ "J. Atmos. Oceanic Technol., 20, 730-741.",
58
+ description="Rational-function EOS; MITgcm MDJWF.",
59
+ ))
xeos/backends/_mpas.py ADDED
@@ -0,0 +1,103 @@
1
+ """MPAS-Ocean (MPAS-O) equations of state.
2
+
3
+ MPAS-O (the ocean component of E3SM) selects its EOS with the namelist option
4
+ ``config_eos_type``, which accepts exactly three values -- ``linear``, ``jm`` and
5
+ ``wright`` -- implemented in
6
+ ``components/mpas-ocean/src/shared/mpas_ocn_equation_of_state_{linear,jm,wright}.F``.
7
+ The ``jm`` (Jackett & McDougall 1995) and ``wright`` (Wright 1997) kernels are
8
+ byte-for-byte identical -- same coefficients and same algebraic form -- to xeos's
9
+ existing :mod:`._jmd95` and :mod:`._wright` (reduced-range) kernels, so this module
10
+ *reuses those kernel functions* rather than re-vendoring the coefficients; the
11
+ MPAS-O truth fixtures (``mpas-jm`` / ``mpas-wright``) validate that reuse against
12
+ MPAS-O's own compiled Fortran. ``mpas-linear`` is the linear form with MPAS-O's
13
+ namelist defaults baked in.
14
+
15
+ State variables (all three): potential temperature [degC], practical salinity
16
+ [PSU]; ``jm`` takes sea pressure [dbar], ``wright`` pressure [Pa], ``linear`` is
17
+ pressure-independent.
18
+
19
+ Two MPAS-O specifics are *documented here but intentionally not exposed* (the
20
+ facade takes pressure as an input, and the validation grid is in-range):
21
+
22
+ * **T/S clamping** before the polynomial -- ``jm`` clamps to T in [-2, 40] degC and
23
+ S in [0, 42] PSU; ``wright`` to T in [-3, 30] degC and S in [28, 38] PSU.
24
+ * **depth -> pressure** -- ``jm`` derives a per-layer reference pressure in bars
25
+ from depth via a POP/Levitus polynomial
26
+ ``P_bar(z) = 0.059808*(exp(-0.025*z) - 1) + 0.100766*z + 2.28405e-7*z**2``;
27
+ ``wright`` uses the Boussinesq linearisation ``p = -rho0*g*z``.
28
+ """
29
+
30
+ from ..conventions import TemperatureKind, SalinityKind, PressureUnit
31
+ from ..registry import EOSBackend, register
32
+ from ._jmd95 import density as _jmd95_density
33
+ from ._wright import REDUCED as _WRIGHT_REDUCED, _make as _wright_make
34
+
35
+ # --- mpas-linear: rho = RhoRef - Alpha*(T - Tref) + Beta*(S - Sref) -----------
36
+ # MPAS-O Registry defaults (config_eos_linear_*): a *decrease* with temperature
37
+ # (-Alpha) and *increase* with salinity (+Beta); Alpha/Beta are dimensional
38
+ # (kg m-3 per degC / per PSU), not normalised by density.
39
+ _RHO_REF = 1000.0 # config_eos_linear_densityref [kg m-3]
40
+ _ALPHA = 0.2 # config_eos_linear_alpha [kg m-3 degC-1]
41
+ _BETA = 0.8 # config_eos_linear_beta [kg m-3 PSU-1]
42
+ _T_REF = 5.0 # config_eos_linear_Tref [degC]
43
+ _S_REF = 35.0 # config_eos_linear_Sref [PSU]
44
+
45
+
46
+ def _mpas_linear_density(t, s, p):
47
+ return _RHO_REF - _ALPHA * (t - _T_REF) + _BETA * (s - _S_REF)
48
+
49
+
50
+ def _mpas_linear_drho_dt(t, s, p):
51
+ return -_ALPHA + 0.0 * t
52
+
53
+
54
+ def _mpas_linear_drho_ds(t, s, p):
55
+ return _BETA + 0.0 * s
56
+
57
+
58
+ # --- mpas-wright: reuse the reduced-range Wright (1997) kernel -----------------
59
+ _wright_density, _wright_drho_dt, _wright_drho_ds = _wright_make(_WRIGHT_REDUCED)
60
+
61
+
62
+ register(EOSBackend(
63
+ id="mpas-linear",
64
+ density=_mpas_linear_density,
65
+ drho_dt=_mpas_linear_drho_dt,
66
+ drho_ds=_mpas_linear_drho_ds,
67
+ temperature=TemperatureKind.POTENTIAL,
68
+ salinity=SalinityKind.PRACTICAL,
69
+ pressure_unit=PressureUnit.DBAR,
70
+ reference="MPAS-O config_eos_type='linear' (mpas_ocn_equation_of_state_linear.F).",
71
+ description=(
72
+ f"rho = {_RHO_REF} - {_ALPHA}*(T-{_T_REF}) + {_BETA}*(S-{_S_REF}); "
73
+ "MPAS-O namelist defaults; pressure-independent (incompressible)."
74
+ ),
75
+ ))
76
+
77
+ register(EOSBackend(
78
+ id="mpas-jm",
79
+ density=_jmd95_density,
80
+ temperature=TemperatureKind.POTENTIAL,
81
+ salinity=SalinityKind.PRACTICAL,
82
+ pressure_unit=PressureUnit.DBAR,
83
+ reference=("MPAS-O config_eos_type='jm' (mpas_ocn_equation_of_state_jm.F); "
84
+ "Jackett & McDougall (1995), J. Atmos. Oceanic Technol., 12, 381-389."),
85
+ description=("Jackett-McDougall (1995) UNESCO/EOS-80 refit; identical kernel to "
86
+ "xeos 'jmd95'. MPAS-O clamps T,S and derives pressure from depth "
87
+ "(documented in this module; not applied here)."),
88
+ ))
89
+
90
+ register(EOSBackend(
91
+ id="mpas-wright",
92
+ density=_wright_density,
93
+ drho_dt=_wright_drho_dt,
94
+ drho_ds=_wright_drho_ds,
95
+ temperature=TemperatureKind.POTENTIAL,
96
+ salinity=SalinityKind.PRACTICAL,
97
+ pressure_unit=PressureUnit.PASCAL,
98
+ reference=("MPAS-O config_eos_type='wright' (mpas_ocn_equation_of_state_wright.F); "
99
+ "Wright (1997), J. Atmos. Oceanic Technol., 14, 735-740."),
100
+ description=("Wright (1997) reduced-range coefficients (Table 1, last column); "
101
+ "identical kernel to xeos 'wright97-reduced'. MPAS-O clamps T,S and "
102
+ "uses Boussinesq pressure p=-rho0*g*z (documented; not applied here)."),
103
+ ))
@@ -0,0 +1,162 @@
1
+ """Roquet et al. (2015) 55-term polynomial approximation to TEOS-10.
2
+
3
+ Roquet, F., Madec, G., McDougall, T.J., Barker, P.M. (2015): "Accurate
4
+ polynomial expressions for the density and specific volume of seawater using the
5
+ TEOS-10 standard." Ocean Modelling, 90, 29-43.
6
+
7
+ This is the Boussinesq in-situ density form (``polyTEOS10_bsq``) used by
8
+ Oceananigans / SeawaterPolynomials.jl ``TEOS10EquationOfState``. Coefficients
9
+ ported verbatim from Fabien Roquet's reference ``polyTEOS10.py``.
10
+
11
+ State variables: conservative temperature [degC], absolute salinity [g/kg], and
12
+ sea pressure [dbar] (numerically ~ geopotential depth in m).
13
+ """
14
+
15
+ from ..conventions import TemperatureKind, SalinityKind, PressureUnit
16
+ from ..registry import EOSBackend, register
17
+
18
+ # Reduced-variable normalisation.
19
+ _SAu = 40.0 * 35.16504 / 35.0
20
+ _CTu = 40.0
21
+ _Zu = 1.0e4
22
+ _dS = 32.0
23
+
24
+ # Vertical reference profile of density r0(pp).
25
+ _R0 = (4.6494977072e+01, -5.2099962525e+00, 2.2601900708e-01,
26
+ 6.4326772569e-02, 1.5616995503e-02, -1.7243708991e-03)
27
+
28
+ # Density-anomaly coefficients R_{ijk} (i: salinity, j: temperature, k: pressure).
29
+ R000 = 8.0189615746e+02; R100 = 8.6672408165e+02; R200 = -1.7864682637e+03
30
+ R300 = 2.0375295546e+03; R400 = -1.2849161071e+03; R500 = 4.3227585684e+02
31
+ R600 = -6.0579916612e+01; R010 = 2.6010145068e+01; R110 = -6.5281885265e+01
32
+ R210 = 8.1770425108e+01; R310 = -5.6888046321e+01; R410 = 1.7681814114e+01
33
+ R510 = -1.9193502195e+00; R020 = -3.7074170417e+01; R120 = 6.1548258127e+01
34
+ R220 = -6.0362551501e+01; R320 = 2.9130021253e+01; R420 = -5.4723692739e+00
35
+ R030 = 2.1661789529e+01; R130 = -3.3449108469e+01; R230 = 1.9717078466e+01
36
+ R330 = -3.1742946532e+00; R040 = -8.3627885467e+00; R140 = 1.1311538584e+01
37
+ R240 = -5.3563304045e+00; R050 = 5.4048723791e-01; R150 = 4.8169980163e-01
38
+ R060 = -1.9083568888e-01; R001 = 1.9681925209e+01; R101 = -4.2549998214e+01
39
+ R201 = 5.0774768218e+01; R301 = -3.0938076334e+01; R401 = 6.6051753097e+00
40
+ R011 = -1.3336301113e+01; R111 = -4.4870114575e+00; R211 = 5.0042598061e+00
41
+ R311 = -6.5399043664e-01; R021 = 6.7080479603e+00; R121 = 3.5063081279e+00
42
+ R221 = -1.8795372996e+00; R031 = -2.4649669534e+00; R131 = -5.5077101279e-01
43
+ R041 = 5.5927935970e-01; R002 = 2.0660924175e+00; R102 = -4.9527603989e+00
44
+ R202 = 2.5019633244e+00; R012 = 2.0564311499e+00; R112 = -2.1311365518e-01
45
+ R022 = -1.2419983026e+00; R003 = -2.3342758797e-02; R103 = -1.8507636718e-02
46
+ R013 = 3.7969820455e-01
47
+
48
+ # Thermal-expansion coefficients (a = -dr/dCT).
49
+ ALP000 = -6.5025362670e-01; ALP100 = 1.6320471316e+00; ALP200 = -2.0442606277e+00
50
+ ALP300 = 1.4222011580e+00; ALP400 = -4.4204535284e-01; ALP500 = 4.7983755487e-02
51
+ ALP010 = 1.8537085209e+00; ALP110 = -3.0774129064e+00; ALP210 = 3.0181275751e+00
52
+ ALP310 = -1.4565010626e+00; ALP410 = 2.7361846370e-01; ALP020 = -1.6246342147e+00
53
+ ALP120 = 2.5086831352e+00; ALP220 = -1.4787808849e+00; ALP320 = 2.3807209899e-01
54
+ ALP030 = 8.3627885467e-01; ALP130 = -1.1311538584e+00; ALP230 = 5.3563304045e-01
55
+ ALP040 = -6.7560904739e-02; ALP140 = -6.0212475204e-02; ALP050 = 2.8625353333e-02
56
+ ALP001 = 3.3340752782e-01; ALP101 = 1.1217528644e-01; ALP201 = -1.2510649515e-01
57
+ ALP301 = 1.6349760916e-02; ALP011 = -3.3540239802e-01; ALP111 = -1.7531540640e-01
58
+ ALP211 = 9.3976864981e-02; ALP021 = 1.8487252150e-01; ALP121 = 4.1307825959e-02
59
+ ALP031 = -5.5927935970e-02; ALP002 = -5.1410778748e-02; ALP102 = 5.3278413794e-03
60
+ ALP012 = 6.2099915132e-02; ALP003 = -9.4924551138e-03
61
+
62
+ # Haline-contraction coefficients (b = dr/dSA, before /ss).
63
+ BET000 = 1.0783203594e+01; BET100 = -4.4452095908e+01; BET200 = 7.6048755820e+01
64
+ BET300 = -6.3944280668e+01; BET400 = 2.6890441098e+01; BET500 = -4.5221697773e+00
65
+ BET010 = -8.1219372432e-01; BET110 = 2.0346663041e+00; BET210 = -2.1232895170e+00
66
+ BET310 = 8.7994140485e-01; BET410 = -1.1939638360e-01; BET020 = 7.6574242289e-01
67
+ BET120 = -1.5019813020e+00; BET220 = 1.0872489522e+00; BET320 = -2.7233429080e-01
68
+ BET030 = -4.1615152308e-01; BET130 = 4.9061350869e-01; BET230 = -1.1847737788e-01
69
+ BET040 = 1.4073062708e-01; BET140 = -1.3327978879e-01; BET050 = 5.9929880134e-03
70
+ BET001 = -5.2937873009e-01; BET101 = 1.2634116779e+00; BET201 = -1.1547328025e+00
71
+ BET301 = 3.2870876279e-01; BET011 = -5.5824407214e-02; BET111 = 1.2451933313e-01
72
+ BET211 = -2.4409539932e-02; BET021 = 4.3623149752e-02; BET121 = -4.6767901790e-02
73
+ BET031 = -6.8523260060e-03; BET002 = -6.1618945251e-02; BET102 = 6.2255521644e-02
74
+ BET012 = -2.6514181169e-03; BET003 = -2.3025968587e-04
75
+
76
+
77
+ def _reduced(ct, sa, p_dbar):
78
+ import numpy as np
79
+ ss = np.sqrt((sa + _dS) / _SAu)
80
+ tt = ct / _CTu
81
+ pp = p_dbar / _Zu
82
+ return ss, tt, pp
83
+
84
+
85
+ def _r0(pp):
86
+ R00, R01, R02, R03, R04, R05 = _R0
87
+ return (((((R05 * pp + R04) * pp + R03) * pp + R02) * pp + R01) * pp + R00) * pp
88
+
89
+
90
+ def _anomaly(ss, tt, pp):
91
+ rz3 = R013 * tt + R103 * ss + R003
92
+ rz2 = (R022 * tt + R112 * ss + R012) * tt + (R202 * ss + R102) * ss + R002
93
+ rz1 = ((((R041 * tt + R131 * ss + R031) * tt
94
+ + (R221 * ss + R121) * ss + R021) * tt
95
+ + ((R311 * ss + R211) * ss + R111) * ss + R011) * tt
96
+ + (((R401 * ss + R301) * ss + R201) * ss + R101) * ss + R001)
97
+ rz0 = (((((R060 * tt + R150 * ss + R050) * tt
98
+ + (R240 * ss + R140) * ss + R040) * tt
99
+ + ((R330 * ss + R230) * ss + R130) * ss + R030) * tt
100
+ + (((R420 * ss + R320) * ss + R220) * ss + R120) * ss + R020) * tt
101
+ + ((((R510 * ss + R410) * ss + R310) * ss + R210) * ss + R110) * ss + R010) * tt \
102
+ + (((((R600 * ss + R500) * ss + R400) * ss + R300) * ss + R200) * ss + R100) * ss + R000
103
+ return ((rz3 * pp + rz2) * pp + rz1) * pp + rz0
104
+
105
+
106
+ def density(ct, sa, p_dbar):
107
+ """In-situ density [kg m-3] (conservative temp, absolute salinity, dbar)."""
108
+ ss, tt, pp = _reduced(ct, sa, p_dbar)
109
+ return _r0(pp) + _anomaly(ss, tt, pp)
110
+
111
+
112
+ def _thermal_expansion_a(ct, sa, p_dbar):
113
+ """Boussinesq thermal expansion a = -dr/dCT [kg m-3 K-1]."""
114
+ ss, tt, pp = _reduced(ct, sa, p_dbar)
115
+ return ((ALP003 * pp
116
+ + ALP012 * tt + ALP102 * ss + ALP002) * pp
117
+ + ((ALP031 * tt + ALP121 * ss + ALP021) * tt
118
+ + (ALP211 * ss + ALP111) * ss + ALP011) * tt
119
+ + ((ALP301 * ss + ALP201) * ss + ALP101) * ss + ALP001) * pp \
120
+ + ((((ALP050 * tt + ALP140 * ss + ALP040) * tt
121
+ + (ALP230 * ss + ALP130) * ss + ALP030) * tt
122
+ + ((ALP320 * ss + ALP220) * ss + ALP120) * ss + ALP020) * tt
123
+ + (((ALP410 * ss + ALP310) * ss + ALP210) * ss + ALP110) * ss + ALP010) * tt \
124
+ + ((((ALP500 * ss + ALP400) * ss + ALP300) * ss + ALP200) * ss + ALP100) * ss + ALP000
125
+
126
+
127
+ def _haline_contraction_b(ct, sa, p_dbar):
128
+ """Boussinesq haline contraction b = dr/dSA [kg m-3 (g/kg)-1]."""
129
+ ss, tt, pp = _reduced(ct, sa, p_dbar)
130
+ b = ((BET003 * pp
131
+ + BET012 * tt + BET102 * ss + BET002) * pp
132
+ + ((BET031 * tt + BET121 * ss + BET021) * tt
133
+ + (BET211 * ss + BET111) * ss + BET011) * tt
134
+ + ((BET301 * ss + BET201) * ss + BET101) * ss + BET001) * pp \
135
+ + ((((BET050 * tt + BET140 * ss + BET040) * tt
136
+ + (BET230 * ss + BET130) * ss + BET030) * tt
137
+ + ((BET320 * ss + BET220) * ss + BET120) * ss + BET020) * tt
138
+ + (((BET410 * ss + BET310) * ss + BET210) * ss + BET110) * ss + BET010) * tt \
139
+ + ((((BET500 * ss + BET400) * ss + BET300) * ss + BET200) * ss + BET100) * ss + BET000
140
+ return b / ss
141
+
142
+
143
+ def drho_dt(ct, sa, p_dbar):
144
+ return -_thermal_expansion_a(ct, sa, p_dbar)
145
+
146
+
147
+ def drho_ds(ct, sa, p_dbar):
148
+ return _haline_contraction_b(ct, sa, p_dbar)
149
+
150
+
151
+ register(EOSBackend(
152
+ id="teos10-poly55",
153
+ density=density,
154
+ drho_dt=drho_dt,
155
+ drho_ds=drho_ds,
156
+ temperature=TemperatureKind.CONSERVATIVE,
157
+ salinity=SalinityKind.ABSOLUTE,
158
+ pressure_unit=PressureUnit.DBAR,
159
+ reference="Roquet et al. (2015), Ocean Modelling, 90, 29-43.",
160
+ description="55-term Boussinesq polynomial approx to TEOS-10 "
161
+ "(Oceananigans / SeawaterPolynomials.jl; MOM6 ROQUET_RHO family).",
162
+ ))
@@ -0,0 +1,81 @@
1
+ """Idealized second-order Roquet equations of state (Oceananigans).
2
+
3
+ Roquet, F., G. Madec, L. Brodeau, J. Nycander (2015): "Defining a Simplified
4
+ yet 'Realistic' Equation of State for Seawater." J. Phys. Oceanogr., 45, Table 3.
5
+ (doi:10.1175/JPO-D-15-0080.1)
6
+
7
+ These are the ``RoquetSeawaterPolynomial(:Linear | :Cabbeling | ...)`` options in
8
+ SeawaterPolynomials.jl / Oceananigans. Each is a second-order polynomial of the
9
+ density anomaly in conservative temperature, absolute salinity, and geopotential
10
+ height Z::
11
+
12
+ rho = rho_ref + R100*S + R010*T + R020*T^2 - R011*T*Z + R200*S^2
13
+ - R101*S*Z + R110*S*T
14
+
15
+ Coefficients ported verbatim from SeawaterPolynomials.jl. Geopotential height is
16
+ taken as ``Z = -p`` (sea pressure in dbar ~ depth in m, positive down), matching
17
+ the convention of the 55-term ``teos10-poly55`` backend. There is no standalone
18
+ Python reference for these, so they are validated structurally (exact analytic
19
+ derivatives checked against finite differences; literal check values).
20
+ """
21
+
22
+ from ..conventions import TemperatureKind, SalinityKind, PressureUnit
23
+ from ..registry import EOSBackend, register
24
+
25
+ DEFAULT_REFERENCE_DENSITY = 1024.6 # kg m-3 (Oceananigans / Roquet default)
26
+
27
+ # (R100, R010, R020, R011, R200, R101, R110) for each coefficient set.
28
+ COEFFICIENTS = {
29
+ "roquet-linear": dict(R100=7.718e-1, R010=-1.775e-1),
30
+ "roquet-cabbeling": dict(R100=7.718e-1, R010=-0.844e-1, R020=-4.561e-3),
31
+ "roquet-cabbeling-thermobaricity": dict(
32
+ R100=7.718e-1, R010=-0.651e-1, R020=-5.027e-3, R011=-2.5681e-5),
33
+ "roquet-freezing": dict(
34
+ R100=7.718e-1, R010=-0.491e-1, R020=-5.027e-3, R011=-2.5681e-5),
35
+ "roquet-second-order": dict(
36
+ R100=8.078e-1, R010=0.182e-1, R020=-4.937e-3, R011=-2.4677e-5,
37
+ R200=-1.115e-4, R101=-8.241e-6, R110=-2.446e-3),
38
+ # Simplest-realistic (Roquet 2015 eq. 17): Cb=0.011, Th=2.5e-5, b0=0.77, T0=-4.5.
39
+ # The constant R000 = -Cb*T0^2/2 (~-0.11 kg m-3) is omitted (no dynamical effect),
40
+ # matching SeawaterPolynomials.jl; this offsets the absolute density by that constant.
41
+ "roquet-simplest-realistic": dict(
42
+ R100=0.77, R010=0.011 * -4.5, R020=-0.011 / 2, R011=-2.5e-5),
43
+ }
44
+
45
+
46
+ def make_roquet_idealized(eos_id, reference_density=DEFAULT_REFERENCE_DENSITY, **coeffs):
47
+ c = {k: 0.0 for k in ("R100", "R010", "R020", "R011", "R200", "R101", "R110")}
48
+ c.update(coeffs)
49
+ R100, R010, R020 = c["R100"], c["R010"], c["R020"]
50
+ R011, R200, R101, R110 = c["R011"], c["R200"], c["R101"], c["R110"]
51
+
52
+ def density(t, s, p):
53
+ z = -p # geopotential height [m] ~ -(sea pressure in dbar)
54
+ anomaly = (R100 * s + R010 * t + R020 * t * t - R011 * t * z
55
+ + R200 * s * s - R101 * s * z + R110 * s * t)
56
+ return reference_density + anomaly
57
+
58
+ def drho_dt(t, s, p):
59
+ z = -p
60
+ return R010 + 2.0 * R020 * t - R011 * z + R110 * s
61
+
62
+ def drho_ds(t, s, p):
63
+ z = -p
64
+ return R100 + 2.0 * R200 * s - R101 * z + R110 * t
65
+
66
+ return EOSBackend(
67
+ id=eos_id,
68
+ density=density,
69
+ drho_dt=drho_dt,
70
+ drho_ds=drho_ds,
71
+ temperature=TemperatureKind.CONSERVATIVE,
72
+ salinity=SalinityKind.ABSOLUTE,
73
+ pressure_unit=PressureUnit.DBAR,
74
+ reference="Roquet et al. (2015), J. Phys. Oceanogr., 45, Table 3.",
75
+ description=f"Idealized Roquet EOS '{eos_id.split('-', 1)[1]}' "
76
+ f"(Oceananigans; rho_ref={reference_density}).",
77
+ )
78
+
79
+
80
+ for _eos_id, _coeffs in COEFFICIENTS.items():
81
+ register(make_roquet_idealized(_eos_id, **_coeffs))