Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Density-weighted (mass-flux) adaptive implicit vertical advection#5733

Merged
glwagner merged 10 commits into
mainfrom
glw/mass-flux-implicit-vertical-advection
Jul 2, 2026
Merged

Density-weighted (mass-flux) adaptive implicit vertical advection#5733
glwagner merged 10 commits into
mainfrom
glw/mass-flux-implicit-vertical-advection

Conversation

@glwagner

@glwagner glwagner commented Jun 27, 2026

Copy link
Copy Markdown
Member

Summary

Adaptive implicit vertical advection (AIVA) currently assumes a volume-conserving (Boussinesq) tracer flux. Anelastic and compressible models advect in mass-flux form, ∂t(ρc) = -∇·(ρ 𝐰 c), so they can't reuse the implicit coefficients and have to re-derive them downstream (with type piracy / wrappers — see context below).

This PR adds an optional density to the implicit vertical advection, as a trailing positional argument (default nothing) — keeping the existing positional call structure:

# Boussinesq (unchanged):
implicit_step!(field, solver, closure, closure_fields, idx, clock, fields, Δt, advection, velocities)

# mass-flux model:
implicit_step!(field, solver, closure, closure_fields, idx, clock, fields, Δt, advection, velocities, ρ)

When a density field is supplied, the implicit upwind coefficients are weighted by the density at the advecting interface (densityᶜᶜᶠ) and divided by the density at the reconstructed cell centre (densityᶜᶜᶜ), i.e. c = ρc/ρ:

upper_k =  Δt/V_k · Az_{k+½} · ρᶜᶜᶠ_{k+½} / ρᶜᶜᶜ_{k+1} · min(wⁱ, 0)
lower_k = −Δt/V_k · Az_{k−½} · ρᶜᶜᶠ_{k−½} / ρᶜᶜᶜ_{k−1} · max(wⁱ, 0)

The default density === nothing reproduces the volume-conserving coefficients exactly (ρᶜᶜᶠ/ρᶜᶜᶜ → 1), so existing Boussinesq behaviour is bit-for-bit unchanged.

What changed

  • densityᶜᶜᶜ(i, j, k, grid, ρ) / densityᶜᶜᶠ(i, j, k, grid, ρ) return the density at the tracer cell centre / vertical interface, and 1 for ρ === nothing.
  • implicit_advection_{upper,lower,diagonal} take an optional trailing density = nothing and apply the ρᶜᶜᶠ/ρᶜᶜᶜ weighting.
  • The get_coefficient overloads thread the same optional density through (default nothing).
  • implicit_step! gains a trailing density = nothing positional argument and forwards it to solve!.

Intended for tracers (Center, Center, Center); momentum stays volume-conserving (passing nothing). No exported API changes; all additions are backward-compatible optional arguments.

Tests

Extends test/test_adaptive_implicit_vertical_advection.jl:

  • ρ ≡ 1 reproduces the volume-conserving coefficients;
  • off-diagonal coefficients scale with the density ratio ℑz(ρ)/ρ;
  • the density-weighted implicit solve conserves column mass (the upwind operator is flux-form, so a closed column preserves ∑ V ρ c).

The existing AIVA tests (incl. the WENOVectorInvariant + SplitRungeKutta3 paths) also exercise the density = nothing path through implicit_step!/get_coefficient/the kernel, guarding the Boussinesq behaviour. Full TEST_FILE=test_adaptive_implicit_vertical_advection.jl run: 325 pass / 0 fail.

Motivation / downstream

Breeze.jl (anelastic/compressible) currently reimplements the density-weighted coefficients and wires its own breeze_implicit_step! because there's no hook for ρ in the upstream signature (and extending get_coefficient/implicit_step! with all-Oceananigans argument types is type piracy). With this PR, Breeze can call implicit_step!(…, advection, velocities, ρ) directly and delete that machinery.

🤖 Generated with Claude Code

Adaptive implicit vertical advection (AIVA) currently assumes a volume-conserving
(Boussinesq) tracer flux. Anelastic and compressible models advect in mass-flux
form, ∂t(ρc) = -∇·(ρ 𝐰 c), so they cannot reuse the implicit coefficients and must
re-derive them downstream.

Add an optional `density` to the implicit vertical advection. `implicit_step!` gains
a trailing `density = nothing` positional argument; when a density field is supplied,
the implicit upwind coefficients are weighted by the density at the advecting
interface (`densityᶜᶜᶠ`) and divided by the density at the reconstructed cell centre
(`densityᶜᶜᶜ`), i.e. c = ρc/ρ. The default `density === nothing` reproduces the
volume-conserving coefficients exactly, so existing (Boussinesq) behaviour is unchanged.

