Skip to content

Commit 1ce4418

Browse files
committed
Account for ERA5/ClimaAtmos topo differences when initializing pressure. Account for lapse rate in mslp sea level reduction (diagnostics).
1 parent ee1c274 commit 1ce4418

File tree

3 files changed

+178
-7
lines changed

3 files changed

+178
-7
lines changed

src/diagnostics/core_diagnostics.jl

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1660,11 +1660,26 @@ function compute_mslp!(out, state, cache, time)
16601660
)
16611661
z_level = Fields.level(Fields.coordinate_field(state.c.ρ).z, 1)
16621662

1663-
# compute sea level pressure using the hypsometric equation
1663+
# Reduce to mean sea level using a standard-lapse hypsometric formulation (ERA-style)
1664+
# Using constant lapse rate Γ = 6.5 K/km, with virtual temperature
1665+
# represented via R_m_surf. This more closely matches ERA5's msl computation and
1666+
# reduces biases over very cold or very warm high-topography regions.
1667+
FT = Spaces.undertype(Fields.axes(state.c.ρ))
1668+
Γ = FT(6.5e-3) # K m^-1
1669+
ϵ = FT(1e-6) # small floor to avoid non-positive base
1670+
1671+
# bracket = max(ϵ, 1 + Γ z / T(z))
1672+
bracket = similar(t_level)
1673+
@. bracket = max(ϵ, 1 + Γ * z_level / t_level)
1674+
1675+
# exponent = g / (R_m Γ)
1676+
exponent = similar(R_m_surf)
1677+
@. exponent = g / (R_m_surf * Γ)
1678+
16641679
if isnothing(out)
1665-
return @. p_level * exp(g * z_level / (R_m_surf * t_level))
1680+
return p_level .* (bracket .^ exponent)
16661681
else
1667-
@. out = p_level * exp(g * z_level / (R_m_surf * t_level))
1682+
out .= p_level .* (bracket .^ exponent)
16681683
end
16691684
end
16701685

src/initial_conditions/initial_conditions.jl

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,136 @@ function overwrite_initial_conditions!(
371371
)
372372
end
373373

374+
"""
375+
correct_surface_pressure_for_topography!(
376+
p_sfc,
377+
file_path,
378+
face_space,
379+
Y,
380+
ᶜT,
381+
ᶜq_tot,
382+
thermo_params,
383+
regridder_kwargs,
384+
)
385+
386+
Adjusts the surface pressure field `p_sfc` to account for mismatches between
387+
ERA5 (file) surface altitude and the model orography when specifying pressure.
388+
389+
Δz = z_model_surface - z_sfc
390+
391+
and applies a hydrostatic correction at the surface using the local moist gas
392+
constant and temperature at the surface:
393+
394+
p_sfc .= p_sfc .* exp.(-Δz * g ./ (R_m_sfc .* T_sfc))
395+
396+
where:
397+
- `g` is gravitational acceleration from `thermo_params`
398+
- `R_m_sfc` is the moist-air gas constant evaluated from `ᶜq_tot` at the surface
399+
- `T_sfc` is the air temperature from `ᶜT` at the surface
400+
401+
A small floor is applied to the denominator `R_m_sfc .* T_sfc` to avoid
402+
numerical blow-ups, and non-finite `Δz` entries leave `p_sfc` unchanged.
403+
404+
Returns `true` if the correction is applied. If `z_sfc` is missing in the
405+
provided file, the function logs a warning and returns `false` without
406+
modifying `p_sfc`.
407+
408+
Arguments
409+
- `p_sfc`: face field of surface pressure to be corrected (modified in-place)
410+
- `file_path`: path to the ERA5-derived initialization NetCDF file
411+
- `face_space`: face space of the model grid (for reading/regridding)
412+
- `Y`: prognostic state, used to obtain model surface height
413+
- `ᶜT`: center field of temperature
414+
- `ᶜq_tot`: center field of total specific humidity
415+
- `thermo_params`: thermodynamics parameter set
416+
- `regridder_kwargs`: keyword arguments forwarded to the regridder
417+
418+
Notes
419+
- Expects `z_sfc` units in meters; if the variable is not present, the
420+
correction is skipped.
421+
"""
422+
function correct_surface_pressure_for_topography!(
423+
p_sfc,
424+
file_path,
425+
face_space,
426+
Y,
427+
ᶜT,
428+
ᶜq_tot,
429+
thermo_params,
430+
regridder_kwargs,
431+
)
432+
var_name = "z_sfc"
433+
ᶠz_surface = nothing
434+
# Explicitly check if `z_sfc` exists in the input file; skip if not found
435+
has_z_sfc = NC.NCDataset(file_path) do ds
436+
haskey(ds, var_name)
437+
end
438+
if !has_z_sfc
439+
@warn "Skipping topographic correction because variable `$var_name` is missing from $(file_path)."
440+
return false
441+
end
442+
ᶠz_surface = Fields.level(
443+
SpaceVaryingInputs.SpaceVaryingInput(
444+
file_path,
445+
var_name,
446+
face_space,
447+
regridder_kwargs = regridder_kwargs,
448+
),
449+
Fields.half,
450+
)
451+
452+
if ᶠz_surface === nothing
453+
return false
454+
end
455+
456+
FT = eltype(thermo_params)
457+
grav = thermo_params.grav
458+
459+
# Log initial surface pressure statistics
460+
p_sfc_arr = Array(Fields.field2array(p_sfc))
461+
p_sfc_finite = filter(isfinite, p_sfc_arr)
462+
463+
ᶠz_model_surface = Fields.level(Fields.coordinate_field(Y.f).z, Fields.half)
464+
ᶠΔz = zeros(face_space)
465+
@. ᶠΔz = ᶠz_model_surface - ᶠz_surface
466+
467+
# Log surface height statistics
468+
z_surface_arr = Array(Fields.field2array(ᶠz_surface))
469+
z_model_arr = Array(Fields.field2array(ᶠz_model_surface))
470+
Δz_arr = Array(Fields.field2array(ᶠΔz))
471+
472+
ᶜphase_partition = TD.PhasePartition.(ᶜq_tot)
473+
ᶜR_m = TD.gas_constant_air.(thermo_params, ᶜphase_partition)
474+
ᶠR_m = ᶠinterp.(ᶜR_m)
475+
ᶠR_m_sfc = Fields.level(ᶠR_m, Fields.half)
476+
477+
ᶠT = ᶠinterp.(ᶜT)
478+
ᶠT_sfc = Fields.level(ᶠT, Fields.half)
479+
480+
denom = copy(ᶠR_m_sfc)
481+
@. denom = ᶠR_m_sfc * ᶠT_sfc
482+
denom_floor = FT(10)
483+
@. denom = ifelse(isfinite(denom) && denom > denom_floor, denom, denom_floor)
484+
485+
correction_factor = similar(ᶠΔz)
486+
@. correction_factor = ifelse(
487+
isfinite(ᶠΔz),
488+
exp(FT(-1) * ᶠΔz * grav / denom),
489+
one(FT),
490+
)
491+
492+
@. p_sfc =
493+
p_sfc *
494+
ifelse(
495+
isfinite(ᶠΔz),
496+
exp(FT(-1) * ᶠΔz * grav / denom),
497+
one(FT),
498+
)
499+
500+
@info "Adjusted surface pressure to account for ERA5/model surface-height differences."
501+
return true
502+
end
503+
374504
# WeatherModel function using the shared implementation
375505
function overwrite_initial_conditions!(
376506
initial_condition::WeatherModel,
@@ -421,6 +551,16 @@ function overwrite_initial_conditions!(
421551
center_space,
422552
regridder_kwargs = regridder_kwargs,
423553
)
554+
correct_surface_pressure_for_topography!(
555+
p_sfc,
556+
file_path,
557+
face_space,
558+
Y,
559+
ᶜT,
560+
ᶜq_tot,
561+
thermo_params,
562+
regridder_kwargs,
563+
)
424564

425565
# With the known temperature (ᶜT) and moisture (ᶜq_tot) profile,
426566
# recompute the pressure levels assuming hydrostatic balance is maintained.

src/utils/weather_model.jl

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,28 @@ function to_z_levels(source_file, target_file, target_levels, FT)
159159

160160
# Write 2D surface variables - extend to all levels (TODO: accept 2D variables in atmos)
161161
# Simply repeat the surface values for all levels
162-
surf_map = Dict("skt" => "skt", "sp" => "p")
162+
surf_map = Dict("skt" => "skt", "sp" => "p", "surface_geopotential" => "z_sfc")
163163
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)
164+
# Choose attributes; for z_sfc, set clean altitude attributes
165+
var_attrib = if dst_name == "z_sfc"
166+
Dict(
167+
"standard_name" => "surface_altitude",
168+
"long_name" => "surface altitude derived from ERA5",
169+
"units" => "m",
170+
"source_variable" => src_name,
171+
)
172+
else
173+
ncin[src_name].attrib
174+
end
175+
var_obj = defVar(ncout, dst_name, FT, ("lon", "lat", "z"), attrib = var_attrib)
176+
# Read first time slice and coalesce; follow same convention as sp (use [:, :, 1])
177+
data2d = FT.(coalesce.(ncin[src_name][:, :, 1], NaN))
178+
# Convert geopotential to meters if necessary
179+
if dst_name == "z_sfc"
180+
data2d .= data2d ./ grav
181+
end
166182
for k in 1:length(target_levels)
167-
var_obj[:, :, k] = FT.(ncin[src_name][:, :, 1])
183+
var_obj[:, :, k] = data2d
168184
end
169185
end
170186

0 commit comments

Comments
 (0)