Skip to content

Commit 6f20da6

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

File tree

3 files changed

+179
-7
lines changed

3 files changed

+179
-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: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,137 @@ 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+
try
435+
ᶠz_surface = Fields.level(
436+
SpaceVaryingInputs.SpaceVaryingInput(
437+
file_path,
438+
var_name,
439+
face_space,
440+
regridder_kwargs = regridder_kwargs,
441+
),
442+
Fields.half,
443+
)
444+
catch err
445+
if err isa ArgumentError || err isa KeyError
446+
@warn "Skipping topographic correction because variable `$var_name` is missing from $(file_path)."
447+
return false
448+
else
449+
rethrow(err)
450+
end
451+
end
452+
453+
if ᶠz_surface === nothing
454+
return false
455+
end
456+
457+
FT = eltype(thermo_params)
458+
grav = thermo_params.grav
459+
460+
# Log initial surface pressure statistics
461+
p_sfc_arr = Array(Fields.field2array(p_sfc))
462+
p_sfc_finite = filter(isfinite, p_sfc_arr)
463+
464+
ᶠz_model_surface = Fields.level(Fields.coordinate_field(Y.f).z, Fields.half)
465+
ᶠΔz = zeros(face_space)
466+
@. ᶠΔz = ᶠz_model_surface - ᶠz_surface
467+
468+
# Log surface height statistics
469+
z_surface_arr = Array(Fields.field2array(ᶠz_surface))
470+
z_model_arr = Array(Fields.field2array(ᶠz_model_surface))
471+
Δz_arr = Array(Fields.field2array(ᶠΔz))
472+
473+
ᶜphase_partition = TD.PhasePartition.(ᶜq_tot)
474+
ᶜR_m = TD.gas_constant_air.(thermo_params, ᶜphase_partition)
475+
ᶠR_m = ᶠinterp.(ᶜR_m)
476+
ᶠR_m_sfc = Fields.level(ᶠR_m, Fields.half)
477+
478+
ᶠT = ᶠinterp.(ᶜT)
479+
ᶠT_sfc = Fields.level(ᶠT, Fields.half)
480+
481+
denom = copy(ᶠR_m_sfc)
482+
@. denom = ᶠR_m_sfc * ᶠT_sfc
483+
denom_floor = FT(10)
484+
@. denom = ifelse(isfinite(denom) && denom > denom_floor, denom, denom_floor)
485+
486+
correction_factor = similar(ᶠΔz)
487+
@. correction_factor = ifelse(
488+
isfinite(ᶠΔz),
489+
exp(FT(-1) * ᶠΔz * grav / denom),
490+
one(FT),
491+
)
492+
493+
@. p_sfc =
494+
p_sfc *
495+
ifelse(
496+
isfinite(ᶠΔz),
497+
exp(FT(-1) * ᶠΔz * grav / denom),
498+
one(FT),
499+
)
500+
501+
@info "Adjusted surface pressure to account for ERA5/model surface-height differences."
502+
return true
503+
end
504+
374505
# WeatherModel function using the shared implementation
375506
function overwrite_initial_conditions!(
376507
initial_condition::WeatherModel,
@@ -421,6 +552,16 @@ function overwrite_initial_conditions!(
421552
center_space,
422553
regridder_kwargs = regridder_kwargs,
423554
)
555+
correct_surface_pressure_for_topography!(
556+
p_sfc,
557+
file_path,
558+
face_space,
559+
Y,
560+
ᶜT,
561+
ᶜq_tot,
562+
thermo_params,
563+
regridder_kwargs,
564+
)
424565

425566
# With the known temperature (ᶜT) and moisture (ᶜq_tot) profile,
426567
# 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)