The weight is threaded through `implicit_advection_{upper,lower,diagonal}` and the
`get_coefficient` overloads as an optional trailing argument (default `nothing`).
Intended for tracers (Center, Center, Center); momentum stays volume-conserving.

Tests (extend test_adaptive_implicit_vertical_advection.jl):
- `ρ ≡ 1` reproduces the volume-conserving coefficients;
- off-diagonal coefficients scale with the density ratio ℑz(ρ)/ρ;
- the density-weighted implicit solve conserves column mass (flux-form upwind).

This lets a downstream mass-flux model (e.g. Breeze.jl) call
`implicit_step!(…, advection, velocities, ρ)` directly instead of reimplementing the
density-weighted coefficients and solver wiring.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@glwagner glwagner force-pushed the glw/mass-flux-implicit-vertical-advection branch from c86b244 to b352300 Compare June 27, 2026 18:11
@navidcy navidcy added the numerics 🧮 So things don't blow up and boil the lobsters alive label Jun 29, 2026
#####
##### The upwind flux at face k+1 (top of cell k):
##### F_{k+1} = Az_{k+1} * [max(wⁱ_{k+1}, 0) * c_k + min(wⁱ_{k+1}, 0) * c_{k+1}]
##### The upwind flux at face k+1 (top of cell k), weighted by the face density ϖᶠ:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why ϖ for density and not ρ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

what the... yes, ρ makes more sense to me. I guess sometimes this is 1 and sometimes its ρ which could warrant a new symbol (or not)

Comment thread src/Advection/implicit_vertical_advection.jl Outdated
Comment thread test/test_adaptive_implicit_vertical_advection.jl Outdated
Comment thread src/Advection/implicit_vertical_advection.jl Outdated
Comment thread src/Advection/implicit_vertical_advection.jl Outdated
Comment thread test/test_adaptive_implicit_vertical_advection.jl Outdated
@glwagner glwagner merged commit d6762e9 into main Jul 2, 2026
90 of 91 checks passed
@glwagner glwagner deleted the glw/mass-flux-implicit-vertical-advection branch July 2, 2026 16:19
glwagner added a commit to NumericalEarth/Breeze.jl that referenced this pull request Jul 2, 2026
Replace breeze_implicit_step!, AIVADensity, and the Breeze copies of the
density-weighted implicit-advection diagonals with Oceananigans' extended
implicit_step!(field, ..., advection, velocities, density) seam, upstreamed
in CliMA/Oceananigans.jl#5733 (0.110.8).

Support adaptive implicit vertical advection for momentum under the
anelastic SSPRungeKutta3:

- ρu and ρv ride on the upstream z-Center coefficients directly (exact for
  the horizontally-uniform anelastic reference density).
- ρw gets new density-weighted upwind tridiagonal coefficients for z-Face
  fields (upstream keeps Ww fully explicit and has no z-Face path), routed
  through a Breeze-owned VerticalMomentumImplicitAdvection wrapper so the
  get_coefficient extension commits no type piracy.
- The explicit vertical momentum fluxes are scaled by the velocity CFL:
  Breeze advects momentum with the mass flux (ρu, ρv, ρw) as advecting
  field, so upstream's scaling would split on |ρw| instead of |w|,
  inconsistently with the implicit solve.

Momentum AIVA is rejected with AcousticRungeKutta3, which advances momentum
inside the acoustic substep loop; tracer AIVA there is unchanged. Requires
Oceananigans 0.110.8 ([compat] bumped), so CI waits on that release.

