Skip to content

Commit b1cbb7a

Browse files
authored
off diagonal term in jacobian (#952)
1 parent b24f868 commit b1cbb7a

File tree

10 files changed

+231
-19
lines changed

10 files changed

+231
-19
lines changed

.buildkite/pipeline.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ steps:
9696
- label: "Soilbiogeochem"
9797
command: "julia --color=yes --project=.buildkite experiments/standalone/Biogeochemistry/experiment.jl"
9898

99+
- label: "Implicit stepper full soil; saturated soil"
100+
command: "julia --color=yes --project=.buildkite experiments/standalone/Soil/energy_hydrology_saturated.jl"
101+
99102
- label: "Water conservation"
100103
command: "julia --color=yes --project=.buildkite experiments/standalone/Soil/water_conservation.jl"
101104
artifact_paths: "experiments/standalone/Soil/water_conservation/output_active/water_conservation*png"

docs/tutorials/standalone/Soil/freezing_front.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ ode_algo = CTS.IMEXAlgorithm(
221221
timestepper,
222222
CTS.NewtonsMethod(
223223
max_iters = 3,
224-
update_j = CTS.UpdateEvery(CTS.NewTimeStep),
224+
update_j = CTS.UpdateEvery(CTS.NewNewtonIteration),
225225
),
226226
);
227227

experiments/integrated/fluxnet/US-Var/US-Var_simulation.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ h_stem = FT(0) # m
2020
t0 = Float64(0)
2121

2222
# Time step size:
23-
dt = Float64(900)
23+
dt = Float64(1800)

experiments/long_runs/land.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ end
380380
function setup_and_solve_problem(; greet = false)
381381

382382
t0 = 0.0
383-
tf = 60 * 60.0 * 24 * 365
383+
tf = 60 * 60.0 * 24 * 365 * 2
384384
Δt = 450.0
385385
nelements = (101, 15)
386386
if greet
@@ -413,7 +413,12 @@ short_names = ["gpp", "swc", "et", "ct"]
413413
mktempdir(root_path) do tmpdir
414414
for short_name in short_names
415415
var = get(simdir; short_name)
416-
times = [ClimaAnalysis.times(var)[1], ClimaAnalysis.times(var)[end]]
416+
N = length(ClimaAnalysis.times(var))
417+
times = [
418+
ClimaAnalysis.times(var)[1],
419+
ClimaAnalysis.times(var)[div(N, 2, RoundNearest)],
420+
ClimaAnalysis.times(var)[N],
421+
]
417422
for t in times
418423
fig = CairoMakie.Figure(size = (600, 400))
419424
kwargs = ClimaAnalysis.has_altitude(var) ? Dict(:z => 1) : Dict()

experiments/long_runs/snowy_land.jl

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,12 @@ short_names = ["gpp", "swc", "et", "ct", "swe", "si"]
482482
mktempdir(root_path) do tmpdir
483483
for short_name in short_names
484484
var = get(simdir; short_name)
485-
times = [ClimaAnalysis.times(var)[end]]
485+
N = length(ClimaAnalysis.times(var))
486+
times = [
487+
ClimaAnalysis.times(var)[1],
488+
ClimaAnalysis.times(var)[div(N, 2, RoundNearest)],
489+
ClimaAnalysis.times(var)[N],
490+
]
486491
for t in times
487492
fig = CairoMakie.Figure(size = (600, 400))
488493
kwargs = ClimaAnalysis.has_altitude(var) ? Dict(:z => 1) : Dict()
@@ -500,13 +505,7 @@ mktempdir(root_path) do tmpdir
500505
end
501506
figures = readdir(tmpdir, join = true)
502507
pdfunite() do unite
503-
run(
504-
Cmd([
505-
unite,
506-
figures...,
507-
joinpath(root_path, "last_year_figures.pdf"),
508-
]),
509-
)
508+
run(Cmd([unite, figures..., joinpath(root_path, "figures.pdf")]))
510509
end
511510
end
512511

experiments/long_runs/soil.jl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ end
221221
function setup_and_solve_problem(; greet = false)
222222

223223
t0 = 0.0
224-
tf = 60 * 60.0 * 24 * 365
224+
tf = 60 * 60.0 * 24 * 365 * 2
225225
Δt = 450.0
226226
nelements = (101, 15)
227227
if greet
@@ -254,7 +254,12 @@ short_names = ["swc", "si", "sie"]
254254
mktempdir(root_path) do tmpdir
255255
for short_name in short_names
256256
var = get(simdir; short_name)
257-
times = [ClimaAnalysis.times(var)[1], ClimaAnalysis.times(var)[end]]
257+
N = length(ClimaAnalysis.times(var))
258+
times = [
259+
ClimaAnalysis.times(var)[1],
260+
ClimaAnalysis.times(var)[div(N, 2, RoundNearest)],
261+
ClimaAnalysis.times(var)[N],
262+
]
258263
for t in times
259264
var = get(simdir; short_name)
260265
fig = CairoMakie.Figure(size = (600, 400))
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import SciMLBase
2+
using Statistics
3+
import ClimaTimeSteppers as CTS
4+
using ClimaCore
5+
import ClimaParams as CP
6+
using ClimaLand
7+
using ClimaLand.Domains: Column
8+
using ClimaLand.Soil
9+
import ClimaLand
10+
import ClimaLand.Parameters as LP
11+
12+
FT = Float32;
13+
earth_param_set = LP.LandParameters(FT)
14+
15+
vg_n = FT(2.9)
16+
vg_α = FT(6)
17+
K_sat = FT(4.42 / 3600 / 100) # m/s
18+
hcm = vanGenuchten{FT}(; α = vg_α, n = vg_n)
19+
ν = FT(0.43)
20+
θ_r = FT(0.045)
21+
S_s = FT(1e-3)
22+
ν_ss_om = FT(0.0)
23+
ν_ss_quartz = FT(1.0)
24+
ν_ss_gravel = FT(0.0)
25+
26+
params = ClimaLand.Soil.EnergyHydrologyParameters(
27+
FT;
28+
ν,
29+
ν_ss_om,
30+
ν_ss_quartz,
31+
ν_ss_gravel,
32+
hydrology_cm = hcm,
33+
K_sat,
34+
S_s,
35+
θ_r,
36+
earth_param_set,
37+
);
38+
39+
surface_water_flux = WaterFluxBC((p, t) -> -K_sat / 10)
40+
bottom_water_flux = WaterFluxBC((p, t) -> 0.0);
41+
42+
surface_heat_flux = HeatFluxBC((p, t) -> 0.0)
43+
bottom_heat_flux = HeatFluxBC((p, t) -> 0.0);
44+
45+
boundary_fluxes = (;
46+
top = WaterHeatBC(; water = surface_water_flux, heat = surface_heat_flux),
47+
bottom = WaterHeatBC(; water = bottom_water_flux, heat = bottom_heat_flux),
48+
);
49+
50+
# Domain - single column
51+
zmax = FT(0)
52+
zmin = FT(-0.5)
53+
nelems = 25
54+
soil_domain = Column(; zlim = (zmin, zmax), nelements = nelems)
55+
z = ClimaCore.Fields.coordinate_field(soil_domain.space.subsurface).z;
56+
57+
# Soil model, and create the prognostic vector Y and cache p:
58+
soil = Soil.EnergyHydrology{FT}(;
59+
parameters = params,
60+
domain = soil_domain,
61+
boundary_conditions = boundary_fluxes,
62+
sources = (),
63+
)
64+
Y, p, cds = initialize(soil);
65+
66+
set_initial_cache! = make_set_initial_cache(soil)
67+
exp_tendency! = make_exp_tendency(soil)
68+
imp_tendency! = make_imp_tendency(soil);
69+
jacobian! = ClimaLand.make_jacobian(soil);
70+
jac_kwargs = (; jac_prototype = ImplicitEquationJacobian(Y), Wfact = jacobian!);
71+
72+
function hydrostatic_equilibrium(z, z_interface, params)
73+
(; ν, S_s, hydrology_cm) = params
74+
(; α, n, m) = hydrology_cm
75+
if z < z_interface
76+
return -S_s * (z - z_interface) + ν
77+
else
78+
return ν * (1 +* (z - z_interface))^n)^(-m)
79+
end
80+
end
81+
function init_soil!(Y, z, params)
82+
FT = eltype(Y.soil.ϑ_l)
83+
Y.soil.ϑ_l .= hydrostatic_equilibrium.(z, FT(-0.45), params)
84+
Y.soil.θ_i .= 0
85+
T = FT(296.15)
86+
ρc_s = @. Soil.volumetric_heat_capacity(
87+
Y.soil.ϑ_l,
88+
FT(0),
89+
params.ρc_ds,
90+
params.earth_param_set,
91+
)
92+
Y.soil.ρe_int =
93+
Soil.volumetric_internal_energy.(FT(0), ρc_s, T, params.earth_param_set)
94+
end
95+
96+
# Timestepping:
97+
t0 = Float64(0)
98+
tf = Float64(24 * 3600)
99+
timestepper = CTS.ARS111();
100+
ode_algo = CTS.IMEXAlgorithm(
101+
timestepper,
102+
CTS.NewtonsMethod(
103+
max_iters = 6,
104+
update_j = CTS.UpdateEvery(CTS.NewNewtonIteration),
105+
),
106+
);
107+
108+
# Define the problem and callbacks:
109+
prob = SciMLBase.ODEProblem(
110+
CTS.ClimaODEFunction(
111+
T_exp! = exp_tendency!,
112+
T_imp! = SciMLBase.ODEFunction(imp_tendency!; jac_kwargs...),
113+
dss! = ClimaLand.dss!,
114+
),
115+
Y,
116+
(t0, tf),
117+
p,
118+
);
119+
# Solve at small dt
120+
init_soil!(Y, z, soil.parameters);
121+
set_initial_cache!(p, Y, t0);
122+
sol_dt_small = SciMLBase.solve(prob, ode_algo; dt = 100.0);
123+
init_soil!(Y, z, soil.parameters);
124+
set_initial_cache!(p, Y, t0);
125+
sol_dt_large = SciMLBase.solve(prob, ode_algo; dt = 900.0);
126+
@assert sum(isnan.(sol_dt_large.u[end])) == 0
127+
128+
norm(x) = sqrt(mean(parent(x .^ 2)))
129+
rmse(x, y) = sqrt(mean(parent((x .- y) .^ 2)))
130+
@assert rmse(sol_dt_small[end].soil.ϑ_l, sol_dt_large[end].soil.ϑ_l) /
131+
norm(sol_dt_small[end].soil.ϑ_l) < sqrt(eps(FT))
132+
@assert rmse(sol_dt_small[end].soil.ρe_int, sol_dt_large[end].soil.ρe_int) /
133+
norm(sol_dt_small[end].soil.ρe_int) < sqrt(eps(FT))

