Skip to content

Implement lapse-rate extrapolation for vertical interpolation (extrap_type == 2)#1413

Open
mrixlam wants to merge 5 commits intoMPAS-Dev:developfrom
mrixlam:bugfix-vinterp-lapse-rate
Open

Implement lapse-rate extrapolation for vertical interpolation (extrap_type == 2)#1413
mrixlam wants to merge 5 commits intoMPAS-Dev:developfrom
mrixlam:bugfix-vinterp-lapse-rate

Conversation

@mrixlam
Copy link
Copy Markdown

@mrixlam mrixlam commented Feb 12, 2026

Summary:

This PR implements the missing lapse-rate extrapolation functionality (extrap_type == 2) for vertical interpolation in the init_atmosphere module. Previously, this extrapolation type was documented but not implemented for the upper boundary condition, causing fatal errors when model top exceeded input data vertical range.

Problem Statement:

When initializing MPAS-Atmosphere with input data (e.g., IFS/ECMWF) that has a top level below or near the model's configured vertical extent (config_ztop), vertical interpolation may require extrapolation beyond available data levels. While the code supported three extrapolation types:

Type 0: Constant (use nearest value)
Type 1: Linear (extend using slope between last two points)
Type 2: Lapse-rate (use atmospheric temperature gradient)

Type 2 (lapse-rate) was not implemented for the upper boundary (target_z >= zf(1,nz)), resulting in this fatal error:

ERROR: extrap_type == 2 not implemented for target_z >= zf(1,nz)
CRITICAL ERROR: Error in interpolation of t(k,iCell) for k=1, iCell=1

This prevented users from using the physically more appropriate lapse-rate extrapolation method via config_extrap_airtemp = 'lapse-rate' in namelist.init_atmosphere.

Files Modified:

src/core_init_atmosphere/mpas_init_atm_vinterp.F (+5 lines, -0 lines)
src/core_init_atmosphere/mpas_init_atm_cases.F (+4 lines, -3 lines)

Testing:

Successfully tested MPAS model initialization with quasi-uniform global 15km quasi-uniform mesh with following specifications:

IFS 137-level input data (model top ~80 km)
Model configuration: 66 vertical levels, 40 km top
config_extrap_airtemp = 'lapse-rate' in namelist

Copilot AI review requested due to automatic review settings February 12, 2026 07:02
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Implements the previously missing extrap_type == 2 (“lapse-rate”) behavior for vertical extrapolation at the upper boundary during init-atmosphere vertical interpolation, preventing fatal errors when the model top exceeds the input data’s vertical range.

Changes:

  • Extend vertical_interp to handle extrap_type == 2 for target_z >= zf(1,nz) (upper-boundary extrapolation).
  • Add/standardize an optional ierr output on the init_atm_vinterp implementation to signal invalid extrapolation types.
  • Remove the prior “not implemented” fatal-path for extrap_type == 2 in mpas_init_atm_cases.F.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/core_init_atmosphere/mpas_init_atm_vinterp.F Adds optional ierr and implements extrap_type==2 for upper/lower boundary extrapolation.
src/core_init_atmosphere/mpas_init_atm_cases.F Replaces the previous fatal error for upper-boundary extrap_type==2 with an extrapolation calculation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +85 to +88
else if (extrap_type == 2) then
! Lapse-rate extrapolation: calculate lapse rate from bottom two levels
lapse_rate = (zf(2,2) - zf(2,1)) / (zf(1,2) - zf(1,1))
vertical_interp = zf(2,1) + lapse_rate * (target_z - zf(1,1))
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

extrap_type == 2 (lapse-rate) is implemented here using the same formula as extrap_type == 1 (linear), i.e., a slope computed from the bottom two levels. That makes the new lapse-rate option behaviorally identical to linear extrapolation. If lapse-rate is intended to be a fixed temperature lapse rate (as in the other vertical_interp implementation in mpas_init_atm_cases.F), please apply that constant lapse rate here (or otherwise document/rename the option so it’s not indistinguishable from linear).

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +104
else if (extrap_type == 2) then
! Lapse-rate extrapolation: calculate lapse rate from top two levels
lapse_rate = (zf(2,nz) - zf(2,nz-1)) / (zf(1,nz) - zf(1,nz-1))
vertical_interp = zf(2,nz) + lapse_rate * (target_z - zf(1,nz))
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Upper-boundary extrap_type == 2 uses a lapse rate computed from the top two levels, which is mathematically equivalent to the existing extrap_type == 1 linear extrapolation. If lapse-rate is meant to behave differently from linear (e.g., fixed -6.5 K/km), this should be adjusted so the two extrapolation modes are not duplicates.

