Skip to content

Commit 96e8946

Browse files
committed
respiration updates
1 parent a913587 commit 96e8946

File tree

12 files changed

+471
-94
lines changed

12 files changed

+471
-94
lines changed

src/ClimaLand.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ import .Soil:
418418
sublimation_source,
419419
compute_liquid_influx,
420420
compute_infiltration_energy_flux
421-
import .Soil.Biogeochemistry: soil_temperature, soil_moisture
421+
import .Soil.Biogeochemistry: soil_temperature, soil_moisture, soil_ice
422422
include("standalone/Snow/Snow.jl")
423423
using .Snow
424424
import .Snow: snow_boundary_fluxes!, maximum_snow_cover_fraction!

src/diagnostics/default_diagnostics.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ function get_possible_diagnostics(model::EnergyHydrology)
480480
end
481481

482482
function get_possible_diagnostics(model::SoilCO2Model)
483-
return ["sco2", "hr", "scd", "scms", "so2", "soc", "soc_int"]
483+
return ["sco2", "hr", "scd", "scms", "so2", "soc", "soc_int", "sco2_ppm"]
484484
end
485485
function get_possible_diagnostics(model::CanopyModel)
486486
diagnostics = [
@@ -617,7 +617,7 @@ function get_short_diagnostics(model::EnergyHydrology)
617617
return ["swc", "si", "sie", "tsoil", "et"]
618618
end
619619
function get_short_diagnostics(model::SoilCO2Model)
620-
return ["sco2", "hr", "soc", "soc_int"]
620+
return ["sco2", "hr", "soc", "soc_int", "sco2_ppm"]
621621
end
622622
function get_short_diagnostics(model::CanopyModel)
623623
diagnostics = ["gpp", "ct", "lai", "trans", "er", "sif"]

src/diagnostics/define_diagnostics.jl

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -979,8 +979,8 @@ function define_diagnostics!(land_model, possible_diags)
979979
short_name = "sco2",
980980
long_name = "Soil CO2",
981981
standard_name = "soil_co2",
982-
units = "kg C m^3",
983-
comments = "Concentration of CO2 in the porous air space of the soil. (depth resolved)",
982+
units = "kg C m^-3",
983+
comments = "Soil CO2 carbon mass per soil volume (air + dissolved) (depth resolved).",
984984
compute! = (out, Y, p, t) -> compute_soilco2!(out, Y, p, t, land_model),
985985
)
986986

@@ -1002,7 +1002,7 @@ function define_diagnostics!(land_model, possible_diags)
10021002
long_name = "Soil Organic Carbon",
10031003
standard_name = "soil_organic_carbon",
10041004
units = "kg C m^-3",
1005-
comments = "Mass concentration of soil organic carbon. (depth resolved)",
1005+
comments = "Soil organic carbon mass per soil volume (depth resolved).",
10061006
compute! = (out, Y, p, t) -> compute_soc!(out, Y, p, t, land_model),
10071007
)
10081008
conditional_add_diagnostic_variable!(
@@ -1016,6 +1016,16 @@ function define_diagnostics!(land_model, possible_diags)
10161016
compute_integrated_soc!(out, Y, p, t, land_model),
10171017
)
10181018

