Skip to content

Add TEOS-10 first derivatives of specific volume - #485

Merged
sbrus89 merged 13 commits into
E3SM-Project:developfrom
xylar:omega/add-teos10-alpha-derivatives
Aug 13, 2026
Merged

Add TEOS-10 first derivatives of specific volume#485
sbrus89 merged 13 commits into
E3SM-Project:developfrom
xylar:omega/add-teos10-alpha-derivatives

Conversation

@xylar

@xylar xylar commented Jul 29, 2026

Copy link
Copy Markdown

Add the first derivatives of specific volume with respect to conservative temperature, absolute salinity and pressure, for all three equation of state options. These are needed by the higher-order horizontal pressure gradient, which expands specific volume about a reference state in each layer rather than calling the equation of state at every quadrature point. Nothing calls them yet; this adds the capability and its tests.

The GSW toolbox may only be redistributed without modification, so its derivative routine is not ported or adapted. The derivatives are instead obtained by differentiating the Roquet et al. 2015 polynomial that Omega already carries in Eos.h. GSW-C is used unmodified, through its public API, as an independent check in the unit test.

One commit is answer-changing at roundoff level: the thermal expansion and haline contraction coefficients used by the Brunt-Vaisala frequency are now derived from the new derivatives instead of from a second copy of the same coefficients.


What this adds

  • Eos::computeSpecVolAndDerivs, which fills specific volume and its three first derivatives in a single pass over the equation of state.
  • The SpecVolDCt, SpecVolDSa and SpecVolDP members and their fields in the Eos group, owned and allocated by Eos exactly as SpecVol is.
  • Derivative kernels on Teos10Eos, LinearEos and ConstantEos, so the higher-order pressure gradient will work with any EOS option.
  • Unit tests against GSW-C, plus a finite-difference check.

Units: pressure in is relative pressure in Pa; derivatives out are per degC, per (g/kg), and per Pa (not per dbar).

Eos owns and registers the three derivative arrays alongside SpecVol, so they are available for output like any other EOS field. Nothing computes them yet; PressureGradHighOrder will be the first consumer.

On the GSW-C license

  • The licence permits redistribution "in source and binary forms, without modification". Adapting gsw_specvol_first_derivatives into a Kokkos kernel would be redistribution with modification, so that is not done here. Calling into the library from device code is not an option either.
  • What is done instead: calcPCoeffs and calcRefProfile already contain the published Roquet et al. 2015 coefficients. Differentiating that polynomial analytically is elementary calculus on material Omega already has.
  • GSW-C's role is unchanged from today — an unmodified submodule, called through its public API in EosTest, exactly as it already is for gsw_specvol and gsw_ct_freezing_poly.

No new numerical constants

Worth knowing before reading the diff, because it makes the review much cheaper.

  • Teos10BruntVaisalaFreqSq::calcAlpha/calcBeta already held the differentiated coefficient tables under different names.
  • Before writing any code, the 75-term table was differentiated mechanically and compared against them term by term: all 50 A*** and all 50 B*** coefficients match exactly in decimal arithmetic, since A(i,j,k) = (j+1)·V(i,j+1,k) and B(i,j,k) = (i+1)·V(i+1,j,k).
  • So the tables were moved into the new derivative helpers and the old copy deleted. No constant in this PR is new — the diff relocates and regroups existing ones. The pressure derivative needs no table at all; it reuses the coefficients calcPCoeffs already assembles.

Verification

Maximum relative difference against GSW-C over 216 states (salinity 0–38.5 g/kg, temperature −2 to 35 °C, pressure 0–10000 dbar):

Quantity Agreement
SpecVol 2.4e-14
d(SpecVol)/dCt 3.1e-14
d(SpecVol)/dSa 8.5e-15
d(SpecVol)/dP 2.3e-12
alpha, beta 3.1e-14

Why the pressure derivative is gated more loosely

The initial 1e-12 gate failed for d(SpecVol)/dP above 8000 dbar. The error is GSW-C's, not ours, and it is worth stating explicitly so the looser tolerance does not read as a fudge:

  • Evaluating the exact derivative of the 75-term polynomial in 60-digit arithmetic puts Omega within ~1e-16 of it and GSW-C at ~2e-12, growing with pressure.
  • Rounding the coefficient table to 11 significant digits reproduces GSW-C's error pattern, so its v_P is evaluated from coefficients pre-multiplied by their pressure exponents and rounded. Omega differentiates the full-precision ones.
  • That one comparison is therefore gated at 1e-10, bounding GSW-C's rounding. The finite-difference check pins the value independently of GSW.

The finite-difference check is deliberately redundant with the GSW comparison: it pins the unit convention without reference to GSW, so a pressure derivative that silently became per dbar would fail there too.

The one answer-changing commit

Compute Brunt-Vaisala coefficients from the specific volume derivatives rewrites calcAlpha/calcBeta over the new derivatives and deletes the duplicate tables. Same polynomial, different Horner arrangement, and the normalized salinity is now formed the way calcPCoeffs forms it, so answers move at roundoff:

  • agreement with GSW-C: alpha 2.4e-14 → 3.1e-14, beta 2.4e-14 → 3.2e-14
  • BruntVaisalaFreqSq is gated at 1e-10 and passes unchanged
  • VERTMIX_TEST, which consumes it, passes

A baseline comparison is appropriate rather than a bit-for-bit check.

Note the commit ordering here: the guard test comparing calcAlpha/calcBeta against gsw_specvol_alpha_beta is added in the preceding commit, on purpose. Those two functions were previously exercised only through a single hardcoded BruntVaisalaFreqSq value at a tolerance too loose to catch a mistake in either, so the rewrite lands against a real oracle rather than against itself.

Reading the commits

Each is self-contained and they are meant to be read in order:

  1. Add TEOS-10 first derivatives of specific volume — the derivative helpers. Largest diff, but mostly relocated coefficients.
  2. Add specific volume derivatives for linear and constant EOS — closed form.
  3. Add Eos::computeSpecVolAndDerivs — the dispatch, plus the three arrays it owns and their field registration.
  4. Add unit tests for TEOS-10 specific volume derivatives — including the alpha/beta guard used by commit 5.
  5. Compute Brunt-Vaisala coefficients from the specific volume derivatives — the answer-changing one; deletes ~160 lines of duplicated constants.
  6. Document specific volume derivatives in the EOS guides — dev and user guides.

Testing

  • Full Omega CTest suite on chrysalis (oneapi-ifx, Release): 47/47 pass.
  • omega_pr suite from Polaris.

Out of scope

Everything else in Phase 1 of the higher-order pressure gradient design: the PressureGradHighOrder functor, the mean-preserving vertical reconstruction, the sloping-interface integral, and the VertCoord geopotential prerequisite. Second derivatives are Phase 2.

Checklist

  • Documentation:

  • Linting

  • Building

    • CMake build does not produce any new warnings from changes in this PR
  • Testing

    aurora, oneapi-ifx, mpich

    • CTests Pass
    • Polaris omega_pr Pass

    chrysalis, oneapi-ifx, openmpi

    • CTests Pass
    • Polaris omega_pr Pass

    frontier, craygnu, mpich

    • CTests Pass
    • Polaris omega_pr Pass

    frontier, craygnu-mphipcc, mpich

    • CTests Pass
    • Polaris omega_pr Pass

    pm-cpu, gnu, mpich

    • CTests Pass
    • Polaris omega_pr Pass

    pm-gpu, gnugpu, mpich

    • CTests Pass
    • Polaris omega_pr Pass
  • Provide relevant details in a comment to the PR titled Testing with the following:

    • Which machines CTest unit tests
      have been run on and indicate that are all passing.
    • The Polaris omega_pr test suite
      has passed, using the Polaris e3sm_submodules/Omega baseline
    • Document machine(s), compiler(s), and the build path(s) used for -p for both the baseline (Polaris e3sm_submodules/Omega) and the PR build
    • Indicate "All tests passed" or document failing tests
    • Document testing used to verify the changes including any tests that are added/modified/impacted.
  • New tests:

    • CTest unit tests for new features have been added per the approved design.
  • Stealth Features

    • If any stealth features are included in the PR, please confirm that they have been documented.

@xylar
xylar requested review from katsmith133 and sbrus89 July 29, 2026 15:14
@xylar xylar self-assigned this Jul 29, 2026
@xylar xylar added the enhancement New feature or request label Jul 29, 2026
@xylar

xylar commented Jul 29, 2026

Copy link
Copy Markdown
Author

This work is needed for the finite-volume pressure-gradient approach described in #448.

@xylar

xylar commented Jul 29, 2026

Copy link
Copy Markdown
Author

Testing

CTest unit tests:

  • Machine: chrysalis
  • Compiler: oneapi-ifx
  • Build type: Release
  • Result: All tests passed
  • Log: /lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260729/omega-teos10-derivs/build/ctests.log

Polaris omega_pr suite

  • Baseline workdir: /lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260729/omega-pr-develop/
  • Baseline build: /lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260729/omega-pr-develop/build
  • PR build: /lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260729/omega-teos10-derivs/build
  • PR workdir: /lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260729/omega-teos10-derivs
  • Machine: chrysalis
  • Partition: compute
  • Compiler: oneapi-ifx
  • Build type: Release
  • Log: /lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260729/omega-teos10-derivs/polaris_omega_pr.o1259933
  • Result: All tests passed

@xylar

xylar commented Jul 29, 2026

Copy link
Copy Markdown
Author

@sbrus89, please have a look and see if the organization is what you wanted or if you have suggestions.

@katsmith133, could you have a look, since this makes some modifications to your previous work? I'd appreciate feedback on the organization and testing.

@xylar

xylar commented Jul 29, 2026

Copy link
Copy Markdown
Author

@sbrus89 and @katsmith133, hold off for now on looking this over. I made a bad call in the design phase. I thought SpecVol lived outside of the Eos class and therefore designed things so the derivatives did, too. But that's a weird inconsistency that I'd like to fix. I'll keep you posted.

@xylar
xylar force-pushed the omega/add-teos10-alpha-derivatives branch from 494e8fd to 0f80640 Compare July 29, 2026 16:09
@xylar

