Skip to content

Commit 144dc52

Browse files
committed
wip
1 parent 96e8946 commit 144dc52

File tree

2 files changed

+312
-19
lines changed

2 files changed

+312
-19
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
# # Single-column test in the Amazon for soil biogeochemistry diagnostics
2+
#
3+
# Runs a single column at (-60, -3) (central Amazon) with ERA5 forcing
4+
# for one month, then plots timeseries of respiration and soil biogeochemistry
5+
# variables at multiple depths.
6+
7+
import ClimaComms
8+
ClimaComms.@import_required_backends
9+
using ClimaCore
10+
import ClimaParams as CP
11+
using Dates
12+
13+
using ClimaDiagnostics
14+
using ClimaUtilities
15+
import ClimaUtilities.TimeVaryingInputs:
16+
TimeVaryingInput, LinearInterpolation, PeriodicCalendar
17+
18+
using ClimaLand
19+
using ClimaLand.Domains: Column
20+
using ClimaLand.Snow
21+
using ClimaLand.Soil
22+
using ClimaLand.Soil.Biogeochemistry
23+
using ClimaLand.Canopy
24+
import ClimaLand
25+
import ClimaLand.Parameters as LP
26+
using ClimaLand.Simulations: LandSimulation, solve!
27+
using CairoMakie
28+
29+
const FT = Float64
30+
31+
# --- Domain setup ---
32+
# Single column in the central Amazon
33+
lat = FT(-3.0)
34+
lon = FT(-60.0)
35+
zmin = FT(-15.0) # Same as global run
36+
zmax = FT(0.0)
37+
nelements = 15
38+
dz_tuple = (FT(3.0), FT(0.05)) # Stretched grid: 3m at bottom, 0.05m at top
39+
40+
domain = Column(;
41+
zlim = (zmin, zmax),
42+
nelements,
43+
dz_tuple,
44+
longlat = (lon, lat),
45+
)
46+
surface_domain = ClimaLand.Domains.obtain_surface_domain(domain)
47+
surface_space = domain.space.surface
48+
49+
# --- Time setup ---
50+
start_date = DateTime("2008-03-01")
51+
stop_date = DateTime("2008-04-01") # 1 month
52+
Δt = 450.0 # 7.5 minutes
53+
54+
# --- Parameters ---
55+
toml_dict = LP.create_toml_dict(FT)
56+
context = ClimaComms.context()
57+
58+
# --- Forcing ---
59+
atmos, radiation = ClimaLand.prescribed_forcing_era5(
60+
start_date,
61+
stop_date,
62+
surface_space,
63+
toml_dict,
64+
FT;
65+
use_lowres_forcing = true,
66+
context,
67+
)
68+
forcing = (; atmos, radiation)
69+
70+
# --- LAI ---
71+
LAI = ClimaLand.Canopy.prescribed_lai_modis(
72+
surface_space,
73+
start_date,
74+
stop_date,
75+
)
76+
77+
# --- Build model ---
78+
prognostic_land_components = (:canopy, :snow, :soil, :soilco2)
79+
80+
# Use the P model for photosynthesis
81+
photosynthesis = PModel{FT}(surface_domain, toml_dict)
82+
conductance = PModelConductance{FT}(toml_dict)
83+
soil_moisture_stress =
84+
ClimaLand.Canopy.PiecewiseMoistureStressModel{FT}(domain, toml_dict)
85+
maxLAI = FT(6.0) # Typical Amazon max LAI
86+
biomass = ClimaLand.Canopy.PrescribedBiomassModel{FT}(
87+
domain,
88+
LAI,
89+
maxLAI,
90+
toml_dict,
91+
)
92+
canopy = ClimaLand.Canopy.CanopyModel{FT}(
93+
surface_domain,
94+
(;
95+
atmos = forcing.atmos,
96+
radiation = forcing.radiation,
97+
ground = ClimaLand.PrognosticGroundConditions{FT}(),
98+
),
99+
LAI,
100+
toml_dict;
101+
prognostic_land_components,
102+
photosynthesis,
103+
conductance,
104+
soil_moisture_stress,
105+
biomass,
106+
)
107+
108+
snow = Snow.SnowModel(
109+
FT,
110+
surface_domain,
111+
forcing,
112+
toml_dict,
113+
Δt;
114+
prognostic_land_components,
115+
)
116+
117+
land = LandModel{FT}(
118+
forcing,
119+
LAI,
120+
toml_dict,
121+
domain,
122+
Δt;
123+
prognostic_land_components,
124+
canopy,
125+
snow,
126+
)
127+
128+
# --- Diagnostics ---
129+
output_vars = [
130+
"ra", # autotrophic respiration (surface, mol CO2 m-2 s-1)
131+
"hr", # heterotrophic respiration (surface, mol CO2 m-2 s-1)
132+
"soc", # soil organic carbon (depth, kg C m-3)
133+
"sco2_ppm", # soil CO2 in ppm (depth)
134+
"so2", # soil O2 fraction (depth, m3/m3)
135+
"scms", # soil CO2 microbial source (depth, kg C m-3 s-1)
136+
]
137+
138+
diags = ClimaLand.default_diagnostics(
139+
land,
140+
start_date;
141+
output_writer = ClimaDiagnostics.Writers.DictWriter(),
142+
output_vars,
143+
reduction_period = :daily,
144+
)
145+
146+
simulation = LandSimulation(
147+
start_date,
148+
stop_date,
149+
Δt,
150+
land;
151+
diagnostics = diags,
152+
)
153+
154+
@info "Running Amazon single-column simulation..."
155+
@time solve!(simulation)
156+
157+
# --- Debug: print key state variables ---
158+
Y = simulation._integrator.u
159+
p = simulation._integrator.p
160+
z_coords = parent(domain.fields.z)[:]
161+
@info "Final state (top → bottom):"
162+
@info " z: $(round.(z_coords[end:-1:1]; digits=2))"
163+
@info " CO2 (kgC/m³ soil): $(round.(parent(Y.soilco2.CO2)[end:-1:1]; sigdigits=4))"
164+
@info " O2_f: $(round.(parent(Y.soilco2.O2_f)[end:-1:1]; sigdigits=4))"
165+
@info " SOC: $(round.(parent(Y.soilco2.SOC)[end:-1:1]; sigdigits=4))"
166+
@info " θ_a: $(round.(parent(p.soilco2.θ_a)[end:-1:1]; sigdigits=4))"
167+
@info " θ_eff: $(round.(parent(p.soilco2.θ_eff)[end:-1:1]; sigdigits=4))"
168+
@info " CO2_air_eq: $(round.(parent(p.soilco2.CO2_air_eq)[end:-1:1]; sigdigits=4))"
169+
@info " D (diffusivity): $(round.(parent(p.soilco2.D)[end:-1:1]; sigdigits=4))"
170+
@info " Sm (source): $(round.(parent(p.soilco2.Sm)[end:-1:1]; sigdigits=4))"
171+
@info " top_bc: $(round.(parent(p.soilco2.top_bc); sigdigits=4))"
172+
@info " θ_l: $(round.(parent(Y.soil.ϑ_l)[end:-1:1]; sigdigits=4))"
173+
# Expected atmos CO2_air_eq: c_co2 * P * M_C / (R * T) ≈ 4.2e-4 * 1e5 * 0.012 / (8.314 * 300) ≈ 2e-4 kgC/m³
174+
@info " c_co2 (atm): $(parent(p.drivers.c_co2))"
175+
@info " P_sfc: $(parent(p.drivers.P))"
176+
177+
# --- Plotting ---
178+
writer = first(diags).output_writer
179+
180+
# Get depth coordinates from the model
181+
z = parent(domain.fields.z)[:] # z coordinates of cell centers
182+
nlayers = length(z)
183+
184+
# Select a few layers to plot for depth-resolved variables
185+
# Layer numbering: 1 = bottom, nlayers = top
186+
layer_top = nlayers # near surface
187+
layer_mid1 = nlayers - 2 # ~shallow
188+
layer_mid2 = nlayers - 5 # mid-depth
189+
layer_deep = max(1, nlayers - 10) # deeper
190+
selected_layers = [layer_top, layer_mid1, layer_mid2, layer_deep]
191+
layer_labels = ["z = $(round(z[l]; digits=2)) m" for l in selected_layers]
192+
193+
# Helper to extract timeseries
194+
# DictWriter keys use the format "{short_name}_1d_average" for daily diagnostics
195+
function get_ts(writer, varname; layer = nothing)
196+
key = "$(varname)_1d_average"
197+
return ClimaLand.Diagnostics.diagnostic_as_vectors(
198+
writer,
199+
key;
200+
layer,
201+
)
202+
end
203+
204+
# Convert ITime vector to days since start
205+
function time_to_days(times)
206+
t0 = times[1]
207+
return [Float64((t - t0).counter) / 86400.0 for t in times]
208+
end
209+
210+
# Create figure
211+
fig = Figure(size = (1400, 1800))
212+
213+
# Unit conversion: mol CO2 m-2 s-1 → gC m-2 d-1
214+
# 1 mol CO2 = 12.011 g C, 1 day = 86400 s
215+
const mol_co2_to_gC_per_day = 12.011 * 86400.0
216+
217+
# --- Panel 1: Autotrophic Respiration (surface) ---
218+
ax1 = Axis(
219+
fig[1, 1],
220+
xlabel = "Days",
221+
ylabel = "Ra (gC m⁻² d⁻¹)",
222+
title = "Autotrophic Respiration",
223+
)
224+
times_ra, vals_ra = get_ts(writer, "ra")
225+
lines!(ax1, time_to_days(times_ra), vals_ra .* mol_co2_to_gC_per_day)
226+
227+
# --- Panel 2: Heterotrophic Respiration (surface) ---
228+
ax2 = Axis(
229+
fig[1, 2],
230+
xlabel = "Days",
231+
ylabel = "HR (gC m⁻² d⁻¹)",
232+
title = "Heterotrophic Respiration",
233+
)
234+
times_hr, vals_hr = get_ts(writer, "hr")
235+
lines!(ax2, time_to_days(times_hr), vals_hr .* mol_co2_to_gC_per_day)
236+
237+
# --- Panel 3: SOC at different depths ---
238+
ax3 = Axis(
239+
fig[2, 1],
240+
xlabel = "Days",
241+
ylabel = "SOC (kg C m⁻³)",
242+
title = "Soil Organic Carbon",
243+
)
244+
for (i, l) in enumerate(selected_layers)
245+
times_soc, vals_soc = get_ts(writer, "soc"; layer = l)
246+
lines!(ax3, time_to_days(times_soc), vals_soc; label = layer_labels[i])
247+
end
248+
axislegend(ax3; position = :rt)
249+
250+
# --- Panel 4: Soil CO2 in ppm at different depths ---
251+
ax4 = Axis(
252+
fig[2, 2],
253+
xlabel = "Days",
254+
ylabel = "Soil CO₂ (ppm)",
255+
title = "Soil CO₂",
256+
)
257+
for (i, l) in enumerate(selected_layers)
258+
times_sco2, vals_sco2 = get_ts(writer, "sco2_ppm"; layer = l)
259+
lines!(ax4, time_to_days(times_sco2), vals_sco2; label = layer_labels[i])
260+
end
261+
axislegend(ax4; position = :rt)
262+
263+
# --- Panel 5: Soil O2 at different depths ---
264+
ax5 = Axis(
265+
fig[3, 1],
266+
xlabel = "Days",
267+
ylabel = "O₂ fraction (m³/m³)",
268+
title = "Soil O₂ Volumetric Fraction",
269+
)
270+
for (i, l) in enumerate(selected_layers)
271+
times_so2, vals_so2 = get_ts(writer, "so2"; layer = l)
272+
lines!(ax5, time_to_days(times_so2), vals_so2; label = layer_labels[i])
273+
end
274+
axislegend(ax5; position = :rb)
275+
276+
# --- Panel 6: Microbial source at different depths ---
277+
ax6 = Axis(
278+
fig[3, 2],
279+
xlabel = "Days",
280+
ylabel = "Sm (mgC m⁻³ s⁻¹)",
281+
title = "Microbial CO₂ Source",
282+
)
283+
for (i, l) in enumerate(selected_layers)
284+
times_sm, vals_sm = get_ts(writer, "scms"; layer = l)
285+
lines!(ax6, time_to_days(times_sm), vals_sm .* 1e6; label = layer_labels[i])
286+
end
287+
axislegend(ax6; position = :rt)
288+
289+
savedir = joinpath(
290+
pkgdir(ClimaLand),
291+
"experiments/integrated/amazon_column_soilco2_out",
292+
)
293+
mkpath(savedir)
294+
save(joinpath(savedir, "amazon_soilco2_timeseries.png"), fig)
295+
@info "Saved plot to $savedir"
296+
display(fig)

0 commit comments

Comments
 (0)