eos_buoyancy_coeffs Subroutine

public pure elemental subroutine eos_buoyancy_coeffs(eos, T, S, p, alpha_T, beta_S)

Thermal-expansion and haline-contraction coefficients of the ACTIVE equation of state at a point, in the SAME DIMENSIONAL convention the eos_t members alpha_T / beta_S carry:

alpha_T = −∂ρ/∂T (kg/m³ per degC; > 0 — warm water expands) beta_S = +∂ρ/∂S (kg/m³ per PSU; > 0 — salt contracts)

so that ρ ≈ ρ_ref − alpha_T·ΔT + beta_S·ΔS to first order, matching the linear branch of eos_density_point term for term. They are NOT the fractional α = −(1/ρ)∂ρ/∂T; divide by ρ (or by ρ₀ in a Boussinesq consumer) for that. ocean_cavity_const_t deliberately carries its own FRACTIONAL ISOMIP+ pair — see its docstring.

Why this exists. Seawater’s thermal expansion is strongly state-dependent: it collapses toward zero near the freezing point and grows with pressure (thermobaricity). At (−1.9 degC, 34.5 PSU) Wright (1997) gives roughly a quarter of the 10 degC surface value, and about twice that again by 1000 dbar. Any consumer that sizes a buoyancy flux with a CONSTANT α while the dyn-core integrates a NONLINEAR ρ is inconsistent with its own density field — worst under an ice shelf, where the constant can mis-magnitude (and in the cold-fresh corner mis-sign) the melt-driven surface buoyancy flux that sets the boundary layer.

Exactness, per branch — this is load-bearing for bit-identity. Each branch is differentiated in CLOSED FORM from that variant’s own density expression; no branch round-trips through −ρ²·dSV/dX, because for the LINEAR branch that would return ρ(T,S)²·alpha_T/ρ₀² — equal to eos%alpha_T only to round-off. As written the linear branch returns the handle members BIT-FOR-BIT, which is what lets a consumer routed through here stay byte-identical to the constant path under eos = "linear".

  • linear — ρ = ρ₀ + β(S−S_ref) − α(T−T_ref) ⇒ the members themselves, exactly; p is ignored (no pressure dependence).
  • Wright (1997) — ρ = P/D with P = p + p₀(T,S) and D = λ(T,S) + α₀(T,S)·P, so by the quotient rule ∂ρ/∂X = (P_X·D − P·D_X)/D², D_X = λ_X + α₀_X·P + α₀·P_X, all three polynomials from Table A1. Algebraically identical to −ρ²·dSV/dX from eos_specvol_derivs, evaluated once.
  • Roquet et al. (2015) SpV — ρ = 1/SV ⇒ ∂ρ/∂X = −SV_X/SV², reusing the SINGLE fused roquet_spv_point evaluation that already returns SV and both model-variable sensitivities (no duplicated polynomial — see eos_specvol_derivs).

The else is unreachable-by-contract: eos_validate guarantees eos%variant is in the device-callable set at configure time (device code cannot error stop), exactly as for eos_density_point / eos_specvol_derivs.

elemental (all dummies scalar, eos_t a flat POD by value) so a host-side sweep can evaluate whole arrays in one reference, and !$acc routine seq so a do concurrent kernel can call it — the same shape eos_freezing_point already ships.

Arguments

Type IntentOptional Attributes Name
type(eos_t), intent(in) :: eos

Shared EOS handle (variant + scalar coeffs), by value.

real(kind=wp), intent(in) :: T

Potential temperature (degC) and practical salinity (PSU) — the model-prognostic pair, per the TS_POT_PRAC convention.

real(kind=wp), intent(in) :: S

Potential temperature (degC) and practical salinity (PSU) — the model-prognostic pair, per the TS_POT_PRAC convention.

real(kind=wp), intent(in) :: p

Pressure (Pa) at which to evaluate. Which pressure is the CONSUMER’s choice and is documented at each call site: a surface buoyancy flux wants the top of the column (multilayer_state_t%p_top under &ocean_psurf_nml in_eos, else eos%p_ref); an interior-interface closure wants the true in-situ hydrostatic pressure there. Never a horizontally-varying value fed back into ms%rho_layer — see the p_top seam contract in src/core/ocean/README.md.

real(kind=wp), intent(out) :: alpha_T

−∂ρ/∂T (kg/m³ per degC).

real(kind=wp), intent(out) :: beta_S

+∂ρ/∂S (kg/m³ per PSU).


Calls

proc~~eos_buoyancy_coeffs~~CallsGraph proc~eos_buoyancy_coeffs eos_buoyancy_coeffs proc~roquet_spv_point roquet_spv_point proc~eos_buoyancy_coeffs->proc~roquet_spv_point

Called by