Copilot uses AI. Check for mistakes.
Comment on lines +6937 to +6939
! Lapse-rate extrapolation: calculate lapse rate from top two levels
lapse_rate = (zf(2,nz) - zf(2,nz-1)) / (zf(1,nz) - zf(1,nz-1))
vertical_interp = zf(2,nz) + lapse_rate * (target_z - zf(1,nz))
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

For extrap_type == 2 (lapse-rate) at the upper boundary, this change computes lapse_rate from the top two levels, which makes it effectively the same as extrap_type == 1 linear extrapolation. In this same function, the lower-boundary lapse-rate branch uses a fixed 0.0065 K/m (line 6926), so the upper and lower implementations are now inconsistent. Please align the two (either both fixed-lapse-rate or both gradient-derived) and ensure linear vs lapse-rate remain meaningfully different options.

Suggested change
! Lapse-rate extrapolation: calculate lapse rate from top two levels
lapse_rate = (zf(2,nz) - zf(2,nz-1)) / (zf(1,nz) - zf(1,nz-1))
vertical_interp = zf(2,nz) + lapse_rate * (target_z - zf(1,nz))
! Lapse-rate extrapolation: use fixed lapse rate (0.0065 K/m)
vertical_interp = zf(2,nz) - (target_z - zf(1,nz))*0.0065

Copilot uses AI. Check for mistakes.
@mgduda
Copy link
Copy Markdown
Contributor

mgduda commented Mar 24, 2026

@mrixlam While looking through the changes in this PR, I was reminded that we in fact have two vertical_interp functions in the init_atmosphere core: one in mpas_init_atm_vinterp.F and the other in mpas_init_atm_cases.F. Before considering the changes in this PR, we may try to refactor the init_atmosphere code so that there is only a single vertical_interp routine. After we've done this, I'll follow up here so that we can figure out next steps for this PR.

@mgduda
Copy link
Copy Markdown
Contributor

mgduda commented Mar 25, 2026

@mrixlam We've just merged PR #1429, which consolidates the two different definitions of the vertical_interp function. Would you be able to rework your changes based on merge commit 88ae401, modifying the vertical_interp function in src/core_init_atmosphere/mpas_init_atm_vinterp.F?

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +84 to +89
else if (extrap_type == 2) then
vertical_interp = zf(2,1) - (target_z - zf(1,1))*0.0065
n_lapse = min(nz, 3)
sum_x = 0.0_RKIND; sum_y = 0.0_RKIND
sum_x2 = 0.0_RKIND; sum_xy = 0.0_RKIND
do k = 1, n_lapse
sum_x = sum_x + zf(1,k)
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

The lapse-rate extrapolation for the lower boundary (target_z < zf(1,1)) used to apply a fixed standard lapse rate (0.0065 K/m). This change switches extrap_type==2 to a least-squares slope fit from the lowest 1–3 input levels, which is a behavioral change beyond the PR description (which focuses on missing upper boundary support). Please confirm this is intended; if not, keep the fixed lapse-rate behavior for the lower boundary (and implement the same for the upper boundary) or update the PR description/namelist documentation accordingly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

The hard-code 0.0065 K/m threshold is a field-specific constant that only produce a reasonable extrapolation when it is done for temperature in K. However, this is a field-agnostic function that is also used to extrapolate humidity, for example, which might produce unphysical extrapolated values. We will have a change in PR description before merging it.

Comment on lines +111 to +115
n_lapse = min(nz, 3)
sum_x = 0.0_RKIND; sum_y = 0.0_RKIND
sum_x2 = 0.0_RKIND; sum_xy = 0.0_RKIND
do k = nz - n_lapse + 1, nz
sum_x = sum_x + zf(1,k)
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

