Skip to content

Commit d248230

Browse files
authored
Merge pull request #4022 from CliMA/cc/test_wxquest_ic
Update WeatherModel initial conditions
2 parents 0df0c62 + 929e8fe commit d248230

File tree

5 files changed

+220
-72
lines changed

5 files changed

+220
-72
lines changed

config/default_configs/default_config.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ bubble:
172172
start_date:
173173
help: "Start date and time of the simulation. Specified as either yyyymmdd (defaults to midnight) or yyyymmdd-HHMM. Examples: [`20100101`, `20100101-0000`]"
174174
value: "20100101"
175+
era5_initial_condition_dir:
176+
help: "Directory containing ERA5 initial condition files. Filenames inferred from start_date [none (default)]. Generated with `https://github.com/CliMA/WeatherQuest`."
177+
value: ~
175178
forcing:
176179
help: "Forcing [`nothing` (default), `held_suarez`]"
177180
value: ~

config/model_configs/diagnostic_edmfx_era5_initial_condition.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ hyperdiff: ClimaHyperdiffusion
1515
divergence_damping_factor: 50.0 # large value to damp IC waves
1616
scalar_hyperdiffusion_coefficient: 0.929738
1717
vorticity_hyperdiffusion_coefficient: 0.1857
18-
1918
insolation: "timevarying"
2019
surface_setup: DefaultMoninObukhov
2120
rad: allskywithclear
@@ -30,14 +29,14 @@ edmfx_detr_model: "Generalized"
3029
edmfx_nh_pressure: true
3130
edmfx_sgs_mass_flux: true
3231
edmfx_sgs_diffusive_flux: true
33-
moist: equil
34-
cloud_model: "grid_scale"
32+
cloud_model: "quadrature_sgs"
3533
precip_model: 0M
34+
moist: equil
3635
toml: [toml/diagnostic_edmfx_era5_ic.toml]
3736
use_itime: true
3837
output_default_diagnostics: false
3938
diagnostics:
40-
- short_name: [ta, ua, wa, va, rhoa, hur, hus, clw, cli]
39+
- short_name: [ts, ta, thetaa, ha, pfull, rhoa, ua, va, wa, hur, hus, cl, clw, cli, hussfc, evspsbl]
4140
period: 60mins
42-
- short_name: [rsut, rlut, ts, pr, mslp]
41+
- short_name: [rsut, rlut, tas, pr, mslp]
4342
period: 60mins