Co-Authored-By: Claude Fable 5 <[email protected]>
glwagner added a commit to NumericalEarth/Breeze.jl that referenced this pull request Jul 2, 2026
…-RK3) (#809)

* Add adaptive implicit vertical advection for scalars (anelastic SSP-RK3)

Oceananigans gained adaptive implicit vertical advection (AIVA): each cell
splits its vertical velocity into an explicit part that respects a target CFL
and an implicit first-order-upwind part solved tridiagonally, lifting the
vertical-advection CFL limit on the time step. This wires AIVA into Breeze's
anelastic SSPRungeKutta3 path for scalars.

Opt in per scalar, e.g.

    scalar_advection = (; ρθ = WENO(time_discretization =
        AdaptiveVerticallyImplicitDiscretization(cfl = 0.5)))

Because Breeze advects in mass-flux form ∇·(ρ U c), the volume-conserving
upstream implicit coefficients are re-derived with density weighting; they
reduce exactly to Oceananigans' coefficients when ρ ≡ 1 (tested). The explicit
half needs no new code: it already rides on `_advective_tracer_flux_z`, which
dispatches on the scheme's time discretization, pre-multiplied by ℑz(ρ).

- Density-weighted implicit advection diagonals + get_coefficient/implicit_step!
  seam threading the reference density, combined with the existing
  vertically-implicit diffusion tridiagonal solve
  (AtmosphereModels/implicit_vertical_advection.jl).
- Build the implicit solver when a scalar advection scheme needs it; reject
  adaptive-implicit momentum advection at construction.
- Refresh the AIVA Δt before each tendency build via update_advection_timestep!
  so the explicit (Gⁿ) and implicit velocity splits stay consistent.
- Tests: exact ρ≡1 reduction to Oceananigans, construction wiring, momentum
  rejection, and stability above the explicit vertical CFL.

Momentum and the acoustic/compressible path remain explicit (future work; see
design/implicit_vertical_advection.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* Fix Aqua type piracy in the AIVA implicit-solve seam

CI's quality_assurance (Aqua.jl) caught type piracy: the get_coefficient and
implicit_step! methods extended Oceananigans-owned functions where every
argument type was also Oceananigans-owned.

- Introduce a Breeze-owned `AIVADensity` wrapper for the reference density and
  thread it through `solve!`, so the `get_coefficient` methods carry a
  Breeze-owned type in their signature (no longer piracy). It is unwrapped to
  the bare density field inside the coefficient functions; an `adapt_structure`
  keeps it GPU-compatible.
- Replace the `TimeSteppers.implicit_step!` extension with a Breeze-owned
  `breeze_implicit_step!`, called from the SSP-RK3 AIVA branch.

Verified on a cpu-large dev node (Aqua quality_assurance, turbulence_closures,
advection_schemes, atmosphere_model_construction, implicit_vertical_advection
all pass).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* Run AIVA coefficient-reduction test on CPU to avoid GPU scalar indexing

The "reduce to Oceananigans at ρ ≡ 1" testset compares coefficients pointwise
in a host-side loop, which indexes fields directly (ρ[i,j,k], W[i,j,k]). On a
GPU that triggers disallowed scalar indexing. The comparison is pure,
architecture-independent arithmetic, so evaluate it on a CPU grid regardless of
the default test architecture. Also copy `interior` to the host before the
finiteness check in the stability test.

Verified on an H100 (gpu-prod): implicit_vertical_advection tests pass, which
also exercises the on-device breeze_implicit_step!/AIVADensity solve path.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* Remove design notes from PR

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* Support adaptive implicit vertical advection with the acoustic substepper

Wire AIVA for the transport scalars (moisture, tracers) advanced by
`scalar_substep!` on the AcousticRungeKutta3 (compressible split-explicit) path:
schemes that `needs_implicit_solver` are routed through `breeze_implicit_step!`
with the prognostic density and the substepper's time-averaged transport
velocity (the same `w` the explicit tendencies use, so the explicit/implicit
velocity split stays consistent). Other scalars keep the diffusion-only step.

The thermodynamic density (ρθ/ρe) is integrated inside the acoustic substep loop
rather than the generic scalar implicit step, so AIVA on it is rejected at
construction when the acoustic substepper is selected (momentum was already
rejected globally).

Test: compressible + AcousticRungeKutta3 with an AIVA tracer (construction wires
the solver; a warm-bubble run stays finite), plus ρθ AIVA rejection. Verified on
cpu-large and an H100; acoustic_substepping and quality_assurance regressions pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* Refactor AIVA onto Oceananigans 0.110.8 and support momentum

Replace breeze_implicit_step!, AIVADensity, and the Breeze copies of the
density-weighted implicit-advection diagonals with Oceananigans' extended
implicit_step!(field, ..., advection, velocities, density) seam, upstreamed
in CliMA/Oceananigans.jl#5733 (0.110.8).

Support adaptive implicit vertical advection for momentum under the
anelastic SSPRungeKutta3:

- ρu and ρv ride on the upstream z-Center coefficients directly (exact for
  the horizontally-uniform anelastic reference density).
- ρw gets new density-weighted upwind tridiagonal coefficients for z-Face
  fields (upstream keeps Ww fully explicit and has no z-Face path), routed
  through a Breeze-owned VerticalMomentumImplicitAdvection wrapper so the
  get_coefficient extension commits no type piracy.
- The explicit vertical momentum fluxes are scaled by the velocity CFL:
  Breeze advects momentum with the mass flux (ρu, ρv, ρw) as advecting
  field, so upstream's scaling would split on |ρw| instead of |w|,
  inconsistently with the implicit solve.

Momentum AIVA is rejected with AcousticRungeKutta3, which advances momentum
inside the acoustic substep loop; tracer AIVA there is unchanged. Requires
Oceananigans 0.110.8 ([compat] bumped), so CI waits on that release.

Co-Authored-By: Claude Fable 5 <[email protected]>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

numerics 🧮 So things don't blow up and boil the lobsters alive

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants