Density-weighted (mass-flux) adaptive implicit vertical advection#5733
Merged
Conversation
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]>
c86b244 to
b352300
Compare
simone-silvestri
approved these changes
Jun 28, 2026
navidcy
reviewed
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 ϖᶠ: |
Member
Author
There was a problem hiding this comment.
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)
navidcy
reviewed
Jun 29, 2026
navidcy
reviewed
Jun 29, 2026
navidcy
reviewed
Jun 29, 2026
navidcy
reviewed
Jun 29, 2026
navidcy
reviewed
Jun 29, 2026
Co-authored-by: Navid C. Constantinou <[email protected]>
Co-authored-by: Navid C. Constantinou <[email protected]>
Co-authored-by: Navid C. Constantinou <[email protected]>
Co-authored-by: Navid C. Constantinou <[email protected]>
Co-authored-by: Navid C. Constantinou <[email protected]>
The density-weight locals are literally density, so use ρᶠ/ρᶜ rather than varpi. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
densityto the implicit vertical advection, as a trailing positional argument (defaultnothing) — keeping the existing positional call structure: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 === nothingreproduces 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, and1forρ === nothing.implicit_advection_{upper,lower,diagonal}take an optional trailingdensity = nothingand apply theρᶜᶜᶠ/ρᶜᶜᶜweighting.get_coefficientoverloads thread the same optionaldensitythrough (defaultnothing).implicit_step!gains a trailingdensity = nothingpositional argument and forwards it tosolve!.Intended for tracers (
Center, Center, Center); momentum stays volume-conserving (passingnothing). No exported API changes; all additions are backward-compatible optional arguments.Tests
Extends
test/test_adaptive_implicit_vertical_advection.jl:ρ ≡ 1reproduces the volume-conserving coefficients;ℑz(ρ)/ρ;∑ V ρ c).The existing AIVA tests (incl. the
WENOVectorInvariant+SplitRungeKutta3paths) also exercise thedensity = nothingpath throughimplicit_step!/get_coefficient/the kernel, guarding the Boussinesq behaviour. FullTEST_FILE=test_adaptive_implicit_vertical_advection.jlrun: 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 extendingget_coefficient/implicit_step!with all-Oceananigans argument types is type piracy). With this PR, Breeze can callimplicit_step!(…, advection, velocities, ρ)directly and delete that machinery.🤖 Generated with Claude Code