Skip to content

Conversation

@glwagner
Copy link
Member

@glwagner glwagner commented Jan 8, 2026

This "kinematic driver" can be used to test microphysics schemes.

@glwagner glwagner requested a review from kaiyuan-cheng January 8, 2026 06:00
@kaiyuan-cheng
Copy link
Collaborator

The code appears to work as expected, except for the boundary conditions, see
image

The desired behavior is an upward translation of the profile. I think we may need an inlet boundary condition.

Set the velocity component `name` (`:u`, `:v`, or `:w`) to `value`.
Also updates the corresponding momentum field.
"""
function set_velocity!(model::AtmosphereModel, name::Symbol, value)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we set velocity as a function of time? I believe so, but I’m not certain.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes! For time-dependent velocity fields, you need to build them explicitly in advance of the model with PrescribedVelocityFields and then pass them into the model:

model = AtmosphereModel(grid; velocities=prescribed_velocities)

Copy link
Member Author

Choose a reason for hiding this comment

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

Initially I was thinking taht we wouldn't support set! at all, but it is actually simply enough if the velocities are ordinary Field.

For boundary conditinons, do you think that we should accept boundary conditions on the velocity field? eg something like

w_bcs = FieldBoundaryConditions(bottom=OpenBoundaryCondition(w_inlet))
boundary_conditions = (; w = w_bcs)
model = AtmosphereModel(grid; boundary_conditions)

Copy link
Member Author

Choose a reason for hiding this comment

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

I believe it should also be easy to have inlet boundary conditions with PrescribedVelocityFields (in that case no special thing is needed except defining functions w(z, t) that are non-zero at z=0)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Initially I was thinking taht we wouldn't support set! at all, but it is actually simply enough if the velocities are ordinary Field.

For boundary conditinons, do you think that we should accept boundary conditions on the velocity field? eg something like

w_bcs = FieldBoundaryConditions(bottom=OpenBoundaryCondition(w_inlet))
boundary_conditions = (; w = w_bcs)
model = AtmosphereModel(grid; boundary_conditions)

Looks good to me!

@codecov
Copy link

codecov bot commented Jan 8, 2026

Codecov Report

❌ Patch coverage is 77.60417% with 43 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/KinematicDriver/prescribed_dynamics.jl 78.78% 21 Missing ⚠️
.../KinematicDriver/kinematic_driver_time_stepping.jl 37.93% 18 Missing ⚠️
src/Thermodynamics/reference_states.jl 0.00% 4 Missing ⚠️

📢 Thoughts on this report? Let us know!

@glwagner
Copy link
Member Author

glwagner commented Jan 8, 2026

The code appears to work as expected, except for the boundary conditions, see image

The desired behavior is an upward translation of the profile. I think we may need an inlet boundary condition.

nice! can you paste your code that produced this in this PR?

@kaiyuan-cheng
Copy link
Collaborator

nice! can you paste your code that produced this in this PR?

using Breeze
using CairoMakie
using Printf

# Grid
Nz = 100
Lz = 20000
grid = RectilinearGrid(CPU(); size=Nz, x=0, y=0, z=(0, Lz),
                       topology=(Flat, Flat, Bounded))

# Reference state
constants = ThermodynamicConstants()
θ₀ = 300
p₀ = 100000
reference_state = ReferenceState(grid, constants;
                                 surface_pressure = p₀,
                                 potential_temperature = θ₀)

# Model setup
ρ_surface = reference_state.density[1, 1, 1]
ρθ_bcs = FieldBoundaryConditions(bottom=ValueBoundaryCondition(ρ_surface * θ₀))

dynamics = PrescribedDynamics(reference_state)
model = AtmosphereModel(grid; dynamics,
                        advection = UpwindBiased(order=1),
                        boundary_conditions = (ρθ=ρθ_bcs,),
                        thermodynamic_constants = constants)

# Initial conditions
θₜᵣ = 343
zₜᵣ = 12000
Tₜᵣ = 213
g = constants.gravitational_acceleration
cᵖᵈ = constants.dry_air.heat_capacity

function θᵇᵍ(z)
    θ_troposphere = θ₀ + (θₜᵣ - θ₀) * (z / zₜᵣ)^(5/4)
    θ_stratosphere = θₜᵣ * exp(g / (cᵖᵈ * Tₜᵣ) * (z - zₜᵣ))
    return ifelse(z <= zₜᵣ, θ_troposphere, θ_stratosphere)
end

set!(model; θ=θᵇᵍ, qᵗ=0, w=10.0)

# Simulation
simulation = Simulation(model; Δt=5.0, stop_time=1800, verbose=false)

times = Float64[]
θ_profiles = []

function record_profiles(sim)
    push!(times, time(sim))
    push!(θ_profiles, Array(interior(sim.model.formulation.potential_temperature, 1, 1, :)))
end
add_callback!(simulation, record_profiles, TimeInterval(300))

function progress(sim)
    θ = sim.model.formulation.potential_temperature
    @info "t = $(prettytime(sim)), θ_surface = $(@sprintf("%.1f", θ[1, 1, 1])) K"
end
add_callback!(simulation, progress, TimeInterval(300))

@info "Running kinematic advection test..."
run!(simulation)

# Visualization
z_km = znodes(grid, Center()) ./ 1000
fig = Figure(size=(500, 600))
ax_θ = Axis(fig[1, 1]; xlabel="θ (K)", ylabel="z (km)", title="Potential temperature")

colors = cgrad(:viridis, length(times), categorical=true)
for (n, t) in enumerate(times)
    lines!(ax_θ, θ_profiles[n], z_km; color=colors[n], label="t = $(Int(t/60)) min")
end

hlines!(ax_θ, [zₜᵣ/1000]; color=:gray, linestyle=:dash, label="Tropopause")
axislegend(ax_θ; position=:rb)
Label(fig[0, 1], "Kinematic Driver: dry atmosphere, w = 10 m/s"; fontsize=16, tellwidth=false)

fig

@glwagner
Copy link
Member Author

glwagner commented Jan 9, 2026

So, after adding support for velocity boundary conditions we can in principle use the kinematic driver. Note however that flux form advection requires a velocity field that satisfied the anelastic continuity equation, eg we have to use something like

using Oceananigans.Operators: ℑzᵃᵃᶠ
ρ = reference_state.density
ρ₀ = ℑzᵃᵃᶠ(1, 1, 1, grid, ρ)
ρᵀ = ℑzᵃᵃᶠ(1, 1, grid.Nz+1, grid, ρ)
w = ZFaceField(grid)
W₀ = 10
ρᶠ = @at (Center, Center, Face) 1 * ρ
set!(w, ρ₀ * W₀ / ρᶠ)

we could use a different advection operator for the kinematic driver, although that would mean we can't use WENO.

Here's what I get

Screenshot 2026-01-09 at 4 17 05 PM

with

using Breeze
using CairoMakie
using Printf

# Grid
Nz = 100
Lz = 20000
grid = RectilinearGrid(CPU(); size=Nz, x=0, y=0, z=(0, Lz),
                       topology=(Flat, Flat, Bounded))

# Reference state
constants = ThermodynamicConstants()
θ₀ = 300
p₀ = 100000
reference_state = ReferenceState(grid, constants;
                                 surface_pressure = p₀,
                                 potential_temperature = θ₀)

using Oceananigans.Operators: ℑzᵃᵃᶠ
ρ = reference_state.density
ρ₀ = ℑzᵃᵃᶠ(1, 1, 1, grid, ρ)
ρᵀ = ℑzᵃᵃᶠ(1, 1, grid.Nz+1, grid, ρ)
w = ZFaceField(grid)
W₀ = 10
ρᶠ = @at (Center, Center, Face) 1 * ρ
set!(w, ρ₀ * W₀ / ρᶠ)

# Model setup
ρθ_bcs = FieldBoundaryConditions(bottom=ValueBoundaryCondition(ρ₀ * θ₀))

top_w = OpenBoundaryCondition(W₀ * ρ₀ / ρᵀ)
bottom_w = OpenBoundaryCondition(W₀)
w_bcs = FieldBoundaryConditions(bottom=bottom_w, top=top_w)

dynamics = PrescribedDynamics(reference_state)
model = AtmosphereModel(grid; dynamics,
                        advection = UpwindBiased(order=1),
                        boundary_conditions = (ρθ=ρθ_bcs, w=w_bcs),
                        thermodynamic_constants = constants)

# Initial conditions
θₜᵣ = 343
zₜᵣ = 12000
Tₜᵣ = 213
g = constants.gravitational_acceleration
cᵖᵈ = constants.dry_air.heat_capacity

function θᵇᵍ(z)
    θ_troposphere = θ₀ + (θₜᵣ - θ₀) * (z / zₜᵣ)^(5/4)
    θ_stratosphere = θₜᵣ * exp(g / (cᵖᵈ * Tₜᵣ) * (z - zₜᵣ))
    return ifelse(z <= zₜᵣ, θ_troposphere, θ_stratosphere)
end

set!(model; θ=θᵇᵍ, qᵗ=0, w=w)

# Simulation
simulation = Simulation(model; Δt=1, stop_time=180, verbose=false)

times = Float64[]
θ_profiles = []

function record_profiles(sim)
    push!(times, time(sim))
    push!(θ_profiles, Array(interior(sim.model.formulation.potential_temperature, 1, 1, :)))
end
add_callback!(simulation, record_profiles, TimeInterval(60))

function progress(sim)
    θ = sim.model.formulation.potential_temperature
    @info "t = $(prettytime(sim)), θ_surface = $(@sprintf("%.1f", θ[1, 1, 1])) K"
end
add_callback!(simulation, progress, TimeInterval(60))

@info "Running kinematic advection test..."
run!(simulation)

# Visualization
z_km = znodes(grid, Center()) ./ 1000
fig = Figure(size=(500, 600))
ax_θ = Axis(fig[1, 1]; xlabel="θ (K)", ylabel="z (km)", title="Potential temperature")

colors = cgrad(:viridis, length(times), categorical=true)
for (n, t) in enumerate(times)
    lines!(ax_θ, θ_profiles[n], z_km; color=colors[n], label="t = $(Int(t/60)) min")
end

hlines!(ax_θ, [zₜᵣ/1000]; color=:gray, linestyle=:dash, label="Tropopause")
axislegend(ax_θ; position=:rb)
Label(fig[0, 1], "Kinematic Driver: dry atmosphere, w = 10 m/s"; fontsize=16, tellwidth=false)

fig

@kaiyuan-cheng
Copy link
Collaborator

Great! We now have a working kinematic model. While WENO is desirable, I think this should be good for now.

@glwagner
Copy link
Member Author

Great! We now have a working kinematic model. While WENO is desirable, I think this should be good for now.

@kaiyuan-cheng these results were generated by using a velocity field that satisfies the anelastic continuity equation.

I looked into it and I think there are two cases that we want:

  1. PrescribedDensity and add the divergence term, so that advection looks like this:

$$ \partial_t ( \rho c ) = - \nabla \cdot \left ( \rho u c \right ) + c \nabla \cdot \left ( \rho u \right ) $$

  1. Also evolve the density (compressible kinematic formulation)

The first case is needed for P3 validation I think.

Here's another reference for kinematic drivers:

https://github.com/Adehill/KiD-A/blob/master/docs/KiD_2.3.2625.pdf

note, our kinematic driver will support 1D, 2D, and 3D.

@glwagner
Copy link
Member Author

@kaiyuan-cheng let me know if you agree! The divergence term is easy to add, and with that form we can still use WENO for $\nabla \cdot \left ( \rho u c \right )$

@kaiyuan-cheng
Copy link
Collaborator

@kaiyuan-cheng let me know if you agree! The divergence term is easy to add, and with that form we can still use WENO for ∇ ⋅ ( ρ u c )

The divergence term at the boundaries is necessary if we want to enforce mass continuity. Admittedly, I don’t understand why WENO works in this case but not in the previous one.

@glwagner
Copy link
Member Author

@kaiyuan-cheng let me know if you agree! The divergence term is easy to add, and with that form we can still use WENO for ∇ ⋅ ( ρ u c )

The divergence term at the boundaries is necessary if we want to enforce mass continuity. Admittedly, I don’t understand why WENO works in this case but not in the previous one.

Here's an explanation. The advection term is

$$ \rho u \cdot \nabla c $$

We don't have a WENO method to evaluate this term as is.

Also note that on a staggered grid, this term must be evaluated at cell centers. However, neither $u$ nor $\nabla c$ are at cell centers on a staggered grid.

However, we can rewrite this term because

$$ \nabla \cdot \left ( \rho u c \right ) = \rho u \cdot \nabla c + c \nabla \cdot \left ( \rho u \right ) $$

therefore

$$ \rho u \cdot \nabla c = \nabla \cdot \left ( \rho u c \right ) - c \nabla \cdot \left ( \rho u \right ) $$

The expression on the right has different numerical properties than the expression on the left. The term $\nabla \cdot \left ( \rho u c \right )$ is conservative (eg summing over the volume returns 0). Moreover, the divergence can be evaluated on cells, if the flux $\rho u c$ can be evaluated on the cell interfaces. This is natural on the C-grid, using WENO reconstruction for $c$.

The second term is $c \nabla \cdot \left ( \rho u \right )$. This too is naturally evaluated on the C-grid, because $c$ is already at the right location ans the mass divergence $\nabla \cdot \left ( \rho u \right )$ naturally. Note, we need to evaluate $\nabla \cdot \left ( \rho u \right )$ to step forward density with CompressibleDynamics or to compute the pressure correction with AnelasticDynamics. For that reason, we already have operators for it.

When we have CompressibleDynamics or AnelasticDynamics, this second term combines with the tendency term or vanishes (respectively).

However with PrescribedDynamics, we want to keep this second term.

@kaiyuan-cheng
Copy link
Collaborator

Screenshot 2026-01-09 at 8 17 20 PM

This is the desired result! Is WENO being used in this case?

Comment on lines +40 to +47
# Convenient method for letting the user specify only the divergence parameter
# and infer the others.
function PrescribedDynamics{Div}(density::D,
pressure::P,
surface_pressure::FT,
standard_pressure::FT) where {Div, D, P, FT}
return PrescribedDynamics{Div, D, P, FT}(density, pressure, surface_pressure, standard_pressure)
end
Copy link
Collaborator

Choose a reason for hiding this comment

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

I added this method for resolving the method error when trying to run

return PrescribedDynamics{divergence_correction}(density, pressure, p₀, pˢᵗ)
You expected to be able to call PrescribedDynamics with a single type parameter, but in reality it requires 3 more and there was no method to actually do it. I presume you wanted something like this (and tests pass for me now), but feel free to adapt it if you were thinking something else.

Copy link
Member Author

Choose a reason for hiding this comment

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

do we need a inner constructor then?

docs/make.jl Outdated
Comment on lines 57 to 58
example_pages = [] # Empty for fast builds

Copy link
Collaborator

Choose a reason for hiding this comment

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

This is all green now, can this be removed? Do you want to add the new example to the list?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes and yes

@glwagner
Copy link
Member Author

Screenshot 2026-01-09 at 8 17 20 PM

This is the desired result! Is WENO being used in this case?

yes

@glwagner glwagner merged commit 8f62cd7 into main Jan 14, 2026
10 of 11 checks passed
@glwagner glwagner deleted the glw/kinematic-driver branch January 14, 2026 04:01
@giordano giordano added the breaking change 💔 Concerning a change which breaks the API label Jan 14, 2026
@giordano giordano mentioned this pull request Jan 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change 💔 Concerning a change which breaks the API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants