Refactor ground heat flux into separate subprocess of SEB and add PrescribedGroundHeatFlux - #200
bgroenks96 wants to merge 5 commits into
Conversation
5b8bda6 to
2d832d4
Compare
c7b6156 to
49d9ce0
Compare
|
@olivierbonte I haven't gone through this myself yet so I will also do a review here. |
PrescribedGroundHeatFluxPrescribedGroundHeatFlux
| land model configuration: an implicitly solved skin temperature with all surface fluxes diagnosed | ||
| internally. | ||
|
|
||
| Not every combination of `skin_temperature` and `ground_heat_flux` is well posed; the supported |
There was a problem hiding this comment.
I don't think "well posed" is the right term here.
There was a problem hiding this comment.
replace by "valid"? I also don't like the term well posed in this context, I think it should be reserverd for the mathematical context where this terms orginiates from
| """ | ||
| @propagate_inbounds function compute_ground_heat_flux_demand(i, j, grid, fields, ghf::DiagnosedGroundHeatFlux) | ||
| # Get individual flux terms | ||
| R_net = fields.surface_net_radiation[i, j, end] |
There was a problem hiding this comment.
This works but violates our convention of only accessing fields defined by the function arguments; i.e. those which would be returned by get_fields(state, args...). We could just add seb to the signature.
olivierbonte
left a comment
There was a problem hiding this comment.
What is conceptually not that clear to me: when we are prescribing G, the flux into the ground, or the compound G*, which can include the snow flux? If the first is the case, and skin temperature is also prescribed, I think you have the risk of violating the energy balance. If the compound flux is prescribed, then this needs to be redistributed between G and S in the model code.
In general, the G = R_n + H_s + H_l that is used in a lot of docstrings is a bit confusing if you factor in snow. It would maybe be good to have separate symbols for 1) the demand from the atmosphere 2) the supply by the ground system with potential snow 3) the always soil only flux
| - With [`PrescribedSkinTemperature`](@ref), there is no separate conduction target, so the stored flux *is* the demand, $G = G^\star = R_{\text{net}} + H_s + H_l$. | ||
| - With [`ImplicitSkinTemperature`](@ref), the stored flux is the explicit bare-ground conductive flux evaluated at the current skin temperature, $G = 2\kappa_s (T_g - T_s) / \Delta z_1$. This coincides with the demand only at convergence of the skin temperature solve (see [Skin temperature](@ref "Skin temperature")). | ||
|
|
||
| ```@example ghf |
There was a problem hiding this comment.
why is this ghf needed?
| DiagnosedGroundHeatFlux | ||
| ``` | ||
|
|
||
| [`DiagnosedGroundHeatFlux`](@ref) closes the surface energy balance internally. What is stored depends on the accompanying skin temperature scheme: |
There was a problem hiding this comment.
reading the docs first (before looking at the code) it is unclear what this stored means. Maybe this is logical from a code persepctive, but I can't follow what this means from a process point of view
| PrescribedGroundHeatFlux | ||
| ``` | ||
|
|
||
| [`PrescribedGroundHeatFlux`](@ref) declares `ground_heat_flux` as an *input* variable and computes nothing. Whatever is written into the field, typically by an external coupler that owns the atmosphere-land interface, survives untouched through `compute_auxiliary!` and reaches the ground boundary condition. This is the configuration that makes [`PrescribedSurfaceEnergyBalance`](@ref) possible; see [Surface energy balance](@ref surface_energy_balance_docs) for the supported combinations. |
There was a problem hiding this comment.
| [`PrescribedGroundHeatFlux`](@ref) declares `ground_heat_flux` as an *input* variable and computes nothing. Whatever is written into the field, typically by an external coupler that owns the atmosphere-land interface, survives untouched through `compute_auxiliary!` and reaches the ground boundary condition. This is the configuration that makes [`PrescribedSurfaceEnergyBalance`](@ref) possible; see [Surface energy balance](@ref surface_energy_balance_docs) for the supported combinations. | |
| [`PrescribedGroundHeatFlux`](@ref) declares `ground_heat_flux` as an *input* variable and computes nothing. Whatever is written into the field (e.g. by a land-atmosphere coupler), is not modified by `compute_auxiliary!`. This is the configuration that makes [`PrescribedSurfaceEnergyBalance`](@ref) possible; see [Surface energy balance](@ref surface_energy_balance_docs) for the supported combinations. |
| ```math | ||
| G^\star = R_{\text{net}} + H_s + H_l | ||
| ``` | ||
| where $R_{\text{net}}$ is the net radiation budget, $H_s$ is the sensible heat flux, and $H_l$ is the latent heat flux. How that demand is realized, and whether it is imposed at all, is the responsibility of the [`AbstractGroundHeatFlux`](@ref) sub-process. |
There was a problem hiding this comment.
| where $R_{\text{net}}$ is the net radiation budget, $H_s$ is the sensible heat flux, and $H_l$ is the latent heat flux. How that demand is realized, and whether it is imposed at all, is the responsibility of the [`AbstractGroundHeatFlux`](@ref) sub-process. | |
| where $G^\star$ is the ground heat flux *demanded* by the SEB, $R_{\text{net}}$ is the net radiation budget, $H_s$ is the sensible heat flux, and $H_l$ is the latent heat flux. How that demand is realized, and whether it is imposed at all, is the responsibility of the [`AbstractGroundHeatFlux`](@ref) sub-process. |
| ``` | ||
|
|
||
| !!! warning "Over-determination" | ||
| Nothing enforces $G = R_{\text{net}} + H_s + H_l$ when the ground heat flux is prescribed alongside prescribed radiative and turbulent fluxes. It is the caller's responsibility to supply a mutually consistent set; Terrarium will happily integrate an inconsistent one. |
There was a problem hiding this comment.
| Nothing enforces $G = R_{\text{net}} + H_s + H_l$ when the ground heat flux is prescribed alongside prescribed radiative and turbulent fluxes. It is the caller's responsibility to supply a mutually consistent set; Terrarium will happily integrate an inconsistent one. | |
| Nothing enforces $G = R_{\text{net}} + H_s + H_l$ when the ground heat flux is prescribed alongside prescribed radiative and turbulent fluxes. It is the user's responsibility to supply a mutually consistent set. |
|
|
||
| Ground heat flux `G` supplied externally as an input variable, for example assembled by a coupler from | ||
| the atmosphere-side surface energy budget. Nothing is computed: `compute_ground_heat_flux!` is a no-op, | ||
| so whatever was written into the `ground_heat_flux` field survives to the ground (soil) top boundary |
There was a problem hiding this comment.
| so whatever was written into the `ground_heat_flux` field survives to the ground (soil) top boundary | |
| so whatever was written into the `ground_heat_flux` is passed on directly the ground (soil) top boundary |
| condition. The same positive-upward convention applies as for [`DiagnosedGroundHeatFlux`](@ref). | ||
|
|
||
| Note that with all four surface fluxes prescribed, the residual identity `G = R_net + H_s + H_l` is | ||
| *not* enforced by construction; it is the caller's responsibility to supply a consistent set. |
There was a problem hiding this comment.
| *not* enforced by construction; it is the caller's responsibility to supply a consistent set. | |
| *not* enforced by construction; it is the user's responsibility to supply a consistent set. |
| atmosphere-side demanded flux `G` (`= R_net + H_s + H_l`): `Ts = Tg − G/(2κg/Δzg)`. This is the no-snow | ||
| special case (`f_snow = 0`) of the snow-aware method below; it is a separate method (rather than a default | ||
| `snow = nothing`) purely so it can skip the unused `snow_thermal_interface`/`snow_cover_fraction` calls. | ||
| atmosphere-side demanded flux `G` (see [`compute_ground_heat_flux_demand`](@ref)): `Ts = Tg − G/(2κg/Δzg)`. |
There was a problem hiding this comment.
| atmosphere-side demanded flux `G` (see [`compute_ground_heat_flux_demand`](@ref)): `Ts = Tg − G/(2κg/Δzg)`. | |
| atmosphere-side demanded flux `G*` (see [`compute_ground_heat_flux_demand`](@ref)): `Ts = Tg − G*/(2κg/Δzg)`. |
|
|
||
| [`DiagnosedGroundHeatFlux`](@ref) closes the surface energy balance internally. What is stored depends on the accompanying skin temperature scheme: | ||
|
|
||
| - With [`PrescribedSkinTemperature`](@ref), there is no separate conduction target, so the stored flux *is* the demand, $G = G^\star = R_{\text{net}} + H_s + H_l$. |
There was a problem hiding this comment.
But what happens with the snow in this case? Because we are violating the energy balance if this written G is fixed and not altered by the S flux
| land model configuration: an implicitly solved skin temperature with all surface fluxes diagnosed | ||
| internally. | ||
|
|
||
| Not every combination of `skin_temperature` and `ground_heat_flux` is well posed; the supported |
There was a problem hiding this comment.
replace by "valid"? I also don't like the term well posed in this context, I think it should be reserverd for the mathematical context where this terms orginiates from
Extract the ground heat flux from `AbstractSkinTemperature` into its own surface-energy-balance sub-process, `AbstractGroundHeatFlux`, with `Diagnosed` and `Prescribed` implementations mirroring the existing pairs for the radiative and turbulent fluxes. `PrescribedSurfaceEnergyBalance` becomes a type alias for a `SurfaceEnergyBalance` whose four flux sub-processes are all prescribed, plus a convenience constructor.
Co-authored-by: Olivier Bonte <65309133+olivierbonte@users.noreply.github.com>
70aad11 to
5909761
Compare
Extracts the ground heat flux out of
AbstractSkinTemperatureand into its ownsurface-energy-balance sub-process, so it gets the same
Prescribed/Diagnosedpair that the radiative and turbulent fluxes already have.
Implements
docs/dev/2026-09/2026-09-01_PLAN_prescribed_surface_energy_balance.md(Rev 2, with three deviations logged as Rev 3).
Why
Ghad no process of its own: the accessor, the residual closure, and roughlyeight
compute_ground_heat_flux*methods all dispatched onAbstractSkinTemperature,and the field was declared twice. It could not be prescribed without also replacing
the skin temperature scheme, which is exactly what a coupler needs to do. In the
coupled
EarthSystemModel, a value written intoground_heat_fluxbyInterfaceComputationsdid not survive, because Terrarium recomputed it as theresidual before the soil tendency read it.
What changed
AbstractGroundHeatFluxwithDiagnosedGroundHeatFluxandPrescribedGroundHeatFlux, insrc/processes/surface/ground_heat_flux.jl.The
compute_ground_heat_flux*family moves there fromskin_temperature.jl.ground_heat_fluxis now declared exactly once, by the sub-process:auxiliarywhen diagnosed,
inputwhen prescribed. Without snow it is the soil-topboundary condition, so a prescribed value routes straight to the soil column.
SurfaceEnergyBalancegains a fifth sub-process field, defaulting toDiagnosedGroundHeatFlux. Its type parameters are reordered to match the fieldorder, fixing the pre-existing radiative/turbulent swap.
PrescribedSurfaceEnergyBalanceis a type alias for the all-prescribedconfiguration, plus a convenience constructor.
skin_temperature.mdis retitled "Skin temperature" and its inbound@refs updated.Note for reviewers
The atmosphere-side demand dispatches on
AbstractGroundHeatFlux, not onDiagnosedGroundHeatFluxalone, and is threaded intocompute_skin_temperatureas a new
ghfargument. This is what makesImplicitSkinTemperature+PrescribedGroundHeatFluxwell posed. Without it, that pairing would invertagainst
R_net + H_s + H_lwhile storing an unrelatedG. It is the mainsignature change outside the new file, and it also touches
diagnose_skin_temperature_residual.DiagnosedGroundHeatFluxstill dispatches on the skin temperature scheme: theplan described it as always storing
R_net + H_s + H_l, but that is only thePrescribedSkinTemperaturecase, whileImplicitSkinTemperaturestores theexplicit conductive flux. Both methods moved across unchanged.
Verification
LandModelrun dumping
ground_heat_flux,skin_temperature,surface_net_radiation, bothturbulent fluxes, and the final
internal_energyprofile gives a maximum absolutedifference of exactly
0.0against the base commit, over 180 values.Pkg.test()suite passes; new tests intest/surface/ground_heat_flux.jlcover variable classification, the alias and its type parameter ordering, the
no-overwrite guarantee, and energy conservation (
ΔU = −G·Δt·N).timestep!with respect to the prescribedground_heat_fluxintest/differentiability/prescribed_seb_diff.jl.(3504 B in
solve_surface_energy_balance!, zero incompute_auxiliary!).docs_blockerrors.Out of scope
Trimming
NumericalEarthTerrariumExt'supdate_net_fluxes!and the coupledend-to-end ERA5 run both live in
NumericalEarth.jland follow separately.Moisture is not prescribed here; the vapor seam is the immediate follow-up.
🤖 Generated with Claude Code