proc~~eos_buoyancy_coeffs~~CalledByGraph proc~eos_buoyancy_coeffs eos_buoyancy_coeffs proc~eos_density_derivs eos_density_derivs proc~eos_density_derivs->proc~eos_buoyancy_coeffs proc~vmix_kpp_overlay_impl vmix_kpp_overlay_impl proc~vmix_kpp_overlay_impl->proc~eos_buoyancy_coeffs proc~vmix_split_ddiff_eos_impl vmix_split_ddiff_eos_impl proc~vmix_split_ddiff_eos_impl->proc~eos_buoyancy_coeffs proc~bbl_faces_impl bbl_faces_impl proc~bbl_faces_impl->proc~eos_density_derivs proc~vmix_apply_kpp_overlay vmix_apply_kpp_overlay proc~vmix_apply_kpp_overlay->proc~vmix_kpp_overlay_impl proc~vmix_split_kd_heat_salt vmix_split_kd_heat_salt proc~vmix_split_kd_heat_salt->proc~vmix_split_ddiff_eos_impl proc~vdiff_set_viscous_bbl vdiff_set_viscous_bbl proc~vdiff_set_viscous_bbl->proc~bbl_faces_impl proc~vmix_apply_in_stage vmix_apply_in_stage proc~vmix_apply_in_stage->proc~vmix_apply_kpp_overlay proc~vmix_apply_in_stage->proc~vmix_split_kd_heat_salt proc~ocean_dyn_step ocean_dyn_step proc~ocean_dyn_step->proc~vdiff_set_viscous_bbl proc~run_stage run_stage proc~ocean_dyn_step->proc~run_stage proc~ocean_dyn_step_split ocean_dyn_step_split proc~ocean_dyn_step_split->proc~vdiff_set_viscous_bbl proc~run_stage_split run_stage_split proc~ocean_dyn_step_split->proc~run_stage_split proc~run_stage->proc~vmix_apply_in_stage proc~run_stage_split->proc~vmix_apply_in_stage proc~engine_step engine_step proc~engine_step->proc~ocean_dyn_step proc~engine_step->proc~ocean_dyn_step_split

Variables

Type Visibility Attributes Name Initial
real(kind=wp), private :: T_sq
real(kind=wp), private :: alpha_0
real(kind=wp), private :: big_p
real(kind=wp), private :: denom
real(kind=wp), private :: dlam_ds
real(kind=wp), private :: dlam_dt
real(kind=wp), private :: dp0_ds
real(kind=wp), private :: dp0_dt
real(kind=wp), private :: dsv_ds_roq
real(kind=wp), private :: dsv_dt_roq
real(kind=wp), private :: inv_d2
real(kind=wp), private :: inv_sv2
real(kind=wp), private :: lambda
real(kind=wp), private :: p_0
real(kind=wp), private :: sv_roq