src/initial_conditions/initial_conditions.jl

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ the artifact path.
206206
"""
207207
struct WeatherModel <: InitialCondition
208208
start_date::String
209+
era5_initial_condition_dir::Union{Nothing, String}
209210
end
210211

211212
function (initial_condition::Union{MoistFromFile, WeatherModel})(params)
@@ -377,13 +378,139 @@ function overwrite_initial_conditions!(
377378
thermo_params,
378379
)
379380
extrapolation_bc = (Intp.Periodic(), Intp.Flat(), Intp.Flat())
380-
file_path = weather_model_data_path(initial_condition.start_date)
381-
return _overwrite_initial_conditions_from_file!(
381+
382+
# Extract face coordinates and compute center midpoints
383+
# Compute target levels on CPU to avoid GPU reductions
384+
z_arr_cpu = Array(Fields.field2array(Fields.coordinate_field(Y.c).z))
385+
icol = argmin(z_arr_cpu[1, :])
386+
target_levels = z_arr_cpu[:, icol]
387+
388+
file_path = weather_model_data_path(
389+
initial_condition.start_date,
390+
target_levels,
391+
initial_condition.era5_initial_condition_dir,
392+
)
393+
394+
regridder_kwargs = (; extrapolation_bc)
395+
isfile(file_path) || error("$(file_path) is not a file")
396+
@info "Overwriting initial conditions with data from file $(file_path)"
397+
398+
center_space = Fields.axes(Y.c)
399+
face_space = Fields.axes(Y.f)
400+
401+
# Using surface pressure, air temperature and specific humidity
402+
# from the dataset, compute air pressure.
403+
p_sfc = Fields.level(
404+
SpaceVaryingInputs.SpaceVaryingInput(
405+
file_path,
406+
"p",
407+
face_space,
408+
regridder_kwargs = regridder_kwargs,
409+
),
410+
Fields.half,
411+
)
412+
ᶜT = SpaceVaryingInputs.SpaceVaryingInput(
382413
file_path,
383-
extrapolation_bc,
384-
Y,
385-
thermo_params,
414+
"t",
415+
center_space,
416+
regridder_kwargs = regridder_kwargs,
417+
)
418+
ᶜq_tot = SpaceVaryingInputs.SpaceVaryingInput(
419+
file_path,
420+
"q",
421+
center_space,
422+
regridder_kwargs = regridder_kwargs,
386423
)
424+
425+
# With the known temperature (ᶜT) and moisture (ᶜq_tot) profile,
426+
# recompute the pressure levels assuming hydrostatic balance is maintained.
427+
# Uses the ClimaCore `column_integral_indefinite!` function to solve
428+
# ∂(ln𝑝)/∂z = -g/(Rₘ(q)T), where
429+
# p is the local pressure
430+
# g is the gravitational constant
431+
# q is the specific humidity
432+
# Rₘ is the gas constant for moist air
433+
# T is the air temperature
434+
# p is then updated with the integral result, given p_sfc,
435+
# following which the thermodynamic state is constructed.
436+
ᶜ∂lnp∂z = @. -thermo_params.grav /
437+
(TD.gas_constant_air(thermo_params, TD.PhasePartition(ᶜq_tot)) * ᶜT)
438+
ᶠlnp_over_psfc = zeros(face_space)
439+
Operators.column_integral_indefinite!(ᶠlnp_over_psfc, ᶜ∂lnp∂z)
440+
ᶠp = p_sfc .* exp.(ᶠlnp_over_psfc)
441+
ᶜts = TD.PhaseEquil_pTq.(thermo_params, ᶜinterp.(ᶠp), ᶜT, ᶜq_tot)
442+
443+
# Assign prognostic variables from equilibrium moisture models
444+
Y.c.ρ .= TD.air_density.(thermo_params, ᶜts)
445+
# Velocity is first assigned on cell-centers and then interpolated onto
446+
# cell faces.
447+
vel =
448+
Geometry.UVWVector.(
449+
SpaceVaryingInputs.SpaceVaryingInput(
450+
file_path,
451+
"u",
452+
center_space,
453+
regridder_kwargs = regridder_kwargs,
454+
),
455+
SpaceVaryingInputs.SpaceVaryingInput(
456+
file_path,
457+
"v",
458+
center_space,
459+
regridder_kwargs = regridder_kwargs,
460+
),
461+
SpaceVaryingInputs.SpaceVaryingInput(
462+
file_path,
463+
"w",
464+
center_space,
465+
regridder_kwargs = regridder_kwargs,
466+
),
467+
)
468+
Y.c.uₕ .= C12.(Geometry.UVVector.(vel))
469+
Y.f.u₃ .= ᶠinterp.(C3.(Geometry.WVector.(vel)))
470+
e_kin = similar(ᶜT)
471+
e_kin .= compute_kinetic(Y.c.uₕ, Y.f.u₃)
472+
e_pot = Fields.coordinate_field(Y.c).z .* thermo_params.grav
473+
Y.c.ρe_tot .= TD.total_energy.(thermo_params, ᶜts, e_kin, e_pot) .* Y.c.ρ
474+
# Initialize prognostic EDMF 0M subdomains if present
475+
if hasproperty(Y.c, :sgsʲs)
476+
ᶜmse = TD.specific_enthalpy.(thermo_params, ᶜts) .+ e_pot
477+
for name in propertynames(Y.c.sgsʲs)
478+
s = getproperty(Y.c.sgsʲs, name)
479+
hasproperty(s, :ρa) && fill!(s.ρa, 0)
480+
hasproperty(s, :mse) && (s.mse .= ᶜmse)
481+
hasproperty(s, :q_tot) && (s.q_tot .= ᶜq_tot)
482+
end
483+
end
484+
if hasproperty(Y.c, :ρq_tot)
485+
Y.c.ρq_tot .= ᶜq_tot .* Y.c.ρ
486+
else
487+
error(
488+
"`dry` configurations are incompatible with the interpolated initial conditions.",
489+
)
490+
end
491+
if hasproperty(Y.c, :ρq_sno) && hasproperty(Y.c, :ρq_rai)
492+
Y.c.ρq_sno .=
493+
SpaceVaryingInputs.SpaceVaryingInput(
494+
file_path,
495+
"cswc",
496+
center_space,
497+
regridder_kwargs = regridder_kwargs,
498+
) .* Y.c.ρ
499+
Y.c.ρq_rai .=
500+
SpaceVaryingInputs.SpaceVaryingInput(
501+
file_path,
502+
"crwc",
503+
center_space,
504+
regridder_kwargs = regridder_kwargs,
505+
) .* Y.c.ρ
506+
end
507+
508+
if hasproperty(Y.c, :sgs⁰) && hasproperty(Y.c.sgs⁰, :ρatke)
509+
# NOTE: This is not the most consistent, but it is better than NaNs
510+
fill!(Y.c.sgs⁰.ρatke, 0)
511+
end
512+
513+
return nothing
387514
end
388515

389516
"""