The least-squares slope calculation for extrap_type==2 is duplicated in both the lower- and upper-boundary extrapolation branches. Consider extracting the slope computation into a small helper routine/function (or a shared local block) so future adjustments (e.g., number of points, guarding against degenerate z spacing) don’t need to be made in two places.

Copilot uses AI. Check for mistakes.
@mrixlam mrixlam closed this Apr 29, 2026
@mrixlam mrixlam deleted the bugfix-vinterp-lapse-rate branch April 29, 2026 08:42
@mrixlam mrixlam restored the bugfix-vinterp-lapse-rate branch April 29, 2026 09:02
@mrixlam mrixlam reopened this Apr 29, 2026
@mrixlam mrixlam requested a review from Copilot April 29, 2026 09:22
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +138 to +157
real (kind=RKIND) :: sum_x, sum_y, sum_x2, sum_xy, n_real

n_pts = k_end - k_start + 1

sum_x = 0.0_RKIND; sum_y = 0.0_RKIND
sum_x2 = 0.0_RKIND; sum_xy = 0.0_RKIND

do k = k_start, k_end
sum_x = sum_x + zf(1,k)
sum_y = sum_y + zf(2,k)
sum_x2 = sum_x2 + zf(1,k)**2
sum_xy = sum_xy + zf(1,k)*zf(2,k)
end do

n_real = real(n_pts, RKIND)

if (n_pts > 1) then
ls_lapse_slope = (n_real*sum_xy - sum_x*sum_y) / (n_real*sum_x2 - sum_x**2)
else
ls_lapse_slope = 0.0_RKIND
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

ls_lapse_slope divides by (n_real*sum_x2 - sum_x**2) without guarding against a zero/near-zero denominator. If the selected z-levels contain duplicate (or nearly duplicate) zf(1,k) values, this will trigger a divide-by-zero (or overflow to Inf/NaN) and silently propagate into the extrapolated field. Please add a tolerance check on the denominator (and return a safe fallback slope, e.g., 0 or a 2-point difference), and consider computing the variance term using centered values to avoid catastrophic cancellation when RKIND is single precision.

Suggested change
real (kind=RKIND) :: sum_x, sum_y, sum_x2, sum_xy, n_real
n_pts = k_end - k_start + 1
sum_x = 0.0_RKIND; sum_y = 0.0_RKIND
sum_x2 = 0.0_RKIND; sum_xy = 0.0_RKIND
do k = k_start, k_end
sum_x = sum_x + zf(1,k)
sum_y = sum_y + zf(2,k)
sum_x2 = sum_x2 + zf(1,k)**2
sum_xy = sum_xy + zf(1,k)*zf(2,k)
end do
n_real = real(n_pts, RKIND)
if (n_pts > 1) then
ls_lapse_slope = (n_real*sum_xy - sum_x*sum_y) / (n_real*sum_x2 - sum_x**2)
else
ls_lapse_slope = 0.0_RKIND
real (kind=RKIND) :: sum_x, sum_y, mean_x, mean_y
real (kind=RKIND) :: dx, dy, var_x, cov_xy, tol, scale_x, denom_2pt
real (kind=RKIND) :: n_real
n_pts = k_end - k_start + 1
if (n_pts <= 1) then
ls_lapse_slope = 0.0_RKIND
return
end if
sum_x = 0.0_RKIND
sum_y = 0.0_RKIND
do k = k_start, k_end
sum_x = sum_x + zf(1,k)
sum_y = sum_y + zf(2,k)
end do
n_real = real(n_pts, RKIND)
mean_x = sum_x / n_real
mean_y = sum_y / n_real
var_x = 0.0_RKIND
cov_xy = 0.0_RKIND
scale_x = 0.0_RKIND
do k = k_start, k_end
dx = zf(1,k) - mean_x
dy = zf(2,k) - mean_y
var_x = var_x + dx*dx
cov_xy = cov_xy + dx*dy
scale_x = max(scale_x, abs(zf(1,k)))
end do
tol = max(1.0_RKIND, scale_x)**2 * epsilon(1.0_RKIND) * n_real
if (var_x > tol) then
ls_lapse_slope = cov_xy / var_x
else
denom_2pt = zf(1,k_end) - zf(1,k_start)
if (abs(denom_2pt) > tol) then
ls_lapse_slope = (zf(2,k_end) - zf(2,k_start)) / denom_2pt
else
ls_lapse_slope = 0.0_RKIND
end if

