Skip to content

Commit be82aa3

Browse files
committed
Add WeatherModel initial conditions for prognostic EDMF with support for
water species. Interpolate IC to arbitrary vertical grid. Generalize paths for IC.
1 parent 9fda311 commit be82aa3

File tree

5 files changed

+196
-46
lines changed

5 files changed

+196
-46
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: 3 additions & 4 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, pr]
4140
period: 60mins
4241
- short_name: [rsut, rlut, ts, pr, mslp]
4342
period: 60mins

src/initial_conditions/initial_conditions.jl

Lines changed: 129 additions & 21 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)
@@ -214,7 +215,9 @@ function (initial_condition::Union{MoistFromFile, WeatherModel})(params)
214215
grav = CAP.grav(params)
215216
thermo_params = CAP.thermodynamics_params(params)
216217

217-
T, p = FT(NaN), FT(NaN) # placeholder values
218+
# Use safe defaults to avoid transient NaNs before overwrite
219+
T = FT(CAP.T_0(params))
220+
p = FT(CAP.MSLP(params))
218221

219222
return LocalState(;
220223
params,
@@ -377,7 +380,24 @@ function overwrite_initial_conditions!(
377380
thermo_params,
378381
)
379382
extrapolation_bc = (Intp.Periodic(), Intp.Flat(), Intp.Flat())
380-
file_path = weather_model_data_path(initial_condition.start_date)
383+
384+
# Extract face coordinates and compute center midpoints
385+
z_coords = Fields.axes(Y.c).grid.vertical_grid.topology.mesh.faces
386+
387+
# Compute center coordinates as midpoints between faces
388+
face_z_values = [z.z for z in z_coords]
389+
center_z_values = [
390+
(face_z_values[i] + face_z_values[i + 1]) / 2 for
391+
i in 1:(length(face_z_values) - 1)
392+
]
393+
394+
target_levels = Array(center_z_values)
395+
396+
file_path = weather_model_data_path(
397+
initial_condition.start_date,
398+
target_levels,
399+
initial_condition.era5_initial_condition_dir,
400+
)
381401
return _overwrite_initial_conditions_from_file!(
382402
file_path,
383403
extrapolation_bc,
@@ -454,10 +474,9 @@ function _overwrite_initial_conditions_from_file!(
454474
ᶠlnp_over_psfc = zeros(face_space)
455475
Operators.column_integral_indefinite!(ᶠlnp_over_psfc, ᶜ∂lnp∂z)
456476
ᶠp = p_sfc .* exp.(ᶠlnp_over_psfc)
457-
ᶜts = TD.PhaseEquil_pTq.(thermo_params, ᶜinterp.(ᶠp), ᶜT, ᶜq_tot)
477+
# Compute center pressure for thermodynamic state construction
478+
ᶜp = ᶜinterp.(ᶠp)
458479

459-
# Assign prognostic variables from equilibrium moisture models
460-
Y.c.ρ .= TD.air_density.(thermo_params, ᶜts)
461480
# Velocity is first assigned on cell-centers and then interpolated onto
462481
# cell faces.
463482
vel =
@@ -483,36 +502,125 @@ function _overwrite_initial_conditions_from_file!(
483502
)
484503
Y.c.uₕ .= C12.(Geometry.UVVector.(vel))
485504
Y.f.u₃ .= ᶠinterp.(C3.(Geometry.WVector.(vel)))
505+
506+
# Load cloud/precip water fields if present (fallback to zeros)
507+
has_liq_ice = hasproperty(Y.c, :ρq_liq) && hasproperty(Y.c, :ρq_ice)
508+
has_precip = hasproperty(Y.c, :ρq_rai) && hasproperty(Y.c, :ρq_sno)
509+
# TEMP: diagnose 1M condensed/precip species at runtime (initialize to zero here)
510+
diagnose_1m = true
511+
512+
ᶜq_liq = zeros(center_space)
513+
ᶜq_ice = zeros(center_space)
514+
ᶜq_rai = zeros(center_space)
515+
ᶜq_sno = zeros(center_space)
516+
517+
if has_liq_ice && !diagnose_1m
518+
try
519+
ᶜq_liq = SpaceVaryingInputs.SpaceVaryingInput(
520+
file_path,
521+
"clwc",
522+
center_space,
523+
regridder_kwargs = regridder_kwargs,
524+
)
525+
catch
526+
@info "Variable clwc not found in $(file_path); initializing liquid cloud to zero"
527+
end
528+
try
529+
ᶜq_ice = SpaceVaryingInputs.SpaceVaryingInput(
530+
file_path,
531+
"ciwc",
532+
center_space,
533+
regridder_kwargs = regridder_kwargs,
534+
)
535+
catch
536+
@info "Variable ciwc not found in $(file_path); initializing ice cloud to zero"
537+
end
538+
end
539+
if has_precip && !diagnose_1m
540+
try
541+
ᶜq_rai = SpaceVaryingInputs.SpaceVaryingInput(
542+
file_path,
543+
"crwc",
544+
center_space,
545+
regridder_kwargs = regridder_kwargs,
546+
)
547+
catch
548+
@info "Variable crwc not found in $(file_path); initializing rain water to zero"
549+
end
550+
try
551+
ᶜq_sno = SpaceVaryingInputs.SpaceVaryingInput(
552+
file_path,
553+
"cswc",
554+
center_space,
555+
regridder_kwargs = regridder_kwargs,
556+
)
557+
catch
558+
@info "Variable cswc not found in $(file_path); initializing snow water to zero"
559+
end
560+
end
561+
562+
# Build thermodynamic state consistently with moisture partition
563+
is_nonequil = hasproperty(Y.c, :ρq_liq) || hasproperty(Y.c, :ρq_ice)
564+
# Use vapor from ERA5 q as first component; add cloud+precip to condensed partitions (zeros if not provided)
565+
ᶜq_vap = max.(ᶜq_tot, zero.(ᶜq_tot))
566+
ᶜq_liq .= ifelse.(isfinite.(ᶜq_liq), ᶜq_liq, zero.(ᶜq_liq))
567+
ᶜq_liq .= max.(ᶜq_liq, zero.(ᶜq_liq))
568+
ᶜq_ice .= ifelse.(isfinite.(ᶜq_ice), ᶜq_ice, zero.(ᶜq_ice))
569+
ᶜq_ice .= max.(ᶜq_ice, zero.(ᶜq_ice))
570+
ᶜq_rai .= ifelse.(isfinite.(ᶜq_rai), ᶜq_rai, zero.(ᶜq_rai))
571+
ᶜq_rai .= max.(ᶜq_rai, zero.(ᶜq_rai))
572+
ᶜq_sno .= ifelse.(isfinite.(ᶜq_sno), ᶜq_sno, zero.(ᶜq_sno))
573+
ᶜq_sno .= max.(ᶜq_sno, zero.(ᶜq_sno))
574+
ᶜpartition = TD.PhasePartition.(ᶜq_vap, ᶜq_liq .+ ᶜq_rai, ᶜq_ice .+ ᶜq_sno)
575+
if is_nonequil
576+
ᶜts = TD.PhaseNonEquil_pTq.(thermo_params, ᶜp, ᶜT, ᶜpartition)
577+
else
578+
ᶜts = TD.PhaseEquil_pTq.(thermo_params, ᶜp, ᶜT, ᶜq_tot)
579+
end
580+
581+
# Density
582+
Y.c.ρ .= TD.air_density.(thermo_params, ᶜts)
583+
584+
# Kinetic and potential energy
486585
e_kin = similar(ᶜT)
487586
e_kin .= compute_kinetic(Y.c.uₕ, Y.f.u₃)
488587
e_pot = Fields.coordinate_field(Y.c).z .* thermo_params.grav
588+
589+
# Total energy
489590
Y.c.ρe_tot .= TD.total_energy.(thermo_params, ᶜts, e_kin, e_pot) .* Y.c.ρ
591+
592+
# Moisture mass densities
490593
if hasproperty(Y.c, :ρq_tot)
594+
# Keep ρq_tot equal to ERA5 vapor (do not add precipitating species)
491595
Y.c.ρq_tot .= ᶜq_tot .* Y.c.ρ
492596
else
493597
error(
494598
"`dry` configurations are incompatible with the interpolated initial conditions.",
495599
)
496600
end
497-
if hasproperty(Y.c, :ρq_sno) && hasproperty(Y.c, :ρq_rai)
498-
Y.c.ρq_sno .=
499-
SpaceVaryingInputs.SpaceVaryingInput(
500-
file_path,
501-
"cswc",
502-
center_space,
503-
regridder_kwargs = regridder_kwargs,
504-
) .* Y.c.ρ
505-
Y.c.ρq_rai .=
506-
SpaceVaryingInputs.SpaceVaryingInput(
507-
file_path,
508-
"crwc",
509-
center_space,
510-
regridder_kwargs = regridder_kwargs,
511-
) .* Y.c.ρ
601+
602+
if hasproperty(Y.c, :ρq_rai)
603+
Y.c.ρq_rai .= ᶜq_rai .* Y.c.ρ
604+
Y.c.ρq_sno .= ᶜq_sno .* Y.c.ρ
605+
end
606+
if hasproperty(Y.c, :ρq_liq)
607+
Y.c.ρq_liq .= ᶜq_liq .* Y.c.ρ
608+
Y.c.ρq_ice .= ᶜq_ice .* Y.c.ρ
609+
end
610+
611+
# Set prognostic EDMF draft subdomains to consistent values
612+
if hasproperty(Y.c, :sgsʲs) # if prognostic EDMF
613+
ᶜmse = TD.specific_enthalpy.(thermo_params, ᶜts) .+ e_pot
614+
for name in propertynames(Y.c.sgsʲs)
615+
s = getproperty(Y.c.sgsʲs, name)
616+
hasproperty(s, :ρa) && fill!(s.ρa, 0)
617+
hasproperty(s, :mse) && (s.mse .= ᶜmse)
618+
hasproperty(s, :q_tot) && (s.q_tot .= ᶜq_vap)
619+
end
512620
end
513621

622+
# NOTE: This is not the most consistent, but it is better than NaNs
514623
if hasproperty(Y.c, :sgs⁰) && hasproperty(Y.c.sgs⁰, :ρatke)
515-
# NOTE: This is not the most consistent, but it is better than NaNs
516624
fill!(Y.c.sgs⁰.ρatke, 0)
517625
end
518626

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: 57 additions & 20 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
@@ -168,6 +189,22 @@ function to_z_levels(source_file, target_file, target_levels, FT)
168189
sp_var[:, :, k] = FT.(ncin["sp"][:, :, 1])
169190
end
170191

192+
# Interpolate optional cloud water content variables if available
193+
for var_name in opt_vars
194+
if haskey(ncin, var_name)
195+
@info "Interpolating optional variable: $var_name"
196+
var_data = ncin[var_name][:, :, :, 1]
197+
var_var = defVar(
198+
ncout,
199+
var_name,
200+
FT,
201+
("lon", "lat", "z"),
202+
attrib = ncin[var_name].attrib,
203+
)
204+
var_var[:, :, :] = interpz_3d(target_levels, source_z, FT.(var_data))
205+
end
206+
end
207+
171208
# Close files
172209
close(ncin)
173210
close(ncout)

0 commit comments

Comments
 (0)