1019+
# Soil CO2 in ppm (for NEON comparison)
1020+
add_diagnostic_variable!(
1021+
short_name = "sco2_ppm",
1022+
long_name = "Soil Pore Air CO2 Concentration",
1023+
standard_name = "soil_pore_air_co2_concentration",
1024+
units = "ppm",
1025+
comments = "CO2 concentration in soil pore air space, in parts per million by volume. Computed from air-equivalent CO2 concentration using ideal gas law. (depth resolved)",
1026+
compute! = (out, Y, p, t) -> compute_soilco2_ppm!(out, Y, p, t, land_model),
1027+
)
1028+
10191029
# Soil water content
10201030
conditional_add_diagnostic_variable!(
10211031
possible_diags;

src/diagnostics/land_compute_methods.jl

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,34 @@ end # Convert from kg C to mol CO2.
621621
# To convert from kg C to mol CO2, we need to multiply by:
622622
# [3.664 kg CO2/ kg C] x [10^3 g CO2/ kg CO2] x [1 mol CO2/44.009 g CO2] = 83.26 mol CO2/kg C
623623

624+
# Soil CO2 in ppm (for comparison with NEON observations)
625+
# Converts air-equivalent CO2 concentration to ppm using ideal gas law:
626+
# ppm = (n_CO2 / n_air) × 10^6 = CO2_air_eq * R * T / (M_C * P) × 10^6
627+
function compute_soilco2_ppm!(
628+
out,
629+
Y,
630+
p,
631+
t,
632+
land_model::Union{SoilCanopyModel{FT}, LandModel{FT}},
633+
) where {FT}
634+
params = land_model.soilco2.parameters
635+
M_C = FT(params.M_C)
636+
R = FT(LP.gas_constant(params.earth_param_set))
637+
638+
CO2_air_eq = p.soilco2.CO2_air_eq # kg C / m³ air-equivalent
639+
T_soil = p.soilco2.T # K
640+
P_sfc = p.drivers.P # Pa
641+
642+
if isnothing(out)
643+
out = zeros(axes(CO2_air_eq))
644+
fill!(field_values(out), NaN)
645+
@. out = CO2_air_eq * R * T_soil / (M_C * P_sfc) * FT(1e6)
646+
return out
647+
else
648+
@. out = CO2_air_eq * R * T_soil / (M_C * P_sfc) * FT(1e6)
649+
end
650+
end
651+
624652
## Other ##
625653
@diagnostic_compute "sw_albedo" Union{SoilCanopyModel, LandModel} p.α_sfc
626654
@diagnostic_compute "lw_up" Union{SoilCanopyModel, LandModel} p.LW_u
@@ -721,9 +749,18 @@ function compute_total_runoff!(
721749
out = zeros(soil.domain.space.surface) # Allocates
722750
fill!(field_values(out), NaN) # fill with NaNs, even over the ocean
723751
end
724-
out .=
725-
Runoff.get_surface_runoff(soil.boundary_conditions.top.runoff, Y, p) .+
726-
Runoff.get_subsurface_runoff(soil.boundary_conditions.top.runoff, Y, p)
752+
runoff = soil.boundary_conditions.top.runoff
753+
surface = Runoff.get_surface_runoff(runoff, Y, p)
754+
subsurface = Runoff.get_subsurface_runoff(runoff, Y, p)
755+
if !isnothing(surface) && !isnothing(subsurface)
756+
out .= surface .+ subsurface
757+
elseif !isnothing(surface)
758+
out .= surface
759+
elseif !isnothing(subsurface)
760+
out .= subsurface
761+
else
762+
fill!(out, 0)
763+
end
727764
return out
728765
end
729766

src/integrated/soil_energy_hydrology_biogeochemistry.jl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,15 @@ function soil_moisture(driver::PrognosticMet, p, Y, t, z)
156156
return p.soil.θ_l
157157
end
158158

159+
"""
160+
soil_ice(driver::PrognosticMet, p, Y, t, z)
161+
162+
Returns the prognostic ice content from coupled soil model.
163+
"""
164+
function soil_ice(driver::PrognosticMet, p, Y, t, z)
165+
return Y.soil.θ_i
166+
end
167+
159168

160169
function ClimaLand.get_drivers(model::LandSoilBiogeochemistry)
161170
bc = model.soil.boundary_conditions.top

src/simulations/initial_conditions.jl

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,61 @@ function set_soil_initial_conditions!(
9797
return nothing
9898
end
9999

100+
"""
101+
set_soilco2_initial_conditions!(Y, p, land)
102+
103+
Initialize soil CO2 state to be consistent with atmospheric CO2 and soil state.
104+
CO2 is stored as carbon mass per soil volume (kg C m⁻³ soil).
105+
"""
106+
function set_soilco2_initial_conditions!(Y, p, land)
107+
FT = eltype(Y.soilco2.CO2)
108+
soil = land.soil
109+
soilco2 = land.soilco2
110+
params = soilco2.parameters
111+
112+
θ_l = Y.soil.ϑ_l
113+
θ_i = Y.soil.θ_i
114+
θ_w = θ_l .+ θ_i
115+
ν = soil.parameters.ν
116+
117+
ρc_s = ClimaLand.Soil.volumetric_heat_capacity.(
118+
θ_l,
119+
θ_i,
120+
soil.parameters.ρc_ds,
121+
soil.parameters.earth_param_set,
122+
)
123+
T_soil = ClimaLand.Soil.temperature_from_ρe_int.(
124+
Y.soil.ρe_int,
125+
θ_i,
126+
ρc_s,
127+
soil.parameters.earth_param_set,
128+
)
129+
130+
θ_a = ClimaLand.Soil.Biogeochemistry.volumetric_air_content.(θ_w, ν)
131+
R = FT(ClimaLand.Parameters.gas_constant(params.earth_param_set))
132+
M_C = FT(params.M_C)
133+
134+
K_H = @. ClimaLand.Soil.Biogeochemistry.henry_constant(
135+
params.K_H_co2_298,
136+
params.dln_K_H_co2_dT,
137+
T_soil,
138+
)
139+
β = @. ClimaLand.Soil.Biogeochemistry.beta_gas(K_H, R, T_soil)
140+
θ_eff = @. ClimaLand.Soil.Biogeochemistry.effective_porosity(θ_a, θ_l, β)
141+
142+
@. Y.soilco2.CO2 = θ_eff * p.drivers.c_co2 * p.drivers.P * M_C / (R * T_soil)
143+
Y.soilco2.O2_f .= FT(0.21)
144+
145+
# SOC: exponential decay with depth (same profile as DK-Sor)
146+
# SOC(z) = SOC_bot + (SOC_top - SOC_bot) × exp(z / τ_soc)
147+
SOC_top = FT(15.0) # kgC/m³ at surface
148+
SOC_bot = FT(0.5) # kgC/m³ at depth
149+
τ_soc = FT(1.0 / log(SOC_top / SOC_bot)) # ~0.294 m
150+
z = ClimaCore.Fields.coordinate_field(axes(Y.soilco2.SOC)).z
151+
@. Y.soilco2.SOC = SOC_bot + (SOC_top - SOC_bot) * exp(z / τ_soc)
152+
return nothing
153+
end
154+
100155
"""
101156
clip_to_bounds(
102157
T::FT,
@@ -256,12 +311,6 @@ function make_set_initial_state_from_file(
256311
if atmos isa ClimaLand.PrescribedAtmosphere
257312
evaluate!(p.drivers.T, atmos.T, t0)
258313
end
259-
# SoilCO2 IC
260-
if !isnothing(land.soilco2)
261-
Y.soilco2.CO2 .= FT(0.000412) # set to atmospheric co2, mol co2 per mol air
262-
Y.soilco2.O2_f .= FT(0.21) # atmospheric O2 volumetric fraction
263-
Y.soilco2.SOC .= FT(5.0) # default SOC concentration (kg C/m³)
264-
end
265314
# Soil IC
266315
if enforce_constraints
267316
# get only the values over land
@@ -280,6 +329,11 @@ function make_set_initial_state_from_file(
280329
enforce_constraints,
281330
)
282331

332+
# SoilCO2 IC (requires soil state)
333+
if !isnothing(land.soilco2)
334+
set_soilco2_initial_conditions!(Y, p, land)
335+
end
336+
283337
# Snow IC
284338
# Use soil temperature at top to set IC
285339
soil = land.soil
@@ -396,10 +450,6 @@ function make_set_initial_state_from_file(
396450
evaluate!(p.drivers.T, atmos.T, t0)
397451
end
398452

399-
Y.soilco2.CO2 .= FT(0.000412) # set to atmospheric co2, mol co2 per mol air
400-
Y.soilco2.O2_f .= FT(0.21) # atmospheric O2 volumetric fraction
401-
Y.soilco2.SOC .= FT(5.0) # default SOC concentration (kg C/m³)
402-
403453
if enforce_constraints
404454
# get only the values over land
405455
T_atmos = Array(parent(p.drivers.T))[:]
@@ -417,6 +467,9 @@ function make_set_initial_state_from_file(
417467
enforce_constraints,
418468
)
419469

470+
# SoilCO2 IC (requires soil state)
471+
set_soilco2_initial_conditions!(Y, p, land)
472+
420473
# Canopy IC
421474
# First determine if leaf water potential is in the file. If so, use
422475
# that to set the IC; otherwise choose steady state with the soil water.
@@ -551,7 +604,12 @@ function make_set_subseasonal_initial_conditions(
551604
# Set initial conditions that aren't read in from file
552605
Y.soilco2.CO2 .= FT(0.000412) # set to atmospheric co2, mol co2 per mol air
553606
Y.soilco2.O2_f .= FT(0.21) # atmospheric O2 volumetric fraction
554-
Y.soilco2.SOC .= FT(5.0) # default SOC concentration (kg C/m³)
607+
# SOC: exponential decay with depth (same profile as DK-Sor)
608+
SOC_top = FT(15.0) # kgC/m³ at surface
609+
SOC_bot = FT(0.5) # kgC/m³ at depth
610+
τ_soc = FT(1.0 / log(SOC_top / SOC_bot)) # ~0.294 m
611+
z = ClimaCore.Fields.coordinate_field(axes(Y.soilco2.SOC)).z
612+
@. Y.soilco2.SOC = SOC_bot + (SOC_top - SOC_bot) * exp(z / τ_soc)
555613
Y.canopy.hydraulics.ϑ_l .= land.canopy.hydraulics.parameters.ν
556614

557615
# Set snow T first to use in computing snow internal energy from IC file
@@ -781,12 +839,6 @@ function make_set_initial_state_from_atmos_and_parameters(
781839
if atmos isa ClimaLand.PrescribedAtmosphere
782840
evaluate!(p.drivers.T, atmos.T, t0)
783841
end
784-
# SoilCO2 IC
785-
if !isnothing(land.soilco2)
786-
Y.soilco2.CO2 .= FT(0.000412) # set to atmospheric co2, mol co2 per mol air
787-
Y.soilco2.O2_f .= FT(0.21) # atmospheric O2 volumetric fraction
788-
Y.soilco2.SOC .= FT(5.0) # default SOC concentration (kg C/m³)
789-
end
790842
(; θ_r, ν, ρc_ds) = land.soil.parameters
791843
@. Y.soil.ϑ_l = θ_r +- θ_r) / 2
792844
Y.soil.θ_i .= FT(0.0)
@@ -805,6 +857,18 @@ function make_set_initial_state_from_atmos_and_parameters(
805857
earth_param_set,
806858
)
807859

860+
# SoilCO2 IC (requires soil state)
861+
if !isnothing(land.soilco2)
862+
Y.soilco2.CO2 .= FT(0.000412) # set to atmospheric co2, mol co2 per mol air
863+
Y.soilco2.O2_f .= FT(0.21) # atmospheric O2 volumetric fraction
864+
# SOC: exponential decay with depth (same profile as DK-Sor)
865+
SOC_top = FT(15.0) # kgC/m³ at surface
866+
SOC_bot = FT(0.5) # kgC/m³ at depth
867+
τ_soc = FT(1.0 / log(SOC_top / SOC_bot)) # ~0.294 m
868+
z = ClimaCore.Fields.coordinate_field(axes(Y.soilco2.SOC)).z
869+
@. Y.soilco2.SOC = SOC_bot + (SOC_top - SOC_bot) * exp(z / τ_soc)
870+
end
871+
808872
Y.snow.S .= FT(0)
809873
Y.snow.S_l .= FT(0)
810874
Y.snow.U .= FT(0)

0 commit comments

Comments
 (0)