Copilot uses AI. Check for mistakes.
Comment on lines 77 to 95
if (target_z < zf(1,1)) then
if (extrap_type == 0) then
vertical_interp = zf(2,1)
else if (extrap_type == 1) then
slope = (zf(2,2) - zf(2,1)) / (zf(1,2) - zf(1,1))
vertical_interp = zf(2,1) + slope * (target_z - zf(1,1))
else if (extrap_type == 2) then
vertical_interp = zf(2,1) - (target_z - zf(1,1))*0.0065
n_lapse = min(nz, 3)
slope = ls_lapse_slope(zf, nz, 1, n_lapse)
vertical_interp = zf(2,1) + slope * (target_z - zf(1,1))
end if
return
end if
if (target_z >= zf(1,nz)) then
if (extrap_type == 0) then
vertical_interp = zf(2,nz)
else if (extrap_type == 1) then
slope = (zf(2,nz) - zf(2,nz-1)) / (zf(1,nz) - zf(1,nz-1))
vertical_interp = zf(2,nz) + slope * (target_z - zf(1,nz))
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

In the extrapolation branches, the linear extrapolation path assumes nz >= 2 (accesses zf(:,2) / zf(:,nz-1)). If the caller provides only a single vertical level (e.g., nz==1), this becomes an out-of-bounds access. Please add an explicit nz < 2 fallback (e.g., treat as constant extrapolation and/or set ierr + log an error) before computing the slope for extrap_type == 1.

Copilot uses AI. Check for mistakes.
Comment on lines 77 to 100
if (target_z < zf(1,1)) then
if (extrap_type == 0) then
vertical_interp = zf(2,1)
else if (extrap_type == 1) then
slope = (zf(2,2) - zf(2,1)) / (zf(1,2) - zf(1,1))
vertical_interp = zf(2,1) + slope * (target_z - zf(1,1))
else if (extrap_type == 2) then
vertical_interp = zf(2,1) - (target_z - zf(1,1))*0.0065
n_lapse = min(nz, 3)
slope = ls_lapse_slope(zf, nz, 1, n_lapse)
vertical_interp = zf(2,1) + slope * (target_z - zf(1,1))
end if
return
end if
if (target_z >= zf(1,nz)) then
if (extrap_type == 0) then
vertical_interp = zf(2,nz)
else if (extrap_type == 1) then
slope = (zf(2,nz) - zf(2,nz-1)) / (zf(1,nz) - zf(1,nz-1))
vertical_interp = zf(2,nz) + slope * (target_z - zf(1,nz))
else if (extrap_type == 2) then
call mpas_log_write('extrap_type == 2 not implemented for target_z >= zf(1,nz)', messageType=MPAS_LOG_ERR)
if (present(ierr)) ierr = 1
return
n_lapse = min(nz, 3)
slope = ls_lapse_slope(zf, nz, nz - n_lapse + 1, nz)
vertical_interp = zf(2,nz) + slope * (target_z - zf(1,nz))
end if
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

The extrapolation if/else if chain has no final else to handle unexpected extrap_type values. If a caller passes something other than {0,1,2}, vertical_interp can be returned uninitialized while ierr remains 0. Please add a default branch that logs an error and sets ierr (and/or falls back to constant extrapolation).

Copilot uses AI. Check for mistakes.
Comment on lines 83 to 87
else if (extrap_type == 2) then
vertical_interp = zf(2,1) - (target_z - zf(1,1))*0.0065
n_lapse = min(nz, 3)
slope = ls_lapse_slope(zf, nz, 1, n_lapse)
vertical_interp = zf(2,1) + slope * (target_z - zf(1,1))
end if
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

The PR description focuses on adding missing upper-boundary support for extrap_type==2, but this diff also changes the lower-boundary extrap_type==2 behavior from a fixed 0.0065 K/m to a least-squares fitted slope from the lowest levels. Please update the PR description (and any user-facing docs for config_extrap_airtemp='lapse-rate') to reflect this broader behavior change.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants