Skip to content

Conversation

@haakon-e
Copy link
Member

@haakon-e haakon-e commented Nov 11, 2025

This pull request introduces a new "kinematic driver" configuration and framework for running prescribed flow field simulations based on the Shipway & Hill (2012) setup. The main changes add support for prescribed velocity profiles, new initial and surface conditions, and associated configuration and plotting tools. These updates enable direct comparison and testing of microphysics schemes in a controlled, kinematic environment.

Key changes include:

Support for Prescribed Flow (Kinematic Driver):

  • Added a new prescribed_flow option to the configuration system, allowing users to specify a prescribed velocity profile (currently ShipwayHill2012VelocityProfile). The model infrastructure and types are updated to support this option and propagate it through the simulation. [1] [2] [3] [4] [5] [6] [7]
  • Implemented logic in the solver and prognostic equations to apply the prescribed flow, including setting velocities and fixing energy to initial temperature profiles as needed. [1] [2]

New Initial and Surface Conditions:

  • Added ShipwayHill2012 initial condition and surface setup, matching the profiles and boundary conditions described in Shipway & Hill (2012). These are now selectable via configuration. [1] [2] [3]

Kinematic Driver Configuration and Example:

  • Introduced a new model configuration file (kinematic_driver.yml) and a corresponding example script (KiD_driver.jl) to demonstrate and test the kinematic driver setup. [1] [2]

Upwinding and Advection Improvements:

  • Added a new upwinding scheme (first_order_kid) for vertical advection, with special handling for the prescribed flow scenario. [1] [2] [3]

Postprocessing and Documentation Updates:

  • Added plotting routines and bibliography entry for Shipway & Hill (2012), supporting analysis and reproducibility of the kinematic driver results. [1] [2]

These changes collectively enable controlled, reproducible kinematic microphysics experiments within the modeling framework.


Illustrative results

Simulation with Equil+0M, with precipitation turned off:

KiD comparison: https://buildkite.com/clima/kinematicdriver-ci/builds/913/steps/canvas?sid=019a1277-c888-440f-a2b7-8eb2e6b6242e

image image image
Simulation with Equil+0M, with precipitation turned on:

KiD comparison: https://buildkite.com/clima/kinematicdriver-ci/builds/913/steps/canvas?sid=019a1277-c889-4001-b378-396d34aa2a8a

image image image

@haakon-e haakon-e marked this pull request as draft November 11, 2025 23:59
@haakon-e haakon-e force-pushed the he/ft-add-kinematic-driver-kid branch 4 times, most recently from 7160992 to 874b360 Compare November 20, 2025 17:39
@haakon-e haakon-e marked this pull request as ready for review November 21, 2025 01:37
@haakon-e
Copy link
Member Author

@dennisYatunin, @trontrytel

Probably needs some more cleanup, but would value your input.

Dennis: Could you provide feedback on whether the proposed interface isn't too obtrusive for everyone else who wants to do "normal" ClimaAtmos simulations?

Anna: We can externalize some parameters later, but given the results (expand the sections above), I think this is a great start and we should merge this!