src/shared_utilities/implicit_timestepping.jl

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,21 @@ function ImplicitEquationJacobian(Y::ClimaCore.Fields.FieldVector)
148148
get_j_field(axes(MatrixFields.get_field(Y, var)), FT),
149149
available_implicit_vars,
150150
)
151+
152+
# We include some terms ∂T_x/∂y where x ≠ y
153+
# These are the off-diagonal terms in the Jacobian matrix
154+
# Here, we take the convention that each pair has order (T_x, y) to produce ∂T_x/∂y as above
155+
off_diagonal_pairs = ((@name(soil.ρe_int), @name(soil.ϑ_l)),)
156+
available_off_diagonal_pairs = MatrixFields.unrolled_filter(
157+
pair -> all(is_in_Y.(pair)),
158+
off_diagonal_pairs,
159+
)
160+
implicit_off_diagonals = MatrixFields.unrolled_map(
161+
pair ->
162+
(pair[1], pair[2]) =>
163+
get_j_field(axes(MatrixFields.get_field(Y, pair[1])), FT),
164+
available_off_diagonal_pairs,
165+
)
151166
# For explicitly-stepped variables, use the negative identity matrix
152167
# Note: We have to use FT(-1) * I instead of -I because inv(-1) == -1.0,
153168
# which means that multiplying inv(-1) by a Float32 will yield a Float64.
@@ -156,12 +171,21 @@ function ImplicitEquationJacobian(Y::ClimaCore.Fields.FieldVector)
156171
available_explicit_vars,
157172
)
158173