src/solver/type_getters.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,10 @@ function get_initial_condition(parsed_args, atmos)
423423
parsed_args["start_date"],
424424
)
425425
elseif parsed_args["initial_condition"] == "WeatherModel"
426-
return ICs.WeatherModel(parsed_args["start_date"])
426+
return ICs.WeatherModel(
427+
parsed_args["start_date"],
428+
parsed_args["era5_initial_condition_dir"],
429+
)
427430
else
428431
error(
429432
"Unknown `initial_condition`: $(parsed_args["initial_condition"])",

src/utils/weather_model.jl

Lines changed: 77 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,60 @@ using Dates
33
import ClimaInterpolations.Interpolation1D: interpolate1d!, Linear, Flat
44
import ..parse_date
55

6+
67
"""
7-
weather_model_data_path(start_date)
8+
weather_model_data_path(start_date, target_levels, era5_initial_condition_dir])
89
910
Get the path to the weather model data for a given start date and time.
1011
If the data is not found, will attempt to generate it from raw data. If
1112
the raw data is not found, throw an error
1213
1314
# Arguments
1415
- `start_date`: Start date as string yyyymmdd or yyyymmdd-HHMM
16+
- `target_levels`: Vector of target altitude levels (in meters) to interpolate to
17+
- `era5_initial_condition_dir`: Optional directory containing preprocessed ERA5
18+
19+
- If `era5_initial_condition_dir` is provided, use
20+
`era5_init_processed_internal_YYYYMMDD_0000.nc` from that directory.
21+
- Otherwise, use the `weather_model_ic` artifact.
1522
"""
16-
function weather_model_data_path(start_date)
23+
function weather_model_data_path(
24+
start_date,
25+
target_levels,
26+
era5_initial_condition_dir = nothing,
27+
)
1728
# Parse the date using the existing parse_date function
1829
dt = parse_date(start_date)
1930

2031
# Extract components for filename generation
2132
start_date_str = Dates.format(dt, "yyyymmdd")
22-
start_time = Dates.format(dt, "HHMM")
33+
start_time = Dates.format(dt, "HHMM") # Note: this is not the same as `start_time` in the coupler!
34+
35+
# If user provided a directory with preprocessed initial conditions, use it
36+
if !isnothing(era5_initial_condition_dir)
37+
ic_data_path = joinpath(
38+
era5_initial_condition_dir,
39+
"era5_init_processed_internal_$(start_date_str)_0000.nc", # TODO: generalize for all times once Coupler supports HHMM specification
40+
)
41+
raw_data_path = joinpath(
42+
era5_initial_condition_dir,
43+
"era5_raw_$(start_date_str)_0000.nc",
44+
)
45+
if !isfile(ic_data_path)
46+
if !isfile(raw_data_path)
47+
error(
48+
"Neither preprocessed nor raw initial condition file exist in $(era5_initial_condition_dir). Please run `python get_initial_conditions.py` in the WeatherQuest repository to download the data.",
49+
)
50+
end
51+
@info "Interpolating raw weather model data onto z-levels from user-provided directory"
52+
to_z_levels(raw_data_path, ic_data_path, target_levels, Float32)
53+
else
54+
@info "Using existing interpolated IC file: $ic_data_path"
55+
end
56+
return ic_data_path
57+
end
58+
59+
# Otherwise, use artifact-based paths and generate if needed
2360
ic_data_path = joinpath(
2461
@clima_artifact("weather_model_ic"),
2562
"init",
@@ -31,23 +68,6 @@ function weather_model_data_path(start_date)
3168
"era5_raw_$(start_date_str)_$(start_time).nc",
3269
)
3370

34-
if !isfile(ic_data_path)
35-
@info "Initial condition file $ic_data_path does not exist. Attempting to generate it now..."
36-
if !isfile(raw_data_path)
37-
day = Dates.format(dt, "yyyy-mm-dd")
38-
time = Dates.format(dt, "HH:MM")
39-
error(
40-
"Source file $(raw_data_path) does not exist. Please run `python get_initial_conditions.py --output-dir $(@clima_artifact("weather_model_ic"))/raw --date $(day) --time $(time)` to download the data.",
41-
)
42-
end
43-
# get target levels - TODO: make this more flexible
44-
target_level_path =
45-
joinpath(@clima_artifact("weather_model_ic"), "target_levels.txt")
46-
target_levels = parse.(Float32, readlines(target_level_path))
47-
48-
@info "Interpolating raw weather model data onto z-levels"
49-
to_z_levels(raw_data_path, ic_data_path, target_levels, Float32)
50-
end
5171
return ic_data_path
5272
end
5373

@@ -74,6 +94,7 @@ function to_z_levels(source_file, target_file, target_levels, FT)
7494

7595
# assert ncin has required variables
7696
req_vars = ["u", "v", "w", "t", "q", "skt", "sp"]
97+
opt_vars = ["crwc", "cswc", "clwc", "ciwc"]
7798
@assert all(map(x -> x in (keys(ncin)), req_vars)) "Source file $source_file is missing subset of the required variables: $req_vars"
7899

79100
# Read and cast coordinates to FT type
@@ -118,54 +139,49 @@ function to_z_levels(source_file, target_file, target_levels, FT)
118139
z_var = defVar(ncout, "z", FT, ("z",), attrib = z_attrib)
119140
z_var[:] = target_levels
120141

121-
# Interpolate and write 3D variables
122-
u_var =
123-
defVar(ncout, "u", FT, ("lon", "lat", "z"), attrib = ncin["u"].attrib)
124-
u_var[:, :, :] =
125-
interpz_3d(target_levels, source_z, FT.(ncin["u"][:, :, :, 1]))
126-
127-
v_var =
128-
defVar(ncout, "v", FT, ("lon", "lat", "z"), attrib = ncin["v"].attrib)
129-
v_var[:, :, :] =
130-
interpz_3d(target_levels, source_z, FT.(ncin["v"][:, :, :, 1]))
131-
142+
# Interpolate and write required 3D variables via loop
132143
# ERA5 w is from a hydrostatic model and so isn't meaningful for ClimaAtmos
133144
# See https://agupubs.onlinelibrary.wiley.com/doi/full/10.1002/2017MS001059
134-
w_var =
135-
defVar(ncout, "w", FT, ("lon", "lat", "z"), attrib = ncin["w"].attrib)
136-
w_var[:, :, :] = zeros(FT, length(lon), length(lat), length(target_levels))
137-
138-
t_var =
139-
defVar(ncout, "t", FT, ("lon", "lat", "z"), attrib = ncin["t"].attrib)
140-
t_var[:, :, :] =
141-
interpz_3d(target_levels, source_z, FT.(ncin["t"][:, :, :, 1]))
142-
143-
q_var =
144-
defVar(ncout, "q", FT, ("lon", "lat", "z"), attrib = ncin["q"].attrib)
145-
q_var[:, :, :] =
146-
max.(
147-
interpz_3d(target_levels, source_z, FT.(ncin["q"][:, :, :, 1])),
148-
FT(0),
149-
)
145+
req3d = ["u", "v", "t", "q", "w"]
146+
for var_name in req3d
147+
var_obj =
148+
defVar(ncout, var_name, FT, ("lon", "lat", "z"), attrib = ncin[var_name].attrib)
149+
if var_name == "w"
150+
var_obj[:, :, :] = zeros(FT, length(lon), length(lat), length(target_levels))
151+
else
152+
data = interpz_3d(target_levels, source_z, FT.(ncin[var_name][:, :, :, 1]))
153+
if var_name == "q"
154+
data = max.(data, FT(0))
155+
end
156+
var_obj[:, :, :] = data
157+
end
158+
end
150159

151160
# Write 2D surface variables - extend to all levels (TODO: accept 2D variables in atmos)
152161
# Simply repeat the surface values for all levels
153-
skt_var = defVar(
154-
ncout,
155-
"skt",
156-
FT,
157-
("lon", "lat", "z"),
158-
attrib = ncin["skt"].attrib,
159-
)
160-
for k in 1:length(target_levels)
161-
skt_var[:, :, k] = FT.(ncin["skt"][:, :, 1])
162+
surf_map = Dict("skt" => "skt", "sp" => "p")
163+
for (src_name, dst_name) in surf_map
164+
var_obj =
165+
defVar(ncout, dst_name, FT, ("lon", "lat", "z"), attrib = ncin[src_name].attrib)
166+
for k in 1:length(target_levels)
167+
var_obj[:, :, k] = FT.(ncin[src_name][:, :, 1])
168+
end
162169
end
163170

164-
# TODO: rename p to sp in MoistFromFile for consistency
165-
sp_var =
166-
defVar(ncout, "p", FT, ("lon", "lat", "z"), attrib = ncin["sp"].attrib)
167-
for k in 1:length(target_levels)
168-
sp_var[:, :, k] = FT.(ncin["sp"][:, :, 1])
171+
# Interpolate optional cloud water content variables if available
172+
for var_name in opt_vars
173+
if haskey(ncin, var_name)
174+
@info "Interpolating optional variable: $var_name"
175+
var_data = ncin[var_name][:, :, :, 1]
176+
var_var = defVar(
177+
ncout,
178+
var_name,
179+
FT,
180+
("lon", "lat", "z"),
181+
attrib = ncin[var_name].attrib,
182+
)
183+
var_var[:, :, :] = interpz_3d(target_levels, source_z, FT.(var_data))
184+
end
169185
end
170186

171187
# Close files

0 commit comments

Comments
 (0)