@haakon-e haakon-e linked an issue Nov 21, 2025 that may be closed by this pull request
8 tasks
@haakon-e haakon-e force-pushed the he/ft-add-kinematic-driver-kid branch from c8252b9 to 6fe6b70 Compare November 25, 2025 00:55
(; z) = local_geometry.coordinates
return LocalState(; params, geometry = local_geometry,
thermo_state = TD.PhaseEquil_pθq(thermo_params, p(z), θ(z), q_tot(z)),
precip_state = PrecipStateMassNum(; q_rai = FT(0), q_sno = FT(0)),
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
precip_state = PrecipStateMassNum(; q_rai = FT(0), q_sno = FT(0)),

Removing this line will probably enable GPU compatibility, since FT is not inferable here.

Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't that mean we are initializing the state vector without precipitation in it?

Copy link
Member

Choose a reason for hiding this comment

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

The current version also initializes the state without precipitation. The new version should be identical, as long as the LocalState constructor automatically pads with zeros.

@@ -0,0 +1,68 @@
import ClimaComms
Copy link
Member

@dennisYatunin dennisYatunin Nov 26, 2025

Choose a reason for hiding this comment

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

It looks like everything in this file can be handled by the current driver, so there is no need to add a new one.

Comment on lines +150 to +152
if p.atmos.numerics.energy_q_tot_upwinding == Val(:first_order_kid)
vtt = NullBroadcasted() # Turn off implicit energy q_tot advection
end
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if p.atmos.numerics.energy_q_tot_upwinding == Val(:first_order_kid)
vtt = NullBroadcasted() # Turn off implicit energy q_tot advection
end

This will make the implicit solver's sound wave propagation inconsistent, with ∂ᶜρq_tot/∂ᶠu₃ being zeroed out while ∂ᶠu₃/∂ᶜρq_tot (and all other ᶜρχ blocks) retains its current nonzero value. To handle sound waves like we do in other configurations, leave the implicit tendency and its Jacobian as they are now.

Comment on lines +289 to 293
if energy_q_tot_upwinding == Val(:first_order_kid)
vtt = vertical_transport_kid(ᶜρ, ᶠu³, ᶜq_tot, FT(dt), FT(t))
vtt_central = NullBroadcasted() # turn off implicit advection
end
@. Yₜ.c.ρq_tot += vtt - vtt_central
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if energy_q_tot_upwinding == Val(:first_order_kid)
vtt = vertical_transport_kid(ᶜρ, ᶠu³, ᶜq_tot, FT(dt), FT(t))
vtt_central = NullBroadcasted() # turn off implicit advection
end
@. Yₜ.c.ρq_tot += vtt - vtt_central
vtt_bc = ᶜρq_tot_vertical_transport_bc(ᶠu³, p.atmos.prescribed_flow, t)
@. Yₜ.c.ρq_tot += vtt - vtt_central + vtt_bc

If the implicit tendency change is reverted, you can simplify this explicit term by keeping the current first-order upwinding tendency (minus the implicit sound wave component) and just adding a new boundary term, which could look something like

    function ᶜρq_tot_vertical_transport_bc(ᶠu³, flow::PrescribedFlow{FT}, t) where {FT}
        ρ_sfc = FT(1.1650101)
        q_tot_sfc = FT(0.014778325)
        u₃_sfc = Geometry.WVector(flow(FT(0), t))
        ᶜadvdivᵥ = Operators.DivergenceF2C(;
            bottom = Operators.SetValue(ρ_sfc * q_tot_sfc * u₃_sfc),
        )
        return @. lazy(-(ᶜadvdivᵥ(zero(ᶠu³)))
    end

Also, since this term is treated explicitly, it should probably be defined here in advection.jl instead of implicit_tendency.jl.

Comment on lines 81 to +84
if do_dss(axes(Y.c))
Spaces.weighted_dss!(Y.c => p.ghost_buffer.c, Y.f => p.ghost_buffer.f)
end
prescribe_flow!(Y, p, t, p.atmos.prescribed_flow)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if do_dss(axes(Y.c))
Spaces.weighted_dss!(Y.c => p.ghost_buffer.c, Y.f => p.ghost_buffer.f)
end
prescribe_flow!(Y, p, t, p.atmos.prescribed_flow)
prescribe_flow!(Y, p, t, p.atmos.prescribed_flow)
if do_dss(axes(Y.c))
Spaces.weighted_dss!(Y.c => p.ghost_buffer.c, Y.f => p.ghost_buffer.f)
end

It would be safer to do this in the opposite order, so that DSS is always the last operation in a Runge-Kutta stage. Roundoff errors in the prescribed flow could potentially introduce discontinuities across element boundaries, which will be propagated to the next stage if DSS is not applied afterward.

@. Y.c.ρ = ᶜρ_init_dry + Y.c.ρq_tot
ᶜts = @. lazy(TD.PhaseEquil_ρTq(thermo_params, Y.c.ρ, ᶜT_init, Y.c.ρq_tot / Y.c.ρ))
ᶜe_kin = compute_kinetic(Y.c.uₕ, Y.f.u₃)
ᶜe_pot = @. lazy(grav * z)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
ᶜe_pot = @. lazy(grav * z)
(; ᶜΦ) = p.core

This is already cached in p.

Comment on lines +483 to +486
if !(p.atmos.prescribed_flow isa PrescribedFlow)
set_velocity_at_surface!(Y, ᶠuₕ³, turbconv_model)
set_velocity_at_top!(Y, turbconv_model)
end
Copy link
Member

Choose a reason for hiding this comment

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

In simulations with topography, set_velocity_at_surface! and set_velocity_at_top! are needed to ensure that the state's velocity components match its advective tendency. If the advective tendency assumes zero-flux (CT3(0)) boundary conditions, these functions modify Y.f.u₃ to enforce that. In your case, fluxes at the surface are prescribed some nonzero value (i.e., the projection CT3(ρ_sfc * q_tot_sfc * u₃_sfc), where u₃_sfc is specified as a WVector), so set_velocity_at_surface! and set_velocity_at_top! should make Y.c.uₕ and Y.f.u₃ consistent with that.

If you never intend to run this configuration with topography, then you can leave this as is, but add a check somewhere in type_getters.jl to make sure that there is no topography.

Copy link
Member

Choose a reason for hiding this comment

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

Hmmm - we saw some inconsistency at the surface in our advected fields that went away only when we removed those set_velocity functions from our Kid run

z_max: 2e3
z_elem: 128
z_stretch: false
use_auto_jacobian: true # Needed!! (+only 1 Newton iteration, which is the default)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
use_auto_jacobian: true # Needed!! (+only 1 Newton iteration, which is the default)

This shouldn't be needed if you leave the current implicit tendency unchanged.

Cd = FT(0.0)
Ch = FT(0.0)
parameterization = ExchangeCoefficients(; Cd, Ch)
# <<<
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# <<<

struct ShipwayHill2012 end
function (::ShipwayHill2012)(params)
function surface_state(surface_coordinates, interior_z, t)
FT = eltype(surface_coordinates)
Copy link
Member

Choose a reason for hiding this comment

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

We don't need most of it since we ended up setting the coefficients to zero and prescribing the flux?

FT = eltype(ᶜρ)
ᶜJ = Fields.local_geometry_field(axes(ᶜρ)).J
ᶠJ = Fields.local_geometry_field(axes(ᶠu³)).J
ρ_sfc = FT(1.1650101) # TODO: Get from initial conditions or state
Copy link
Member

Choose a reason for hiding this comment

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

Would be great to avoid hardcoding. But we can also solve it later

@trontrytel trontrytel self-requested a review November 27, 2025 00:04
Copy link
Member

@trontrytel trontrytel left a comment

Choose a reason for hiding this comment

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

LGTM! But I was also there when most of it was written so I may be biased ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Shipway and Hill 2012 kinematic driver tests to ClimaAtmos

4 participants