Solve the three-equation system with any implemented exchange law, and return the full interface state plus the fluxes a coupling seam will consume.
For an EXPLICIT law (const_gamma) this is one call to the
closed-form quadratic.
For a STRATIFICATION-DEPENDENT law (hj99, yung25) the system
is implicit — the exchange coefficients depend on the interfacial
buoyancy flux, which depends on the melt rate they produce — and
the outer iteration is BISECTION on x = ln(L+) over
[CAVITY_LP_X_LO, CAVITY_LP_X_HI]. It is guaranteed convergent
because the residual G(x) = ln(L+_new(x)) - x provably changes
sign across that bracket:
x -> x_lo (maximal suppression): gamma -> 0, so
|B_b| -> 0 and L+_new -> +infinity, giving G > 0;x -> x_hi (neutral): gamma sits at its cap, |B_b| is
maximal and L+_new is finite, giving G < 0.UNLESS the interface is DESTABILISING (B_b >= 0, i.e. freezing
or a strongly cooling interface), in which case L+_new is
+infinity everywhere and the neutral limit IS the fixed point —
short-circuited, not iterated. That is the branch KINK: H&J99
sets eta* := 1 there (p. 1792) and Yung et al. (2025) falls
back to the ConstCoeff values (pp. 5832-5833). The melt rate is
continuous across it; its derivative is not, which is exactly why
this is bisection and not Newton.
After convergence the whole column is re-evaluated ONCE at the
converged scalar, so the returned exchange velocities and the
returned (T_b, S_b, m_mass) belong to ONE value of L+.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=wp), | intent(in) | :: | T_w |
Far-field temperature (degC). |
||
| real(kind=wp), | intent(in) | :: | S_w |
Far-field salinity (g/kg). Must exceed |
||
| real(kind=wp), | intent(in) | :: | p_b |
Interface pressure (Pa). |
||
| real(kind=wp), | intent(in) | :: | u_star |
Friction velocity (m/s), strictly positive — from
|
||
| real(kind=wp), | intent(in) | :: | S_i |
Ice salinity (g/kg), >= 0. |
||
| type(ocean_cavity_exchange_t), | intent(in) | :: | par |
Exchange-law bundle (its |
||
| real(kind=wp), | intent(in) | :: | f_cor |
Coriolis parameter (1/s) for this column; |
||
| type(ocean_cavity_ice_t), | intent(in) | :: | ice |
Ice-conduction bundle. |
||
| type(eos_t), | intent(in) | :: | eos |
Shared EOS handle — the liquidus. |
||
| type(ocean_cavity_const_t), | intent(in) | :: | const |
Constants bundle. |
||
| type(ocean_cavity_solution_t), | intent(out) | :: | sol |
Interface state, fluxes and solver diagnostics. On any non-OK
status this carries the safe state ( |
||
| integer, | intent(out) | :: | ierr |
|
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| real(kind=wp), | private | :: | S_b | ||||
| real(kind=wp), | private | :: | T_b | ||||
| real(kind=wp), | private | :: | b_flux | ||||
| logical, | private | :: | converged | ||||
| real(kind=wp), | private | :: | g_hi | ||||
| real(kind=wp), | private | :: | g_lo | ||||
| real(kind=wp), | private | :: | g_mid | ||||
| real(kind=wp), | private | :: | gamma_s | ||||
| real(kind=wp), | private | :: | gamma_t | ||||
| real(kind=wp), | private | :: | hi | ||||
| integer, | private | :: | it | ||||
| real(kind=wp), | private | :: | l_plus | ||||
| real(kind=wp), | private | :: | lo | ||||
| real(kind=wp), | private | :: | lp_new | ||||
| real(kind=wp), | private | :: | m_mass | ||||
| real(kind=wp), | private | :: | mid | ||||
| logical, | private | :: | settled | ||||
| real(kind=wp), | private | :: | xstar |
pure subroutine cavity_solve_melt_f(T_w, S_w, p_b, u_star, S_i, par, f_cor, ice, eos, & const, sol, ierr) !! Solve the three-equation system with any implemented exchange !! law, and return the full interface state plus the fluxes a !! coupling seam will consume. !! !! For an EXPLICIT law (`const_gamma`) this is one call to the !! closed-form quadratic. !! !! For a STRATIFICATION-DEPENDENT law (`hj99`, `yung25`) the system !! is implicit — the exchange coefficients depend on the interfacial !! buoyancy flux, which depends on the melt rate they produce — and !! the outer iteration is BISECTION on `x = ln(L+)` over !! `[CAVITY_LP_X_LO, CAVITY_LP_X_HI]`. It is guaranteed convergent !! because the residual `G(x) = ln(L+_new(x)) - x` provably changes !! sign across that bracket: !! !! * `x -> x_lo` (maximal suppression): `gamma -> 0`, so !! `|B_b| -> 0` and `L+_new -> +infinity`, giving `G > 0`; !! * `x -> x_hi` (neutral): `gamma` sits at its cap, `|B_b|` is !! maximal and `L+_new` is finite, giving `G < 0`. !! !! UNLESS the interface is DESTABILISING (`B_b >= 0`, i.e. freezing !! or a strongly cooling interface), in which case `L+_new` is !! `+infinity` everywhere and the neutral limit IS the fixed point — !! short-circuited, not iterated. That is the branch KINK: H&J99 !! sets `eta* := 1` there (p. 1792) and Yung et al. (2025) falls !! back to the ConstCoeff values (pp. 5832-5833). The melt rate is !! continuous across it; its derivative is not, which is exactly why !! this is bisection and not Newton. !! !! After convergence the whole column is re-evaluated ONCE at the !! converged scalar, so the returned exchange velocities and the !! returned `(T_b, S_b, m_mass)` belong to ONE value of `L+`. !$acc routine seq real(wp), intent(in) :: T_w !! Far-field temperature (degC). real(wp), intent(in) :: S_w !! Far-field salinity (g/kg). Must exceed `S_i`. real(wp), intent(in) :: p_b !! Interface pressure (Pa). real(wp), intent(in) :: u_star !! Friction velocity (m/s), strictly positive — from !! `cavity_ustar`. real(wp), intent(in) :: S_i !! Ice salinity (g/kg), >= 0. type(ocean_cavity_exchange_t), intent(in) :: par !! Exchange-law bundle (its `f_cor` member is NOT read). real(wp), intent(in) :: f_cor !! Coriolis parameter (1/s) for this column; `hj99` only. See !! `cavity_exchange_velocities_f` for why it is not `par%f_cor`. type(ocean_cavity_ice_t), intent(in) :: ice !! Ice-conduction bundle. type(eos_t), intent(in) :: eos !! Shared EOS handle — the liquidus. type(ocean_cavity_const_t), intent(in) :: const !! Constants bundle. type(ocean_cavity_solution_t), intent(out) :: sol !! Interface state, fluxes and solver diagnostics. On any non-OK !! status this carries the safe state (`m_mass` exactly zero). integer, intent(out) :: ierr !! `CAVITY_MELT_*` status. real(wp) :: T_b, S_b, m_mass, gamma_t, gamma_s, b_flux, lp_new, l_plus real(wp) :: lo, hi, mid, xstar, g_lo, g_hi, g_mid integer :: it logical :: converged, settled ierr = CAVITY_MELT_OK call cavity_solution_reset(sol) sol%u_star = u_star sol%l_plus = CAVITY_L_PLUS_NEUTRAL sol%L_obukhov = CAVITY_L_PLUS_NEUTRAL call cavity_safe_state(eos, S_w, p_b, sol%T_b, sol%S_b, sol%m_mass) if (.not. (ieee_is_finite(T_w) .and. ieee_is_finite(S_w) .and. & ieee_is_finite(p_b) .and. ieee_is_finite(u_star) .and. & ieee_is_finite(S_i))) then ierr = CAVITY_MELT_NONFINITE_INPUT return end if if (u_star <= 0.0_wp) then ierr = CAVITY_MELT_BAD_INPUT return end if converged = .true. it = 0 l_plus = CAVITY_L_PLUS_NEUTRAL ! The neutral evaluation is both the answer for an explicit law and ! the upper bracket for an implicit one. call cavity_state_at_x(CAVITY_LP_X_HI, par, f_cor, ice, eos, const, u_star, T_w, S_w, & p_b, S_i, T_b, S_b, m_mass, gamma_t, gamma_s, b_flux, & lp_new, ierr) if (ierr /= CAVITY_MELT_OK) then call cavity_safe_state(eos, S_w, p_b, sol%T_b, sol%S_b, sol%m_mass) return end if l_plus = lp_new if (cavity_law_is_implicit(par%law)) then g_hi = cavity_outer_residual(lp_new, CAVITY_LP_X_HI) ! `cavity_l_plus_is_neutral(lp_new)` is the destabilising ! short-circuit and makes `g_hi = +huge >= 0`; the explicit ! `g_hi >= 0` test below therefore covers both accept branches. if (g_hi < 0.0_wp) then call cavity_state_at_x(CAVITY_LP_X_LO, par, f_cor, ice, eos, const, u_star, T_w, & S_w, p_b, S_i, T_b, S_b, m_mass, gamma_t, gamma_s, & b_flux, lp_new, ierr) if (ierr /= CAVITY_MELT_OK) then call cavity_safe_state(eos, S_w, p_b, sol%T_b, sol%S_b, sol%m_mass) return end if g_lo = cavity_outer_residual(lp_new, CAVITY_LP_X_LO) if (g_lo <= 0.0_wp) then ! The bracket argument failed — refuse rather than return ! whichever end happens to look plausible. call cavity_safe_state(eos, S_w, p_b, sol%T_b, sol%S_b, sol%m_mass) ierr = CAVITY_MELT_NOT_CONVERGED return end if lo = CAVITY_LP_X_LO hi = CAVITY_LP_X_HI converged = .false. do it = 1, CAVITY_MAX_ITER mid = 0.5_wp*(lo + hi) settled = (mid <= lo) .or. (mid >= hi) if (.not. settled) then call cavity_state_at_x(mid, par, f_cor, ice, eos, const, u_star, T_w, S_w, & p_b, S_i, T_b, S_b, m_mass, gamma_t, gamma_s, & b_flux, lp_new, ierr) if (ierr /= CAVITY_MELT_OK) then call cavity_safe_state(eos, S_w, p_b, sol%T_b, sol%S_b, sol%m_mass) return end if g_mid = cavity_outer_residual(lp_new, mid) if (g_mid == 0.0_wp) then settled = .true. else if (g_mid > 0.0_wp) then lo = mid else hi = mid end if if ((.not. settled) .and. (hi - lo < CAVITY_LP_X_TOL)) settled = .true. end if if (settled) then converged = .true. exit end if end do if (.not. converged) then call cavity_safe_state(eos, S_w, p_b, sol%T_b, sol%S_b, sol%m_mass) ierr = CAVITY_MELT_NOT_CONVERGED return end if ! One final evaluation at the converged scalar, so the ! returned gammas and the returned interface state belong to ! the SAME `L+`. xstar = 0.5_wp*(lo + hi) call cavity_state_at_x(xstar, par, f_cor, ice, eos, const, u_star, T_w, S_w, p_b, & S_i, T_b, S_b, m_mass, gamma_t, gamma_s, b_flux, & lp_new, ierr) if (ierr /= CAVITY_MELT_OK) then call cavity_safe_state(eos, S_w, p_b, sol%T_b, sol%S_b, sol%m_mass) return end if l_plus = exp(xstar) end if end if sol%T_b = T_b sol%S_b = S_b sol%m_mass = m_mass sol%gamma_t = gamma_t sol%gamma_s = gamma_s sol%b_flux = b_flux sol%l_plus = l_plus sol%L_obukhov = cavity_obukhov_length(const, u_star, b_flux) sol%T_star = T_w - eos_freezing_point(eos, S_w, p_b) sol%S_star = S_w - S_b call cavity_heat_fluxes(T_w, T_b, m_mass, gamma_t, ice, const, & sol%q_ocean, sol%q_ice, sol%q_latent) sol%n_iter = it sol%converged = converged end subroutine cavity_solve_melt_f