Skip to content

Commit df68c6a

Browse files
committed
explicit and implicit sources
1 parent e4e4d7a commit df68c6a

File tree

15 files changed

+172
-56
lines changed

15 files changed

+172
-56
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ ClimaLand.jl Release Notes
33

44
main
55
-------
6+
- Add capability to step some sources implicitly PR[#1113](https://github.com/CliMA/ClimaLand.jl/pull/1113)
67

78
v0.15.14
89
-------

docs/list_tutorials.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ tutorials = [
4343
"Intro to multi-component models" => "standalone/Usage/LSM_single_column_tutorial.jl",
4444
"Intro to ClimaLand Domains" => "standalone/Usage/domain_tutorial.jl",
4545
"Intro to forced site-level runs" => "shared_utilities/driver_tutorial.jl",
46+
"Intro to implicit/explicit timestepping" => "shared_utilities/timestepping.jl",
4647
],
4748
]
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# # Mixed implicit and explicit timestepping
2+
# The goal of this tutorial is to describe how
3+
# the timestepping of a ClimaLand model is carried out.
4+
# We will use forward and backward Euler as a demonstration,
5+
# but higher order methods are available in ClimaTimesteppers.
6+
7+
# # Explicit vs. implicit stepping
8+
# Given a differential equation for a prognostic variable
9+
# Y
10+
11+
# ```
12+
# \frac{d Y }{dt} = g(Y, t) + h(Y, t),
13+
# ```
14+
15+
# an explicit (forward) Euler step would entail
16+
17+
# ```
18+
# Y(t+\Delta t) = Y(t) + [g(Y(t), t) + h(Y(t),t)] \times \Delta t,
19+
# ```
20+
21+
# while an implicit (backward) Euler step would entail
22+
23+
# ```
24+
# Y(t+\Delta t) = Y(t) + [g(Y(t+\Delta t), t) + h(Y(t+\Delta t),t)] \times \Delta t,
25+
# ```
26+
27+
# which reqires us to solve an implicit equation for Y(t+ Δt). We
28+
# usually do so using Newton's method, which requires the derivative of
29+
# the entire right hand side with respect to the variable we are solving
30+
# for, `Y(t+ Δt)`. This is called the Jacobian.
31+
32+
# Sometimes certain terms must be stepped implicit for numerical stability,
33+
# while others are more slowly varying or stable. In this case, a mixed
34+
# approach would be
35+
36+
# ```
37+
# Y(t+\Delta t) = Y(t) + [g(Y(t+\Delta t), t) + h(Y(t),t)] \times \Delta t,
38+
# ```
39+
40+
# assuming that `h` is the slow term, and `g` is the fast term. Note that
41+
# solving this implicit equation for `(Y(t+ Δt)` with Newton's method
42+
# would be similar to that of the fully implicit approach, but with an
43+
# approximated Jacobian (neglecting ∂h/∂Y).
44+
45+
# If our timestepping scheme involves evaluating all right-hand-side
46+
# tendencies at the current (known) value of a prognostic variable,
47+
# we refer to that prognostic variable as explicit. If any of the
48+
# right-hand-side tendencies are evaluated at the next (unknown) value
49+
# of the prognostic variable, we refer to it as implicit. In the latter
50+
# case, the Jacobian would include a term like `∂ tendency/∂ variable`,
51+
# even if it is an approximate (not exact) form of the Jacobian.
52+
53+
# # Implicit and explicit prognostic variables of the land model
54+
# We treat two prognostic variables of the soil model (ϑ_l,
55+
# ρe_int) and the canopy temperature implicitly, and
56+
# the canopy water content, the soil ice content,
57+
# and all prognostic
58+
# variables of the snow model explicitly.
59+
60+
# # Implicit vs explicit tendencies - not complete as of 4/23/25
61+
# Implicit:
62+
# - Vertical contribution of the divergence of the Darcy flux
63+
# - Vertical contribution of the divergence of diffusive heat flux
64+
# - Vertical contribution of the divergence of heat flux due to Darcy flow
65+
# - Canopy temperature (except root extraction of energy)
66+
# - SHF, LHF, evaporation, and sublimation of soil (note that these are explicit in θ_i!)
67+
# - Soil radiation (does not contribute to Jacobian)
68+
# - Subsurface runoff (this is computed in the same function the same time as surface runoff, but does not contribute to Jacobian.)
69+
70+
# Explicit
71+
# - Phase changes in soil
72+
# - Root extraction
73+
# - all snow tendencies
74+
# - Darcy flux within the canopy

docs/tutorials/standalone/Soil/sublimation.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ init_soil!(Y, z, soil.parameters);
158158
# Timestepping:
159159
t0 = Float64(0)
160160
tf = Float64(24 * 3600 * 4)
161-
dt = Float64(5)
161+
dt = Float64(10)
162162

163163
# We also set the initial conditions of the cache here:
164164
set_initial_cache! = make_set_initial_cache(soil)
@@ -175,7 +175,7 @@ timestepper = CTS.ARS111()
175175
ode_algo = CTS.IMEXAlgorithm(
176176
timestepper,
177177
CTS.NewtonsMethod(
178-
max_iters = 1,
178+
max_iters = 3,
179179
update_j = CTS.UpdateEvery(CTS.NewNewtonIteration),
180180
),
181181
)

src/integrated/soil_canopy_root_interactions.jl

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,11 @@ in an LSM with both soil and plant hydraulic components.
125125
This is paired with the source term `Canopy.PrognosticSoil`:both
126126
are used at the same time,
127127
ensuring that the water flux into the roots is extracted correctly
128-
from the soil.
128+
from the soil; treated explicitly in all prognostic variables.
129129
"""
130-
struct RootExtraction{FT} <: Soil.AbstractSoilSource{FT} end
131-
130+
@kwdef struct RootExtraction{FT} <: ClimaLand.Soil.AbstractSoilSource{FT}
131+
explicit::Bool = true
132+
end
132133
"""
133134
ClimaLand.source!(dY::ClimaCore.Fields.FieldVector,
134135
src::RootExtraction,
@@ -150,5 +151,16 @@ function ClimaLand.source!(
150151
)
151152
@. dY.soil.ϑ_l += -1 * p.root_extraction
152153
@. dY.soil.ρe_int += -1 * p.root_energy_extraction
154+
153155
# if flow is negative, towards soil -> soil water increases, add in sign here.
156+
# Currently, these column integrals are done twice rather than add space to the cache.
157+
# We can revisit this as needed.
158+
ClimaCore.Operators.column_integral_definite!(
159+
p.scratch1,
160+
p.root_energy_extraction,
161+
)
162+
@. dY.soil.∫F_e_dt += p.scratch1
163+
164+
ClimaCore.Operators.column_integral_definite!(p.scratch1, p.root_extraction)
165+
@. dY.soil.∫F_vol_liq_water_dt += p.scratch1
154166
end

src/integrated/soil_snow_model.jl

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,9 +390,15 @@ end
390390
SoilSublimationwithSnow{FT} <: AbstractSoilSource{FT}
391391
392392
Soil Sublimation source type. Used to defined a method
393-
of `ClimaLand.source!` for soil sublimation with snow present.
393+
of `ClimaLand.source!` for soil sublimation with snow present;
394+
treated implicitly
395+
in ϑ_l, ρe_int but explicitly in θ_i.
396+
394397
"""
395-
struct SoilSublimationwithSnow{FT} <: ClimaLand.Soil.AbstractSoilSource{FT} end
398+
@kwdef struct SoilSublimationwithSnow{FT} <:
399+
ClimaLand.Soil.AbstractSoilSource{FT}
400+
explicit::Bool = false
401+
end
396402

397403
"""
398404
source!(dY::ClimaCore.Fields.FieldVector,
@@ -419,7 +425,10 @@ function ClimaLand.source!(
419425
@. dY.soil.θ_i +=
420426
-p.soil.turbulent_fluxes.vapor_flux_ice *
421427
(1 - p.snow.snow_cover_fraction) *
422-
_ρ_l / _ρ_i * heaviside(z + 2 * Δz_top) # only apply to top layer, recall that z is negative
428+
_ρ_l / _ρ_i * heaviside(z + 2 * Δz_top) / (2 * Δz_top) # only apply to top layer, recall that z is negative
429+
@. dY.soil.∫F_vol_liq_water_dt +=
430+
-p.soil.turbulent_fluxes.vapor_flux_ice *
431+
(1 - p.snow.snow_cover_fraction) # The integral of the source is designed to be this
423432
return nothing
424433
end
425434

src/shared_utilities/Domains.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -935,7 +935,7 @@ function get_additional_coordinate_field_data(subsurface_space)
935935
z_face = ClimaCore.Fields.coordinate_field(face_space).z
936936
z_sfc = top_face_to_surface(z_face, surface_space)
937937
d = depth(subsurface_space)
938-
Δz_min = minimum(Δz_top)
938+
Δz_min = minimum(Δz)
939939
fields = (;
940940
z = z,
941941
Δz_top = Δz_top,

src/standalone/Soil/Runoff/Runoff.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,21 @@ The runoff flux is given by Equation 12 of Niu et al. (2005),
163163
"A simple TOPMODEL-based runoff parameterization (SIMTOP) for
164164
use in global climate models".
165165
166+
This is currently treated implicitly (evaluated at the next value of
167+
ϑ_l) but not included in the Jacobian approximation. This is because
168+
`update_runoff` is called as part of the implicit tendency.
169+
166170
$(DocStringExtensions.FIELDS)
167171
"""
168172
struct TOPMODELSubsurfaceRunoff{FT} <: AbstractSoilSource{FT}
169173
"The subsurface runoff flux (m/s) when the depth to the water table = 1/f_over; calibrated"
170174
R_sb::FT
171175
"A calibrated parameter defining how subsurface runoff decays with depth to water table (1/m ; calibrated)"
172176
f_over::FT
177+
"Explicit vs implicit stepping"
178+
explicit::Bool
179+
TOPMODELSubsurfaceRunoff{FT}(R_sb, f_over) where {FT} =
180+
new{FT}(R_sb, f_over, false)
173181
end
174182

175183
"""
@@ -290,7 +298,7 @@ function ClimaLand.source!(
290298
h∇ = p.soil.h∇
291299
ϵ = eps(FT)
292300
@. dY.soil.ϑ_l -= (p.soil.R_ss / max(h∇, ϵ)) * p.soil.is_saturated # apply only to saturated layers
293-
@. p.soil.∫S_θ_liq_dz -= p.soil.R_ss # the integral is designed to be this flux
301+
@. dY.soil.∫F_vol_liq_water_dt -= p.soil.R_ss # the integral is designed to be this flux
294302
end
295303

296304
"""

src/standalone/Soil/energy_hydrology.jl

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,8 @@ function ClimaLand.make_compute_exp_tendency(
198198
function compute_exp_tendency!(dY, Y, p, t)
199199
z = model.domain.fields.z
200200

201-
p.soil.∫S_θ_liq_dz .= 0
202-
p.soil.∫S_ρe_int_dz .= 0
201+
dY.soil.∫F_vol_liq_water_dt .= 0
202+
dY.soil.∫F_e_dt .= 0
203203

204204
# Don't update the prognostic variables we're stepping implicitly
205205
dY.soil.ϑ_l .= 0
@@ -218,13 +218,13 @@ function ClimaLand.make_compute_exp_tendency(
218218
p,
219219
z,
220220
)
221-
# Source terms, also update the vertical integral of the source: p.soil.∫S_θ_liq_dz, p.soil.∫S_ρe_int_dz
222-
# These change ∫S and dY by +=, which is why we zero them out above.
221+
# Explicitly treated source terms
222+
# These change dY by +=, which is why we ".=" them above
223223
for src in model.sources
224-
ClimaLand.source!(dY, src, Y, p, model)
224+
if src.explicit
225+
ClimaLand.source!(dY, src, Y, p, model)
226+
end
225227
end
226-
dY.soil.∫F_vol_liq_water_dt .= p.soil.∫S_θ_liq_dz # source terms are stepped explicitly
227-
dY.soil.∫F_e_dt .= p.soil.∫S_ρe_int_dz # source terms are stepped explicitly
228228
end
229229
return compute_exp_tendency!
230230
end
@@ -291,8 +291,15 @@ function ClimaLand.make_compute_imp_tendency(
291291
) * gradc2f(p.soil.ψ + z),
292292
)
293293

294-
# Don't update the prognostic variables we're stepping explicitly
295294
@. dY.soil.θ_i = 0
295+
296+
# Source terms
297+
# These change dY by +=, which is why we ".=" above
298+
for src in model.sources
299+
if !src.explicit
300+
ClimaLand.source!(dY, src, Y, p, model)
301+
end
302+
end
296303
end
297304
return compute_imp_tendency!
298305
end
@@ -493,8 +500,6 @@ of `EnergyHydrology`.
493500
ClimaLand.auxiliary_vars(soil::EnergyHydrology) = (
494501
:total_water,
495502
:total_energy,
496-
:∫S_θ_liq_dz,
497-
:∫S_ρe_int_dz,
498503
:K,
499504
,
500505
:θ_l,
@@ -521,8 +526,6 @@ ClimaLand.auxiliary_types(soil::EnergyHydrology{FT}) where {FT} = (
521526
FT,
522527
FT,
523528
FT,
524-
FT,
525-
FT,
526529
boundary_var_types(
527530
soil,
528531
soil.boundary_conditions.top,
@@ -536,8 +539,6 @@ ClimaLand.auxiliary_types(soil::EnergyHydrology{FT}) where {FT} = (
536539
)
537540

538541
ClimaLand.auxiliary_domain_names(soil::EnergyHydrology) = (
539-
:surface,
540-
:surface,
541542
:surface,
542543
:surface,
543544
:subsurface,
@@ -647,9 +648,12 @@ end
647648
"""
648649
PhaseChange{FT} <: AbstractSoilSource{FT}
649650
650-
PhaseChange source type.
651+
PhaseChange source type; treated explicitly in all
652+
prognostic variables.
651653
"""
652-
struct PhaseChange{FT} <: AbstractSoilSource{FT} end
654+
@kwdef struct PhaseChange{FT} <: AbstractSoilSource{FT}
655+
explicit::Bool = true
656+
end
653657

654658
"""
655659
source!(dY::ClimaCore.Fields.FieldVector,
@@ -659,7 +663,8 @@ struct PhaseChange{FT} <: AbstractSoilSource{FT} end
659663
model
660664
)
661665
662-
Computes the source terms for phase change.
666+
Computes the source terms for phase change
667+
explicitly in time.
663668
664669
"""
665670
function ClimaLand.source!(
@@ -728,7 +733,7 @@ function ClimaLand.source!(
728733
_grav,
729734
)
730735

731-
# These source/sink terms conserve total water, so we do not include them in ∫S_θ_liq_dz because their
736+
# These source/sink terms conserve total water, so we do not include them when checking conservation because their
732737
# contribution is zero by design.
733738
end
734739

@@ -737,10 +742,12 @@ end
737742
SoilSublimation{FT} <: AbstractSoilSource{FT}
738743
739744
Soil Sublimation source type. Used to defined a method
740-
of `ClimaLand.source!` for soil sublimation.
745+
of `ClimaLand.source!` for soil sublimation; treated implicitly
746+
in ϑ_l, ρe_int but explicitly in θ_i.
741747
"""
742-
struct SoilSublimation{FT} <: AbstractSoilSource{FT} end
743-
748+
@kwdef struct SoilSublimation{FT} <: AbstractSoilSource{FT}
749+
explicit::Bool = false
750+
end
744751
"""
745752
source!(dY::ClimaCore.Fields.FieldVector,
746753
src::SoilSublimation{FT},
@@ -767,7 +774,7 @@ function ClimaLand.source!(
767774
@. dY.soil.θ_i +=
768775
-p.soil.turbulent_fluxes.vapor_flux_ice * _ρ_l / _ρ_i *
769776
heaviside(z + 2 * Δz_top) / (2 * Δz_top) # only apply to top layer, recall that z is negative
770-
@. p.soil.∫S_θ_liq_dz += -p.soil.turbulent_fluxes.vapor_flux_ice # The integral of the source is designed to be this
777+
@. dY.soil.∫F_vol_liq_water_dt += -p.soil.turbulent_fluxes.vapor_flux_ice # The integral of the source is designed to be this
771778
end
772779

773780
## The functions below are required to be defined

0 commit comments

Comments
 (0)