Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions src/diagnostics/core_diagnostics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1647,24 +1647,32 @@ add_diagnostic_variable!(
function compute_mslp!(out, state, cache, time)
thermo_params = CAP.thermodynamics_params(cache.params)
g = TD.Parameters.grav(thermo_params)
R_m_surf = Fields.level(
lazy.(TD.gas_constant_air.(thermo_params, cache.precomputed.ᶜts)),
1,
)
ts_level = Fields.level(cache.precomputed.ᶜts, 1)
R_m_surf = @. lazy(TD.gas_constant_air(thermo_params, ts_level))

# get pressure, temperature, and height at the lowest atmospheric level
p_level = Fields.level(cache.precomputed.ᶜp, 1)
t_level = Fields.level(
lazy.(TD.air_temperature.(thermo_params, cache.precomputed.ᶜts)),
1,
)
t_level = @. lazy(TD.air_temperature(thermo_params, ts_level))
z_level = Fields.level(Fields.coordinate_field(state.c.ρ).z, 1)

# compute sea level pressure using the hypsometric equation
# Reduce to mean sea level using hypsometric formulation with lapse rate adjustment
# Using constant lapse rate Γ = 6.5 K/km, with virtual temperature
# represented via R_m_surf. This reduces biases over
# very cold or very warm high-topography regions.
FT = Spaces.undertype(Fields.axes(state.c.ρ))
Γ = FT(6.5e-3) # K m^-1

# p_msl = p_z0 * [1 + Γ * z / T_z0]^( g / (R_m Γ))
# where:
# - p_z0 pressure at the lowest model level
# - T_z0 air temperature at the lowest model level
# - R_m moist-air gas constant at the surface (R_m_surf), which
# accounts for virtual-temperature effects in the exponent
# - Γ constant lapse rate (6.5 K/km here)

if isnothing(out)
return @. p_level * exp(g * z_level / (R_m_surf * t_level))
return p_level .* (1 .+ Γ .* z_level ./ t_level) .^ (g / Γ ./ R_m_surf)
else
@. out = p_level * exp(g * z_level / (R_m_surf * t_level))
out .= p_level .* (1 .+ Γ .* z_level ./ t_level) .^ (g / Γ ./ R_m_surf)
end
end

Expand All @@ -1673,7 +1681,7 @@ add_diagnostic_variable!(
long_name = "Mean Sea Level Pressure",
standard_name = "mean_sea_level_pressure",
units = "Pa",
comments = "Mean sea level pressure computed from the hypsometric equation",
comments = "Mean sea level pressure computed using a lapse-rate-dependent hypsometric reduction (ERA-style; Γ=6.5 K/km with virtual temperature via moist gas constant).",
compute! = compute_mslp!,
)

Expand Down
106 changes: 106 additions & 0 deletions src/initial_conditions/initial_conditions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,92 @@ function overwrite_initial_conditions!(
)
end

"""
correct_surface_pressure_for_topography!(
p_sfc,
file_path,
face_space,
Y,
ᶜT,
ᶜq_tot,
thermo_params,
regridder_kwargs;
surface_altitude_var = "z_sfc",
)

Adjusts the surface pressure field `p_sfc` to account for mismatches between
ERA5 (file) surface altitude and the model orography when specifying pressure.

Δz = z_model_surface - z_sfc

and applies a hydrostatic correction at the surface using the local moist gas
constant and temperature at the surface:

p_sfc .= p_sfc .* exp.(-Δz * g ./ (R_m_sfc .* T_sfc))

where:
- `g` is gravitational acceleration from `thermo_params`
- `R_m_sfc` is the moist-air gas constant evaluated from `ᶜq_tot` at the surface
- `T_sfc` is the air temperature from `ᶜT` at the surface

Returns `true` if the correction is applied; returns `false` if the surface
altitude field cannot be loaded.

Arguments
- `p_sfc`: face field of surface pressure to be corrected (modified in-place)
- `file_path`: path to the ERA5-derived initialization NetCDF file
- `face_space`: face space of the model grid (for reading/regridding)
- `Y`: prognostic state, used to obtain model surface height
- `ᶜT`: center field of temperature
- `ᶜq_tot`: center field of total specific humidity
- `thermo_params`: thermodynamics parameter set
- `regridder_kwargs`: keyword arguments forwarded to the regridder
- `surface_altitude_var`: variable name for surface altitude (default `"z_sfc"`)
"""
function correct_surface_pressure_for_topography!(
p_sfc,
file_path,
face_space,
Y,
ᶜT,
ᶜq_tot,
thermo_params,
regridder_kwargs;
surface_altitude_var = "z_sfc",
)
ᶠz_surface = Fields.level(
SpaceVaryingInputs.SpaceVaryingInput(
file_path,
surface_altitude_var,
face_space,
regridder_kwargs = regridder_kwargs,
),
Fields.half,
)

if ᶠz_surface === nothing
return false
end

FT = eltype(thermo_params)
grav = thermo_params.grav

ᶠz_model_surface = Fields.level(Fields.coordinate_field(Y.f).z, Fields.half)
ᶠΔz = zeros(face_space)
@. ᶠΔz = ᶠz_model_surface - ᶠz_surface

ᶠR_m = ᶠinterp.(TD.gas_constant_air.(thermo_params, TD.PhasePartition.(ᶜq_tot)))
ᶠR_m_sfc = Fields.level(ᶠR_m, Fields.half)

ᶠT = ᶠinterp.(ᶜT)
ᶠT_sfc = Fields.level(ᶠT, Fields.half)

@. p_sfc = p_sfc * exp(FT(-1) * ᶠΔz * grav / (ᶠR_m_sfc * ᶠT_sfc))

@info "Adjusted surface pressure to account for ERA5/model surface-height differences."
return true
end

# WeatherModel function using the shared implementation
function overwrite_initial_conditions!(
initial_condition::WeatherModel,
Expand Down Expand Up @@ -436,6 +522,26 @@ function overwrite_initial_conditions!(
center_space,
regridder_kwargs = regridder_kwargs,
)
# Apply hydrostatic surface-pressure correction only if surface altitude is available
surface_altitude_var = "z_sfc"
has_surface_altitude = NC.NCDataset(file_path) do ds
haskey(ds, surface_altitude_var)
end
if has_surface_altitude
correct_surface_pressure_for_topography!(
p_sfc,
file_path,
face_space,
Y,
ᶜT,
ᶜq_tot,
thermo_params,
regridder_kwargs;
surface_altitude_var = surface_altitude_var,
)
else
@warn "Skipping topographic correction because variable `$surface_altitude_var` is missing from $(file_path)."
end

# With the known temperature (ᶜT) and moisture (ᶜq_tot) profile,
# recompute the pressure levels assuming hydrostatic balance is maintained.
Expand Down
26 changes: 21 additions & 5 deletions src/utils/weather_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,29 @@ function to_z_levels(source_file, target_file, target_levels, FT)
end

# Write 2D surface variables - extend to all levels (TODO: accept 2D variables in atmos)
# Simply repeat the surface values for all levels
surf_map = Dict("skt" => "skt", "sp" => "p")
# Duplicate 2D surface field across all target vertical levels
surf_map = Dict("skt" => "skt", "sp" => "p", "surface_geopotential" => "z_sfc")
for (src_name, dst_name) in surf_map
var_obj =
defVar(ncout, dst_name, FT, ("lon", "lat", "z"), attrib = ncin[src_name].attrib)
# Choose attributes; for z_sfc, set clean altitude attributes
var_attrib = if dst_name == "z_sfc"
Dict(
"standard_name" => "surface_altitude",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the data you're writing is geopotential based on the ERA5 name? If that is the case the units are gravity * height (m^2/s^2) and would be good to at least rename the long_name and the units. I would also use zg_sfc even though ERA5 uses z for geopotential because then it's very clear.

Copy link
Member

@Julians42 Julians42 Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok I see what is happening now, you're correct. Not sure I have an immediate suggestion other that it is a little confusing because there are two if statements in this loop to catch z_sfc since it needs a new var_attrib and needs to be divided by gravity. Possibly separating it out could be cleaner / easier to read if a little longer.

"long_name" => "surface altitude derived from ERA5",
"units" => "m",
"source_variable" => src_name,
)
else
ncin[src_name].attrib
end
var_obj = defVar(ncout, dst_name, FT, ("lon", "lat", "z"), attrib = var_attrib)
# Read first time slice and coalesce; follow same convention as sp (use [:, :, 1])
data2d = FT.(coalesce.(ncin[src_name][:, :, 1], NaN))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a coalesce? Are there instances when the data is NaN and our model will still run? If not then we could consider asserting not NaN

# Convert geopotential to meters if necessary
if dst_name == "z_sfc"
data2d .= data2d ./ grav
end
for k in 1:length(target_levels)
var_obj[:, :, k] = FT.(ncin[src_name][:, :, 1])
var_obj[:, :, k] = data2d
end
end

Expand Down
Loading