xylar commented Jul 29, 2026

Copy link
Copy Markdown
Author

Retesting after reorg

CTest unit tests:

  • Machine: chrysalis
  • Compiler: oneapi-ifx
  • Build type: Release
  • Result: All tests passed
  • Log: /lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260729/omega-teos10-derivs/build/ctests.log

Polaris omega_pr suite

  • Baseline workdir: /lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260729/omega-pr-fix-explicit-bot-drag
  • Baseline build: /lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260729/omega-pr-fix-explicit-bot-drag/build
  • PR build: /lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260729/omega-teos10-derivs/build
  • PR workdir: /lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260729/omega-pr-teos10-derivs2
  • Machine: chrysalis
  • Partition: compute
  • Compiler: oneapi-ifx
  • Build type: Release
  • Log: /lcrc/group/e3sm/ac.xylar/polaris_1.0/chrysalis/test_20260729/omega-pr-teos10-derivs2/polaris_omega_pr.o1260097
  • Result: All tests passed

The reference here is what is now develop (now that #478 is merged)

@xylar

xylar commented Jul 29, 2026

Copy link
Copy Markdown
Author

@katsmith133 and @sbrus89, sorry for the moving target. The reorganization is now in.

Comment thread components/omega/doc/devGuide/EOS.md
@katsmith133

Copy link
Copy Markdown

I can review this on Sunday

@xylar
xylar force-pushed the omega/add-teos10-alpha-derivatives branch from ab84f77 to 222301e Compare August 1, 2026 13:33
@xylar
xylar marked this pull request as ready for review August 2, 2026 13:57

@katsmith133 katsmith133 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great work @xylar!

I think this PR is almost there. I think most of my comments are probably due to me not understanding something, so just some explanation/clarification would be helpful.

I am working on testing on Frontier. Ctests for both craygnu and craygnu-mphipcc have passed. I am working on getting results for the omega_pr suite. Do you want me to check them off on the PR description as I go, or do you prefer to not check them off until we are testing the final code implementation?

Also, thanks for adding in more rigorous testing and making sure we don't violate GSW-C's terms. Are there any other currently applied functions that you noticed we need to make this modification for? I think some of the frazil helper functions that I am implementing will need close consideration as they are not explicitly in Roquet et al., 2014.

Comment thread components/omega/doc/devGuide/EOS.md
Comment thread components/omega/src/ocn/Eos.h Outdated
Comment thread components/omega/src/ocn/Eos.h Outdated
Comment thread components/omega/src/ocn/Eos.h Outdated
Comment thread components/omega/test/ocn/EosTest.cpp
Comment thread components/omega/test/ocn/EosTest.cpp
@katsmith133

Copy link
Copy Markdown

Testing on Frontier

CTest unit tests:

  • Machine: Frontier
  • Compiler: craygnu, craygnu-mphipcc
  • Build type: Release
  • Result: All tests passed

Polaris omega_pr suite

  • Baseline workdir: /lustre/orion/cli115/scratch/kmsmith/polaris_testing_pr485//baseline_o\ mega_pr
  • Baseline build: /ccs/home/kmsmith/polaris-main/omega_build
  • PR build: /ccs/home/kmsmith/polaris-pr485/omega_build
  • PR workdir: /lustre/orion/cli115/scratch/kmsmith/polaris_testing_pr485/pr485_omega_pr
  • Machine: frontier
  • Partition: batch
  • Compiler: craygnu-mphipcc
  • Build type: Release
  • Log: not found
  • Result: All tests passed

@xylar

xylar commented Aug 2, 2026

Copy link
Copy Markdown
Author

@katsmith133, thanks so much for your review and testing, particularly over the weekend! I'll reply to your comments shortly.

@xylar

xylar commented Aug 2, 2026

Copy link
Copy Markdown
Author

Are there any other currently applied functions that you noticed we need to make this modification for? I think some of the frazil helper functions that I am implementing will need close consideration as they are not explicitly in Roquet et al., 2014.

Good question. I think there are 4 existing functions where we might need to re-implement them without directly referencing our using the GSW code: calcGibbsDerivPt0Pt0 (cf. gsw_gibbs_pt0_pt0), calcPtFromCt (cf. gsw_pt_from_ct), calcCtFromPt (cf. gsw_ct_from_pt), and calcCtFreezing (cf. gsw_ct_freezing_poly). We should not use GSW as the direct reference and should not say, "from GSW package". None of these are in Roquet et al. 2015, so we need to figure out the right way to port them.

They're untouched by this PR and it wouldn't make sense to address them here (you weren't suggesting it was). But an issue would probably be a good idea. I'll try to find time.

For the frazil helpers, the pattern I'd suggest is the one used here: implement from the published equations rather than from the GSW-C source, cite the paper in the comment (the GSW documentation names the source reference for each routine, which makes this straightforward), and use GSW-C unmodified through its public API for testing. That keeps following their license.

@katsmith133

Copy link
Copy Markdown

Are there any other currently applied functions that you noticed we need to make this modification for? I think some of the frazil helper functions that I am implementing will need close consideration as they are not explicitly in Roquet et al., 2014.

Good question. I think there are 4 existing functions where we might need to re-implement them without directly referencing our using the GSW code: calcGibbsDerivPt0Pt0 (cf. gsw_gibbs_pt0_pt0), calcPtFromCt (cf. gsw_pt_from_ct), calcCtFromPt (cf. gsw_ct_from_pt), and calcCtFreezing (cf. gsw_ct_freezing_poly). We should not use GSW as the direct reference and should not say, "from GSW package". None of these are in Roquet et al. 2015, so we need to figure out the right way to port them.

They're untouched by this PR and it wouldn't make sense to address them here (you weren't suggesting it was). But an issue would probably be a good idea. I'll try to find time.

For the frazil helpers, the pattern I'd suggest is the one used here: implement from the published equations rather than from the GSW-C source, cite the paper in the comment (the GSW documentation names the source reference for each routine, which makes this straightforward), and use GSW-C unmodified through its public API for testing. That keeps following their license.

Ok, great, those were the ones I was thinking we would need to re-do. We touch some of them with the thermo-coupling/frazil codes, so perhaps I can address some of them there. Though I think just getting those PRs in is the higher priority. So an issue and a later PR to address them all might be good.

Beyond that, I think you have addressed all of my comments and questions. Is there anything else you'd like for me to do to help with this PR? Testing on PM, re-testing on Frontier, etc? Those are easy enough that I should be able to do them while at the meeting this week.

@sbrus89 sbrus89 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xylar, the organization of this looks great to me. I will do some testing over the rest of the week.

Comment thread components/omega/src/ocn/Eos.cpp Outdated
@xylar
xylar force-pushed the omega/add-teos10-alpha-derivatives branch from c508b3a to 48ce21e Compare August 6, 2026 08:28
@xylar

xylar commented Aug 6, 2026

Copy link
Copy Markdown
Author

Retesting

CTest unit tests:

  • Machine: chrysalis
  • Compiler: intel
  • Build type: Release
  • Result: All tests passed
  • Log: /lcrc/group/e3sm/ac.xylar/polaris_1.1/chrysalis/test_20260806/omega-pr-teos10-derivs/build/ctests.log

Polaris omega_pr suite

  • Baseline workdir: /lcrc/group/e3sm/ac.xylar/polaris_1.1/chrysalis/test_20260806/omega-pr-develop
  • Baseline build: /lcrc/group/e3sm/ac.xylar/polaris_1.1/chrysalis/test_20260806/omega-pr-develop/build
  • PR build: /lcrc/group/e3sm/ac.xylar/polaris_1.1/chrysalis/test_20260806/omega-pr-teos10-derivs/build
  • PR workdir: /lcrc/group/e3sm/ac.xylar/polaris_1.1/chrysalis/test_20260806/omega-pr-teos10-derivs
  • Machine: chrysalis
  • Partition: compute
  • Compiler: intel
  • Build type: Release
  • Log: /lcrc/group/e3sm/ac.xylar/polaris_1.1/chrysalis/test_20260806/omega-pr-teos10-derivs/polaris_omega_pr.o1264903
  • Result:
    • Failures (4 of 17):
      • ocean/column/vmix_stable
      • ocean/column/vmix_unstable
      • ocean/column/ekman
      • ocean/column/inertial

The failures are known and unrelated to this PR.

@xylar

xylar commented Aug 7, 2026

Copy link
Copy Markdown
Author

@katsmith133 and @sbrus89, anything else I can do to help this along?

@katsmith133

Copy link
Copy Markdown

@xylar Let me retest with the latest changes and then I am ok with approving. I'll test on PM, if @sbrus89 wants to take on Frontier?

@sbrus89

sbrus89 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Sounds good @katsmith133, thanks!

@sbrus89

sbrus89 commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

I tested a local merge of this PR on Frontier. There were machine-precision level diffs with the ocean/column/horiz_press_grad/salinity_gradient tests with craygnu. There were small initial machine-precision level diffs (that grow in time) with the ocean/planar/overflow/nonlinear/pstar/smoke_test_horiz_adv_order_4_del4 test as well. I'm not sure what would be causing these. Everything was BFB with craygnu-mphicc. The ocean/column/ekman fail is unrelated to this PR (E3SM-Project/polaris#699).

CTests pass with both compilers.

Polaris omega_pr suite

  • Baseline workdir: /ccs/home/brus/run/polaris_baseline_omega_pr_omega_craygnu_mpich/
  • Baseline build: /ccs/home/brus/run/polaris_baseline_omega_pr_omega_craygnu_mpich/build
  • PR build: /ccs/home/brus/run/polaris_teos10_deriv_omega_pr_omega_craygnu_mpich/build
  • PR workdir: /ccs/home/brus/run/polaris_teos10_deriv_omega_pr_omega_craygnu_mpich
  • Machine: frontier
  • Partition: batch
  • Compiler: craygnu
  • Build type: Release
  • Log: not found
  • Result:
    • Failures (1 of 18):
      • ocean/column/ekman
    • Diffs (2 of 18):
      • ocean/column/horiz_press_grad/salinity_gradient
      • ocean/planar/overflow/nonlinear/pstar/smoke_test_horiz_adv_order_4_del4

Example diffs for ocean/column/horiz_press_grad/salinity_gradient:

NormalVelocityTend   Time index: 0
0:  l1: 2.30975720320881e-16  l2: 1.60844729569124e-16  linf: 1.13732807893729e-16

NormalVelocityTend   Time index: 0
0:  l1: 1.13570177567857e-17  l2: 1.88355750843331e-18  linf: 5.42101086242752e-19

NormalVelocityTend   Time index: 0
0:  l1: 2.66171633345191e-17  l2: 3.85916390982992e-18  linf: 1.30104260698261e-18

NormalVelocityTend   Time index: 0
0:  l1: 5.76255572258414e-17  l2: 6.55547380446152e-18  linf: 1.73472347597681e-18

Diffs for ocean/planar/overflow/nonlinear/pstar/smoke_test_horiz_adv_order_4_del4:

temperature          Time index: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47
2:  l1: 2.48689957516035e-14  l2: 1.06581410364015e-14  linf: 7.10542735760100e-15
...
47:  l1: 1.43245656731850e-05  l2: 5.88218664734571e-07  linf: 5.36648840920861e-08

salinity             Time index: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47
2:  l1: 6.39488462184090e-14  l2: 2.75192018236753e-14  linf: 1.42108547152020e-14
...
47:  l1: 4.71190730877424e-09  l2: 1.73867644148174e-11  linf: 2.91322521661641e-13

normalVelocity       Time index: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47
2:  l1: 9.54981764489356e-11  l2: 4.33101410424428e-11  linf: 2.96601296989737e-11
...
47:  l1: 4.46142633643401e-01  l2: 1.19981166052409e-02  linf: 1.90441611361928e-03

Polaris omega_pr suite

  • Baseline workdir: /ccs/home/brus/run/polaris_baseline_omega_pr_omega_craygnu-mphipcc_mpich/
  • Baseline build: /ccs/home/brus/run/polaris_baseline_omega_pr_omega_craygnu-mphipcc_mpich/build
  • PR build: /ccs/home/brus/run/polaris_teos10_deriv_omega_pr_omega_craygnu-mphipcc_mpich/build
  • PR workdir: /ccs/home/brus/run/polaris_teos10_deriv_omega_pr_omega_craygnu-mphipcc_mpich
  • Machine: frontier
  • Partition: batch
  • Compiler: craygnu-mphipcc
  • Build type: Release
  • Log: not found
  • Result:
    • Failures (1 of 18):
      • ocean/column/ekman

@xylar

xylar commented Aug 7, 2026

Copy link
Copy Markdown
Author

@sbrus89 even the small diffs are unexpected. I will have to think about whether there's an explanation for them.

xylar and others added 13 commits August 8, 2026 07:28
Add the first derivatives of the specific volume with respect to conservative
temperature, absolute salinity and pressure to the Teos10Eos functor, as
needed by the higher-order pressure gradient (PGradHighOrder.md 3.3).

The GSW-C license permits redistribution only without modification, so
gsw_specvol_first_derivatives cannot be adapted into Kokkos device code, and
calling into the library from a device kernel is not possible in any case.
The derivatives here are instead the analytic derivatives of the Roquet et
al. 2015 75-term polynomial that calcPCoeffs and calcRefProfile already
carry, differentiated with respect to each of the three normalized variables
and converted back with the chain rule.

The temperature and salinity coefficient tables are the same values already
present in Teos10BruntVaisalaFreqSq::calcAlpha and calcBeta, regrouped by
power of pressure so they follow the calcPCoeffs/calcDelta split. That they
are the same values was verified independently before writing this code, by
differentiating the 75-term table mechanically and comparing term for term:
all 50 A and all 50 B coefficients agree exactly in decimal arithmetic, since
A(i,j,k) = (j+1) V(i,j+1,k) and B(i,j,k) = (i+1) V(i+1,j,k). The duplication
that leaves behind is removed in a later commit.

The pressure derivative needs no new constants at all, being formed from the
pressure coefficients calcPCoeffs already assembles, plus the derivative of
the reference profile. That last term is easy to overlook and is not
optional: it supplies about 99 percent of d(SpecVol)/dP.

Also hoist the polynomial normalization constants to static members, so the
specific volume and its derivatives are guaranteed to be evaluated at the
same normalized state rather than at two spellings of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Give LinearEos and ConstantEos the same calcSpecVolDerivs entry point the
TEOS-10 functor now has, so the higher-order pressure gradient can be run
with any of the three equations of state rather than only with TEOS-10.

Both are closed form. For the linear EOS the derivatives with respect to
conservative temperature and absolute salinity are -DRhodT and -DRhodS times
the square of the specific volume, and there is no pressure dependence at
all; for the constant EOS all three vanish. The idealized Polaris cases that
use these options therefore get an exactly known reference to test the
pressure gradient against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add the array-level entry point that the higher-order pressure gradient will
call, dispatching on the configured EOS to the calcSpecVolDerivs kernel of
the corresponding functor and filling the specific volume and its three
first derivatives in one pass.

Eos owns the SpecVolDCt, SpecVolDSa and SpecVolDP arrays just as it owns
SpecVol, allocating them in the constructor and registering them as fields in
the Eos group so they can be written to a stream. Their valid range spans the
full range of Real rather than starting at zero, since the salinity derivative
is negative everywhere and the temperature derivative is negative in cold,
nearly fresh water.

Since computeSpecVolAndDerivs fills SpecVol as well, it replaces a call to
computeSpecVol rather than accompanying one. The two are kept separate because
the derivatives roughly double the TEOS-10 arithmetic per cell and layer, and
only the higher-order pressure gradient needs them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Check the new derivatives against the unmodified GSW-C library over a cross
product of 216 states spanning the oceanographic range and its corners,
including fresh water, sub-zero temperatures and the full pressure range.
Add an array-level test over the mesh with a state that varies with depth,
so the device kernel and the vertical chunking are exercised over a range of
values rather than a single one, and closed-form tests for the linear and
constant options.

Also compare the thermal expansion and haline contraction coefficients used
by the Brunt-Vaisala frequency against gsw_specvol_alpha_beta. Those two
functions were previously exercised only through a single hardcoded
BruntVaisalaFreqSq value at a tolerance too loose to catch a mistake in
either; the check is added before the following commit rewrites them.

Measured agreement with GSW-C: 2.4e-14 for the specific volume, 3.1e-14 for
the temperature derivative, 8.5e-15 for the salinity derivative, and 2.4e-14
for alpha and beta.

The pressure derivative agrees only to 2.3e-12 and is gated separately at
1e-10. The discrepancy is GSW-C's: its v_P comes from a table of coefficients
pre-multiplied by their pressure exponents and rounded, so it departs from
the exact derivative of the 75-term polynomial by about 2e-12 at 10000 dbar,
growing with pressure. Evaluating the exact derivative in 60-digit arithmetic
puts the Omega value within 1e-16 of it and GSW-C's at 1.8e-12, and rounding
the coefficient table to 11 digits reproduces GSW-C's error pattern. The
looser gate therefore bounds GSW-C's rounding rather than ours.

Add a finite-difference check as well. It is redundant with the GSW-C
comparison while that library is present and correct, which is the point: it
pins the unit convention of the Omega interface, per degC, per (g/kg) and per
Pa, without reference to GSW, and would catch a pressure derivative that
silently became per dbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rewrite Teos10BruntVaisalaFreqSq::calcAlpha and calcBeta over the TEOS-10
derivative helpers and delete the A and B coefficient tables, which were a
second copy of the same 100 constants. Alpha is the temperature derivative of
the specific volume divided by the specific volume and beta is minus the
salinity derivative divided by it, so both follow directly. The coefficient
assemblers are made static, since they carry no state, so the frequency
functor can reach them without holding an equation of state instance.

This changes answers at roundoff level: the same polynomial is evaluated in
a different Horner arrangement, and the normalized salinity is now formed as
sqrt((Sa + DeltaS) / SaNorm) as calcPCoeffs does it rather than from a
separately rounded reciprocal. Measured against the GSW-C library over the
216 test states, the maximum relative difference moves from 2.4e-14 to 3.1e-14
for alpha and from 2.4e-14 to 3.2e-14 for beta. BruntVaisalaFreqSq is gated at
1e-10 against a hardcoded value and continues to pass unchanged.

The guard test added in the previous commit is what makes this safe: alpha and
beta are now pinned to gsw_specvol_alpha_beta over the full state range, so a
mistake in the rewrite fails there rather than hiding inside the single
frequency value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Describe computeSpecVolAndDerivs in the developer guide: the signature, the
unit convention, that the results land in Eos members registered as fields in
the Eos group, that it replaces rather than accompanies computeSpecVol since
it fills SpecVol too, and that the Brunt-Vaisala expansion and contraction
coefficients are now derived from them. Record the licensing position, since
it is the reason the implementation looks the way it does, and the measured
agreement with GSW-C, including why the pressure derivative agrees less well
than the other two.

Add a shorter user guide section noting that the derivatives exist for all
three EOS options, that they carry no configuration of their own, that they
cost nothing unless a scheme that needs them is enabled, and that they are
available for output like SpecVol.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rename the single-state entry point to calcSpecVolAndDerivsAtPoint and the
vertical-chunk entry point to calcSpecVolAndDerivsInChunk, so the two are
told apart at the call site rather than by the presence of "And" in the
name. Document what each is for.
Say explicitly that the higher-order pressure gradient will need the
derivatives every time step and that computeSpecVolAndDerivs then replaces
the computeSpecVol call, while configurations that do not use it keep the
cheaper call.  Note why there is no displaced counterpart and what adding
one would take.
The Brunt-Vaisala frequency evaluates alpha and beta at the interface, from
temperature, salinity and pressure averaged across the two adjacent layers,
while SpecVolDCt and SpecVolDSa hold the derivatives at the layer centers.
Record that in the calcAlpha and calcBeta comments so the duplication does
not look accidental.
Both compare against GSW-C, but testEosTeos10Derivs covers the array-level
machinery -- dispatch, chunking, layer masking, member arrays and field
registration -- on one realistic profile, while checkValueGswcSpecVolDerivs
covers the polynomial itself point by point at the corners of the
oceanographic range.  Say so in each test's comment.
AuxiliaryState::computeMomVertAux is the only caller of computeSpecVol;
computeBruntVaisalaFreqSq consumes the SpecVol array rather than recomputing
it, and computeSpecVolDisp is a separate evaluation.  Describe the actual
call graph, and give the two reasons the plain computeSpecVol is still
wanted: the centered pressure gradient is the config default, and the
VertMix refresh of SpecVol feeds nothing that reads the derivatives.
Vertical chunking is on its way out of Omega (PR E3SM-Project#473 removes it from the
auxiliary variables and tendency terms), so this branch should not add more
of it. The EOS is not touched by that PR, so remove chunking here across the
board rather than leaving the new specific volume derivative code to be
converted later.

The functors now follow the same pattern as the de-chunked auxiliary
variables: each takes a TeamMember and a cell index and loops over the
active layers with

    parallelForInner(Team, Range{KMin, KMax}, INNER_LAMBDA(int K) { ... });

so the callers in Eos.cpp reduce to a single call inside parallelForOuter,
with no vertRangeChunked and no inner loop over chunks.

Dropping the chunk loop also removes VecLength from the TEOS-10 polynomial
helpers. calcPCoeffs, calcPCoeffsDTt, calcPCoeffsDSs, calcDelta,
calcDeltaDeriv and calcDeltaDP took arrays sized 6 * VecLength or
5 * VecLength together with a KVec index into them; they now take plain
[6] and [5] arrays and no index. That storage was never shared between
layers -- the coefficients are recomputed for every layer -- so nothing is
lost, and the point-wise calcSpecVolAndDerivsAtPoint and the calcAlpha and
calcBeta helpers no longer allocate VecLength times more stack than they
use.

With the chunk loop gone, the array-level derivative routine is exactly
calcSpecVolAndDerivsAtPoint evaluated at each cell and layer, so it now
calls it instead of repeating the polynomial evaluation. That leaves a
single implementation of the TEOS-10 derivatives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The InChunk suffix named an implementation detail that no longer exists,
and it was flagged in review as unusual and potentially confusing. The
auxiliary variables name these routines for the mesh element they act on
rather than for the vertical loop structure -- computeVarsOnCell,
computeVarsOnEdge, computeVarsOnVertex -- and keep those names through the
removal of chunking, so follow that convention here.

The point-wise entry point keeps the name calcSpecVolAndDerivsAtPoint; the
pair now reads as scalars in and out versus arrays over the mesh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@xylar
xylar force-pushed the omega/add-teos10-alpha-derivatives branch from 48ce21e to fa2ffdf Compare August 8, 2026 11:28
@xylar

xylar commented Aug 8, 2026

Copy link
Copy Markdown
Author

@sbrus89, I'm also seeing the same diffs as you. I imagine some order of operations changed on craygnu on Frontier. I think we should bless the change since it's clearly at machine round-off.

@xylar

xylar commented Aug 8, 2026

Copy link
Copy Markdown
Author

Testing on Frontier (CPU)

Polaris omega_pr suite

  • Baseline workdir: /lustre/orion/cli115/scratch/xylar/polaris_1.1/frontier/test_20260808/omega-pr-develop-craygnu
  • Baseline build: /lustre/orion/cli115/scratch/xylar/polaris_1.1/frontier/test_20260808/omega-pr-develop-craygnu/build
  • PR build: /lustre/orion/cli115/scratch/xylar/polaris_1.1/frontier/test_20260808/omega-pr-teos10-derivs/build
  • PR workdir: /lustre/orion/cli115/scratch/xylar/polaris_1.1/frontier/test_20260808/omega-pr-teos10-derivs
  • Machine: frontier
  • Partition: batch
  • Compiler: craygnu
  • Build type: Release
  • Log: /lustre/orion/cli115/scratch/xylar/polaris_1.1/frontier/test_20260808/omega-pr-teos10-derivs/polaris_omega_pr.o5204160
  • Result:
    • Failures (1 of 22):
      • ocean/column/ekman
    • Diffs (3 of 22):
      • ocean/column/horiz_press_grad/salinity_gradient
      • ocean/planar/overflow/nonlinear/pstar/smoke_test_horiz_adv_order_4_del4
      • ocean/planar/seamount/nonlinear/sigma/short

So also diffs in the seamount test with TEOS-10, no surprise given the other diffs.

@katsmith133

Copy link
Copy Markdown

Ctests pass on PM-CPU and PM-GPU. Waiting for LCRC to come back up (down for maintenance today) before I can finish the omega_pr test suite on PM (suite requires a global mesh download from LCRC).

@katsmith133

Copy link
Copy Markdown

Testing on Perlmutter (CPU)

Polaris omega_pr suite

  • Baseline workdir: /pscratch/sd/k/katsmith/polaris_testing_pr485//baseline_omega_pr
  • Baseline build: /pscratch/sd/k/katsmith/polaris-main/omega_build
  • PR build: /pscratch/sd/k/katsmith/polaris-pr485/omega_build
  • PR workdir: /pscratch/sd/k/katsmith/polaris_testing_pr485/pr485_omega_pr
  • Machine: pm-cpu
  • Compiler: gnu
  • Build type: Release
  • Log: not found
  • Result:
    • Failures (1 of 22):
      • ocean/column/ekman

@katsmith133 katsmith133 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved upon visual inspection and testing by myself, @sbrus89, and @xylar

@xylar

xylar commented Aug 11, 2026

Copy link
Copy Markdown
Author

Thank you @katsmith133!

@sbrus89

sbrus89 commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Local merge passes CTests and omega_pr with craygnu and craygnu-mphipcc on Frontier. The craygnu diffs are expected based on conversation above and should be blessed. The ekman fail is unrelated to this PR.

Polaris omega_pr suite

  • Baseline workdir: /ccs/home/brus/run/polaris_baseline_omega_pr_omega_craygnu_mpich/
  • Baseline build: /ccs/home/brus/run/polaris_baseline_omega_pr_omega_craygnu_mpich/build
  • PR build: /ccs/home/brus/run/polaris_teos10_deriv_omega_pr_omega_craygnu_mpich/build
  • PR workdir: /ccs/home/brus/run/polaris_teos10_deriv_omega_pr_omega_craygnu_mpich
  • Machine: frontier
  • Partition: batch
  • Compiler: craygnu
  • Build type: Release
  • Log: not found
  • Result:
    • Failures (1 of 22):
      • ocean/column/ekman
    • Diffs (3 of 22):
      • ocean/column/horiz_press_grad/salinity_gradient
      • ocean/planar/overflow/nonlinear/pstar/smoke_test_horiz_adv_order_4_del4
      • ocean/planar/seamount/nonlinear/sigma/short

Polaris omega_pr suite

  • Baseline workdir: /ccs/home/brus/run/polaris_baseline_omega_pr_omega_craygnu-mphipcc_mpich/
  • Baseline build: /ccs/home/brus/run/polaris_baseline_omega_pr_omega_craygnu-mphipcc_mpich/build
  • PR build: /ccs/home/brus/run/polaris_teos10_deriv_omega_pr_omega_craygnu-mphipcc_mpich/build
  • PR workdir: /ccs/home/brus/run/polaris_teos10_deriv_omega_pr_omega_craygnu-mphipcc_mpich
  • Machine: frontier
  • Partition: batch
  • Compiler: craygnu-mphipcc
  • Build type: Release
  • Log: not found
  • Result:
    • Failures (1 of 22):
      • ocean/column/ekman

Also passes CTests and omega_pr with gnu and gnugpu and pm-cpu/pm-gpu:

Polaris omega_pr suite

  • Baseline workdir: /global/homes/s/sbrus/scratch/polaris_baseline_omega_pr_omega_gpu/
  • Baseline build: /global/homes/s/sbrus/scratch/polaris_baseline_omega_pr_omega_gpu/build
  • PR build: /global/homes/s/sbrus/scratch/polaris_teos10_deriv_omega_pr_omega_gpu/build
  • PR workdir: /global/homes/s/sbrus/scratch/polaris_teos10_deriv_omega_pr_omega_gpu
  • Machine: pm-gpu
  • Compiler: gnugpu
  • Build type: Release
  • Log: /global/homes/s/sbrus/scratch/polaris_teos10_deriv_omega_pr_omega_gpu/polaris_omega_pr.o56895357
  • Result:
    • Failures (1 of 22):
      • ocean/column/ekman

Polaris omega_pr suite

  • Baseline workdir: /global/homes/s/sbrus/scratch/polaris_baseline_omega_pr_omega_cpu/
  • Baseline build: /global/homes/s/sbrus/scratch/polaris_baseline_omega_pr_omega_cpu/build
  • PR build: /global/homes/s/sbrus/scratch/polaris_teos10_deriv_omega_pr_omega_cpu/build
  • PR workdir: /global/homes/s/sbrus/scratch/polaris_teos10_deriv_omega_pr_omega_cpu
  • Machine: pm-cpu
  • Compiler: gnu
  • Build type: Release
  • Log: not found
  • Result:
    • Failures (1 of 22):
      • ocean/column/ekman

@sbrus89 sbrus89 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved based on inspection and testing above. Thanks @xylar!

@sbrus89
sbrus89 merged commit 7d4d8bf into E3SM-Project:develop Aug 13, 2026
1 check passed
@xylar
xylar deleted the omega/add-teos10-alpha-derivatives branch August 14, 2026 06:34
@xylar

xylar commented Aug 14, 2026

Copy link
Copy Markdown
Author

@sbrus89, thank you so much for your testing and review, and coordinating these Omega merges.

xylar pushed a commit to E3SM-Project/polaris that referenced this pull request Aug 14, 2026
 - Brings in non-B4B behavior on Frontier (craygnu) due to the
   TEOS10 derivative PR: E3SM-Project/Omega#485
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants