-
Notifications
You must be signed in to change notification settings - Fork 26
Account for ERA5/ClimaAtmos topo differences #4098
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1660,11 +1660,26 @@ function compute_mslp!(out, state, cache, time) | |
| ) | ||
| 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 a standard-lapse hypsometric formulation (ERA-style) | ||
| # Using constant lapse rate Γ = 6.5 K/km, with virtual temperature | ||
| # represented via R_m_surf. This more closely matches ERA5's msl computation and | ||
| # 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 | ||
| ϵ = FT(1e-6) # small floor to avoid non-positive base | ||
|
|
||
| # bracket = max(ϵ, 1 + Γ z / T(z)) | ||
| bracket = similar(t_level) | ||
| @. bracket = max(ϵ, 1 + Γ * z_level / t_level) | ||
|
|
||
| # exponent = g / (R_m Γ) | ||
| exponent = similar(R_m_surf) | ||
| @. exponent = g / (R_m_surf * Γ) | ||
|
Comment on lines
+1672
to
+1677
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will allocate new arrays which is slow. Maybe @dennisYatunin could recommend a fix? |
||
|
|
||
| if isnothing(out) | ||
| return @. p_level * exp(g * z_level / (R_m_surf * t_level)) | ||
| return p_level .* (bracket .^ exponent) | ||
| else | ||
| @. out = p_level * exp(g * z_level / (R_m_surf * t_level)) | ||
| out .= p_level .* (bracket .^ exponent) | ||
| end | ||
| end | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -371,6 +371,136 @@ function overwrite_initial_conditions!( | |
| ) | ||
| end | ||
|
|
||
| """ | ||
| correct_surface_pressure_for_topography!( | ||
| p_sfc, | ||
| file_path, | ||
| face_space, | ||
| Y, | ||
| ᶜT, | ||
| ᶜq_tot, | ||
| thermo_params, | ||
| regridder_kwargs, | ||
| ) | ||
|
|
||
| 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 | ||
|
|
||
| A small floor is applied to the denominator `R_m_sfc .* T_sfc` to avoid | ||
| numerical blow-ups, and non-finite `Δz` entries leave `p_sfc` unchanged. | ||
|
|
||
| Returns `true` if the correction is applied. If `z_sfc` is missing in the | ||
| provided file, the function logs a warning and returns `false` without | ||
| modifying `p_sfc`. | ||
|
|
||
| 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 | ||
|
|
||
| Notes | ||
| - Expects `z_sfc` units in meters; if the variable is not present, the | ||
| correction is skipped. | ||
| """ | ||
| function correct_surface_pressure_for_topography!( | ||
| p_sfc, | ||
| file_path, | ||
| face_space, | ||
| Y, | ||
| ᶜT, | ||
| ᶜq_tot, | ||
| thermo_params, | ||
| regridder_kwargs, | ||
| ) | ||
| var_name = "z_sfc" | ||
| ᶠz_surface = nothing | ||
| # Explicitly check if `z_sfc` exists in the input file; skip if not found | ||
| has_z_sfc = NC.NCDataset(file_path) do ds | ||
| haskey(ds, var_name) | ||
| end | ||
| if !has_z_sfc | ||
| @warn "Skipping topographic correction because variable `$var_name` is missing from $(file_path)." | ||
| return false | ||
| end | ||
|
Comment on lines
+438
to
+441
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this |
||
| ᶠz_surface = Fields.level( | ||
| SpaceVaryingInputs.SpaceVaryingInput( | ||
| file_path, | ||
| var_name, | ||
| face_space, | ||
| regridder_kwargs = regridder_kwargs, | ||
| ), | ||
| Fields.half, | ||
| ) | ||
|
|
||
| if ᶠz_surface === nothing | ||
| return false | ||
| end | ||
|
|
||
| FT = eltype(thermo_params) | ||
| grav = thermo_params.grav | ||
|
|
||
| # Log initial surface pressure statistics | ||
| p_sfc_arr = Array(Fields.field2array(p_sfc)) | ||
| p_sfc_finite = filter(isfinite, p_sfc_arr) | ||
|
Comment on lines
+460
to
+461
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again is Array needed after field2array? Is isfinite needed if its surface pressure - shouldn't it be finite? I also think one line and a clearer comment would help |
||
|
|
||
| ᶠz_model_surface = Fields.level(Fields.coordinate_field(Y.f).z, Fields.half) | ||
| ᶠΔz = zeros(face_space) | ||
| @. ᶠΔz = ᶠz_model_surface - ᶠz_surface | ||
|
|
||
| # Log surface height statistics | ||
| z_surface_arr = Array(Fields.field2array(ᶠz_surface)) | ||
| z_model_arr = Array(Fields.field2array(ᶠz_model_surface)) | ||
| Δz_arr = Array(Fields.field2array(ᶠΔz)) | ||
|
Comment on lines
+468
to
+470
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't |
||
|
|
||
| ᶜphase_partition = TD.PhasePartition.(ᶜq_tot) | ||
| ᶜR_m = TD.gas_constant_air.(thermo_params, ᶜphase_partition) | ||
| ᶠR_m = ᶠinterp.(ᶜR_m) | ||
| ᶠR_m_sfc = Fields.level(ᶠR_m, Fields.half) | ||
|
|
||
| ᶠT = ᶠinterp.(ᶜT) | ||
| ᶠT_sfc = Fields.level(ᶠT, Fields.half) | ||
|
|
||
| denom = copy(ᶠR_m_sfc) | ||
| @. denom = ᶠR_m_sfc * ᶠT_sfc | ||
| denom_floor = FT(10) | ||
| @. denom = ifelse(isfinite(denom) && denom > denom_floor, denom, denom_floor) | ||
|
Comment on lines
+480
to
+483
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is written confusingly. Could you either comment to explain why we need so many steps (e.g. why is floor and finite checks needed?) Why is 10 chosen as a floor? |
||
|
|
||
| correction_factor = similar(ᶠΔz) | ||
| @. correction_factor = ifelse( | ||
| isfinite(ᶠΔz), | ||
| exp(FT(-1) * ᶠΔz * grav / denom), | ||
| one(FT), | ||
| ) | ||
|
|
||
| @. p_sfc = | ||
| p_sfc * | ||
| ifelse( | ||
| isfinite(ᶠΔz), | ||
| exp(FT(-1) * ᶠΔz * grav / denom), | ||
| one(FT), | ||
| ) | ||
|
Comment on lines
+485
to
+498
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Repeated code. either delete the first bit or simplify the second |
||
|
|
||
| @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, | ||
|
|
@@ -421,6 +551,16 @@ function overwrite_initial_conditions!( | |
| center_space, | ||
| regridder_kwargs = regridder_kwargs, | ||
| ) | ||
| correct_surface_pressure_for_topography!( | ||
| p_sfc, | ||
| file_path, | ||
| face_space, | ||
| Y, | ||
| ᶜT, | ||
| ᶜq_tot, | ||
| thermo_params, | ||
| regridder_kwargs, | ||
| ) | ||
|
|
||
| # With the known temperature (ᶜT) and moisture (ᶜq_tot) profile, | ||
| # recompute the pressure levels assuming hydrostatic balance is maintained. | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -159,12 +159,28 @@ function to_z_levels(source_file, target_file, target_levels, FT) | |||||||
|
|
||||||||
| # 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") | ||||||||
| 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", | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||
| "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)) | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think this comment would be helpful here |
||||||||
| for k in 1:length(target_levels) | ||||||||
| var_obj[:, :, k] = FT.(ncin[src_name][:, :, 1]) | ||||||||
| var_obj[:, :, k] = data2d | ||||||||
| end | ||||||||
| end | ||||||||
|
|
||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you write out the whole equation here for clarity? Then it matters less how you break up the computation for performance. e.g., "p_msl = p(z1)(1+\Gamma z1 / t)^(g/(Rm*\Gamma) is the ERA5 standard calculation for MSLP...".