174+
matrix = MatrixFields.FieldMatrix(
175+
implicit_blocks...,
176+
implicit_off_diagonals...,
177+
explicit_blocks...,
178+
)
159179

160-
161-
matrix = MatrixFields.FieldMatrix(implicit_blocks..., explicit_blocks...)
162-
163-
# Set up block diagonal solver for block Jacobian
164-
alg = MatrixFields.BlockDiagonalSolve()
180+
# Choose algorithm based on whether off-diagonal blocks are present
181+
if is_in_Y(@name(soil.ρe_int)) && is_in_Y(@name(soil.ϑ_l))
182+
# Set up lower triangular solver for block Jacobian with off-diagonal blocks
183+
# Specify which variable to compute ∂T_x/∂x for first
184+
alg = MatrixFields.BlockLowerTriangularSolve(@name(soil.ϑ_l))
185+
else
186+
# Set up block diagonal solver for block Jacobian with no off-diagonal blocks
187+
alg = MatrixFields.BlockDiagonalSolve()
188+
end
165189
solver = MatrixFields.FieldMatrixSolver(alg, matrix, Y)
166190

167191
return ImplicitEquationJacobian(matrix, solver)

src/standalone/Soil/energy_hydrology.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ function ClimaLand.make_compute_jacobian(model::EnergyHydrology{FT}) where {FT}
322322
# The derivative of the residual with respect to the prognostic variable
323323
∂ϑres∂ϑ = matrix[@name(soil.ϑ_l), @name(soil.ϑ_l)]
324324
∂ρeres∂ρe = matrix[@name(soil.ρe_int), @name(soil.ρe_int)]
325+
∂ρeres∂ϑ = matrix[@name(soil.ρe_int), @name(soil.ϑ_l)]
325326
# If the top BC is a `MoistureStateBC`, add the term from the top BC
326327
# flux before applying divergence
327328
if haskey(p.soil, :dfluxBCdY)
@@ -373,6 +374,26 @@ function ClimaLand.make_compute_jacobian(model::EnergyHydrology{FT}) where {FT}
373374
)
374375
) - (I,)
375376
end
377+
@. ∂ρeres∂ϑ =
378+
-dtγ * (
379+
divf2c_matrix() MatrixFields.DiagonalMatrixRow(
380+
-interpc2f_op(
381+
volumetric_internal_energy_liq(
382+
p.soil.T,
383+
model.parameters.earth_param_set,
384+
) * p.soil.K,
385+
),
386+
) gradc2f_matrix() MatrixFields.DiagonalMatrixRow(
387+
ClimaLand.Soil.dψdϑ(
388+
hydrology_cm,
389+
Y.soil.ϑ_l,
390+
ν - Y.soil.θ_i, #ν_eff
391+
θ_r,
392+
S_s,
393+
),
394+
)
395+
) - (I,)
396+
376397
@. ∂ρeres∂ρe =
377398
-dtγ * (
378399
divf2c_matrix()

test/shared_utilities/implicit_timestepping/energy_hydrology_model.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,5 +145,27 @@ for FT in (Float32, Float64)
145145
# Check the main diagonal, entry corresponding to top of column
146146
@test Array(parent(jac_ρe.entries.:2))[end] .≈
147147
dtγ * (-κ_ic / dz^2 * dTdρ_ic) - I
148+
149+
# Off diagonal blocks, ∂T_ρe_int/∂ϑ_l
150+
jac_ρeϑ = jacobian.matrix[
151+
MatrixFields.@name(soil.ρe_int),
152+
MatrixFields.@name(soil.ϑ_l)
153+
]
154+
155+
ρe_liq_ic = ClimaLand.Soil.volumetric_internal_energy_liq(
156+
T,
157+
params.earth_param_set,
158+
)
159+
# Check the main diagonal, entry corresponding to bottom of column
160+
@test Array(parent(jac_ρeϑ.entries.:2))[1] .≈
161+
dtγ * (-ρe_liq_ic * K_ic / dz^2 * dψdϑ_ic) - I
162+
# Check the main diagonal, entries corresponding to interior of domain
163+
@test all(
164+
Array(parent(jac_ρeϑ.entries.:2))[2:(end - 1)] .≈
165+
dtγ * (-2 * ρe_liq_ic * K_ic / dz^2 * dψdϑ_ic) - I,
166+
)
167+
# Check the main diagonal, entry corresponding to top of column
168+
@test Array(parent(jac_ρeϑ.entries.:2))[end] .≈
169+
dtγ * (-ρe_liq_ic * K_ic / dz^2 * dψdϑ_ic) - I
148170
end
149171
end

0 commit comments

Comments
 (0)