Source Code

   pure elemental subroutine eos_buoyancy_coeffs(eos, T, S, p, alpha_T, beta_S)
      !! Thermal-expansion and haline-contraction coefficients of the
      !! **ACTIVE** equation of state at a point, in the SAME DIMENSIONAL
      !! convention the `eos_t` members `alpha_T` / `beta_S` carry:
      !!
      !!   alpha_T = −∂ρ/∂T   (kg/m³ per degC; > 0 — warm water expands)
      !!   beta_S  = +∂ρ/∂S   (kg/m³ per PSU;  > 0 — salt contracts)
      !!
      !! so that `ρ ≈ ρ_ref − alpha_T·ΔT + beta_S·ΔS` to first order,
      !! matching the linear branch of `eos_density_point` term for term.
      !! They are NOT the fractional α = −(1/ρ)∂ρ/∂T; divide by ρ (or by
      !! ρ₀ in a Boussinesq consumer) for that.  `ocean_cavity_const_t`
      !! deliberately carries its own FRACTIONAL ISOMIP+ pair — see its
      !! docstring.
      !!
      !! **Why this exists.** Seawater's thermal expansion is strongly
      !! state-dependent: it collapses toward zero near the freezing point
      !! and grows with pressure (thermobaricity).  At (−1.9 degC, 34.5
      !! PSU) Wright (1997) gives roughly a quarter of the 10 degC surface
      !! value, and about twice that again by 1000 dbar.  Any consumer
      !! that sizes a buoyancy flux with a CONSTANT α while the dyn-core
      !! integrates a NONLINEAR ρ is inconsistent with its own density
      !! field — worst under an ice shelf, where the constant can
      !! mis-magnitude (and in the cold-fresh corner mis-sign) the
      !! melt-driven surface buoyancy flux that sets the boundary layer.
      !!
      !! **Exactness, per branch — this is load-bearing for bit-identity.**
      !! Each branch is differentiated in CLOSED FORM from that variant's
      !! own density expression; no branch round-trips through
      !! `−ρ²·dSV/dX`, because for the LINEAR branch that would return
      !! `ρ(T,S)²·alpha_T/ρ₀²` — equal to `eos%alpha_T` only to round-off.
      !! As written the linear branch returns the handle members
      !! BIT-FOR-BIT, which is what lets a consumer routed through here
      !! stay byte-identical to the constant path under `eos = "linear"`.
      !!
      !!   * linear — `ρ = ρ₀ + β(S−S_ref) − α(T−T_ref)` ⇒ the members
      !!     themselves, exactly; `p` is ignored (no pressure dependence).
      !!   * Wright (1997) — `ρ = P/D` with `P = p + p₀(T,S)` and
      !!     `D = λ(T,S) + α₀(T,S)·P`, so by the quotient rule
      !!     `∂ρ/∂X = (P_X·D − P·D_X)/D²`, `D_X = λ_X + α₀_X·P + α₀·P_X`,
      !!     all three polynomials from Table A1.  Algebraically identical
      !!     to `−ρ²·dSV/dX` from `eos_specvol_derivs`, evaluated once.
      !!   * Roquet et al. (2015) SpV — `ρ = 1/SV` ⇒ `∂ρ/∂X = −SV_X/SV²`,
      !!     reusing the SINGLE fused `roquet_spv_point` evaluation that
      !!     already returns `SV` and both model-variable sensitivities
      !!     (no duplicated polynomial — see `eos_specvol_derivs`).
      !!
      !! The `else` is unreachable-by-contract: `eos_validate` guarantees
      !! `eos%variant` is in the device-callable set at configure time
      !! (device code cannot `error stop`), exactly as for
      !! `eos_density_point` / `eos_specvol_derivs`.
      !!
      !! `elemental` (all dummies scalar, `eos_t` a flat POD by value) so a
      !! host-side sweep can evaluate whole arrays in one reference, and
      !! `!$acc routine seq` so a `do concurrent` kernel can call it —
      !! the same shape `eos_freezing_point` already ships.
      !$acc routine seq
      type(eos_t), intent(in) :: eos
         !! Shared EOS handle (variant + scalar coeffs), by value.
      real(wp), intent(in) :: T, S
         !! Potential temperature (degC) and practical salinity (PSU) —
         !! the model-prognostic pair, per the `TS_POT_PRAC` convention.
      real(wp), intent(in) :: p
         !! Pressure (Pa) at which to evaluate.  Which pressure is the
         !! CONSUMER's choice and is documented at each call site: a
         !! surface buoyancy flux wants the top of the column
         !! (`multilayer_state_t%p_top` under `&ocean_psurf_nml in_eos`,
         !! else `eos%p_ref`); an interior-interface closure wants the
         !! true in-situ hydrostatic pressure there.  Never a
         !! horizontally-varying value fed back into `ms%rho_layer` —
         !! see the `p_top` seam contract in `src/core/ocean/README.md`.
      real(wp), intent(out) :: alpha_T
         !! −∂ρ/∂T (kg/m³ per degC).
      real(wp), intent(out) :: beta_S
         !! +∂ρ/∂S (kg/m³ per PSU).

      real(wp) :: T_sq, alpha_0, p_0, lambda, big_p, denom, inv_d2
      real(wp) :: dp0_dt, dlam_dt, dp0_ds, dlam_ds
      real(wp) :: sv_roq, dsv_dt_roq, dsv_ds_roq, inv_sv2

      if (eos%variant == EOS_VARIANT_ROQUET_SPV) then
         call roquet_spv_point(T, S, p, sv_roq, dsv_dt_roq, dsv_ds_roq)
         inv_sv2 = 1.0_wp/(sv_roq*sv_roq)
         alpha_T = dsv_dt_roq*inv_sv2
         beta_S = -dsv_ds_roq*inv_sv2
      else if (eos%variant == EOS_VARIANT_WRIGHT_97) then
         T_sq = T*T
         alpha_0 = WRIGHT_A0 + WRIGHT_A1*T + WRIGHT_A2*S
         p_0 = WRIGHT_B0 + WRIGHT_B1*T + WRIGHT_B2*T_sq + WRIGHT_B3*T_sq*T + &
               WRIGHT_B4*S + WRIGHT_B5*S*T
         lambda = WRIGHT_C0 + WRIGHT_C1*T + WRIGHT_C2*T_sq + WRIGHT_C3*T_sq*T + &
                  WRIGHT_C4*S + WRIGHT_C5*S*T
         dp0_dt = WRIGHT_B1 + 2.0_wp*WRIGHT_B2*T + 3.0_wp*WRIGHT_B3*T_sq + &
                  WRIGHT_B5*S
         dlam_dt = WRIGHT_C1 + 2.0_wp*WRIGHT_C2*T + 3.0_wp*WRIGHT_C3*T_sq + &
                   WRIGHT_C5*S
         dp0_ds = WRIGHT_B4 + WRIGHT_B5*T
         dlam_ds = WRIGHT_C4 + WRIGHT_C5*T
         big_p = p + p_0
         denom = lambda + alpha_0*big_p
         inv_d2 = 1.0_wp/(denom*denom)
         alpha_T = -(dp0_dt*denom &
                     - big_p*(dlam_dt + WRIGHT_A1*big_p + alpha_0*dp0_dt))*inv_d2
         beta_S = (dp0_ds*denom &
                   - big_p*(dlam_ds + WRIGHT_A2*big_p + alpha_0*dp0_ds))*inv_d2
      else
         alpha_T = eos%alpha_T
         beta_S = eos%beta_S
      end if
   end subroutine eos_buoyancy_coeffs