Skip to content

Compute the LCL from actual temperature in lfc/el used by cape_cin - #4067

Open
gaoflow wants to merge 3 commits into
Unidata:mainfrom
gaoflow:fix/lfc-el-lcl-virtual-temperature
Open

Compute the LCL from actual temperature in lfc/el used by cape_cin#4067
gaoflow wants to merge 3 commits into
Unidata:mainfrom
gaoflow:fix/lfc-el-lcl-virtual-temperature

Conversation

@gaoflow

@gaoflow gaoflow commented May 27, 2026

Copy link
Copy Markdown

Description

Fixes #3857.

cape_cin applies the virtual temperature correction by replacing the environmental
and parcel temperature profiles with their virtual temperatures before calling lfc
and el:

temperature = virtual_temperature_from_dewpoint(pressure, temperature, dewpoint)
parcel_profile = virtual_temperature(parcel_profile, parcel_mixing_ratio)
lfc_pressure, _ = lfc(pressure, temperature, dewpoint, parcel_temperature_profile=parcel_profile, ...)
el_pressure, _ = el(pressure, temperature, dewpoint, parcel_temperature_profile=parcel_profile, ...)

lfc and el then recompute the LCL from those arrayslfc from
parcel_temperature_profile[0], el from temperature[0] — i.e. they treat the
virtual temperature as if it were the actual temperature. Because virtual temperature
is always ≥ temperature, the LCL comes out at too low a pressure (too high an
altitude). Since the LFC and EL are floored at the LCL, both get biased upward; the
effect is strongest on the LFC, exactly as described in the issue.

Fix

Give lfc and el optional lcl_pressure (and, for lfc, lcl_temperature)
keywords so the caller can supply an LCL computed from the actual temperatures. When
the keywords are omitted the LCL is computed from the inputs exactly as before, so
direct callers and the public API are unchanged.

cape_cin already computes the LCL from the actual temperatures (for its below_lcl
mixing-ratio logic) before the virtual-temperature substitution, so it simply passes
that LCL through to lfc/el.

Effect

For a sounding whose LFC is floored at the LCL, the actual-temperature LCL sits about
30–45 hPa higher in pressure than the virtual one, so the LFC (and the CAPE integration
that starts there) is corrected accordingly. For example, on the profile in
test_cape_cin_value_error the surface-based CAPE goes from the previously-reported
2161.9 J/kg to 2206.7 J/kg once the LFC is floored at the correct LCL.

Tests

  • New test_lfc_el_lcl_from_actual_temperature: reconstructs the virtual-temperature
    profiles cape_cin builds and shows that, without the override, lfc floors at the
    spuriously-low virtual LCL, while supplying the actual-temperature LCL floors it at
    the correct LCL (and that el honors the keyword).
  • Updated the expected CAPE in test_cape_cin_value_error (whose LFC is floored at the
    LCL) with a comment explaining the change.

The full tests/calc/test_thermo.py suite passes (228 tests); ruff is clean.

@gaoflow
gaoflow requested a review from a team as a code owner May 27, 2026 21:45
@gaoflow
gaoflow requested review from dopplershift and removed request for a team May 27, 2026 21:45
@CLAassistant

CLAassistant commented May 27, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

gaoflow added 2 commits August 1, 2026 00:02
…ta#3857)

cape_cin replaces the environmental and parcel temperature profiles with virtual
temperatures before calling lfc and el. Those functions then recomputed the LCL
from the virtual temperatures, treating virtual temperature as actual temperature.
Because virtual temperature exceeds temperature, the LCL came out at too low a
pressure (too high an altitude), which biased the LFC and EL upward (they are
floored at the LCL).

Add optional lcl_pressure (and, for lfc, lcl_temperature) keywords to lfc and el so
the caller can supply an LCL computed from the actual temperatures. cape_cin already
computes the LCL from the actual temperatures before the virtual-temperature
substitution, so it now passes that LCL through. Direct callers are unaffected: when
the keywords are omitted the LCL is computed as before.

Add a regression test and update the expected CAPE in test_cape_cin_value_error,
whose LFC is floored at the LCL and so increases now that the correct LCL is used.
The lfc/el LCL fix changes the surface-based and most-unstable CAPE for the
sounding used in these docstring examples (their LFC is floored at the LCL,
which now sits at the correct, higher pressure), so refresh the expected
doctest output. Verified with the repo's own doctest command
(python -m pytest --doctest-modules -k 'not test' src): 89 passed.
Copilot AI review requested due to automatic review settings July 31, 2026 22:03
@gaoflow
gaoflow force-pushed the fix/lfc-el-lcl-virtual-temperature branch from 36034a6 to 2f566ac Compare July 31, 2026 22:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a CAPE/CIN bias introduced when cape_cin substitutes virtual temperatures before calling lfc/el, causing those functions to recompute the LCL using virtual temperatures as though they were actual temperatures (GH #3857).

Changes:

  • Add optional lcl_pressure (and lcl_temperature for lfc) parameters so callers can supply an LCL computed from actual temperatures.
  • Update cape_cin to compute the LCL before virtual-temperature substitution and pass it through to lfc/el.
  • Update and add tests to validate corrected CAPE expectations and the new LCL override behavior.

Reviewed changes

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

File Description
src/metpy/calc/thermo.py Adds LCL override kwargs to lfc/el and wires actual-temperature LCL through cape_cin to prevent virtual-temperature LCL bias.
tests/calc/test_thermo.py Updates CAPE expectation and adds regression coverage around LCL recomputation behavior.

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

Comment thread src/metpy/calc/thermo.py
Comment on lines +966 to +972
# Compute LCL for this parcel for future comparisons. Allow the caller to supply a
# precomputed LCL so it can be based on the actual (not virtual) temperatures when the
# temperature profiles passed in are virtual temperatures (see cape_cin).
if lcl_pressure is None:
this_lcl = lcl(pressure[0], parcel_temperature_profile[0], dewpoint_start)
else:
this_lcl = (lcl_pressure, lcl_temperature)
Comment thread tests/calc/test_thermo.py
Comment on lines +2432 to +2436
el_default, _ = el(pressure, temperature_v, dewpoint,
parcel_temperature_profile=profile_v)
el_with_lcl, _ = el(pressure, temperature_v, dewpoint,
parcel_temperature_profile=profile_v, lcl_pressure=lcl_pressure)
assert_almost_equal(el_default, el_with_lcl, 5)
lfc now raises if only one of lcl_pressure/lcl_temperature is supplied, matching its docstring. The el test supplies a LCL above the EL and asserts NaN, so the keyword cannot be silently ignored.
Copilot AI review requested due to automatic review settings August 1, 2026 00:42
@gaoflow

gaoflow commented Aug 1, 2026

Copy link
Copy Markdown
Author

Addressed both review notes: lfc now raises if only one of lcl_pressure/lcl_temperature is supplied (the docstring already required both), and the el test now proves the keyword is honored — a LCL above the EL forces NaN, so ignoring it would fail the test.

Copilot AI left a comment

Copy link
Copy Markdown

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 no new comments.

Suppressed comments (1)

src/metpy/calc/thermo.py:972

  • lfc documents that lcl_pressure must be given together with lcl_temperature, but the implementation does not enforce this. If a caller supplies only lcl_pressure, the function can return (lcl_pressure, None) when the LFC is floored at the LCL, breaking the documented return type and potentially causing downstream failures. Add an explicit validation that either both are provided or neither is.
    if (lcl_pressure is None) != (lcl_temperature is None):
        raise ValueError('lcl_pressure and lcl_temperature must be provided together')
    if lcl_pressure is None:
        this_lcl = lcl(pressure[0], parcel_temperature_profile[0], dewpoint_start)

Copilot AI review requested due to automatic review settings August 1, 2026 00:45

Copilot AI left a comment

Copy link
Copy Markdown

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 no new comments.

Suppressed comments (3)

src/metpy/calc/thermo.py:1090

  • el introduces a new lcl_pressure keyword but it isn’t validated by @check_units, so passing an incorrect type/dimensionality will raise less-informative errors at comparison time. Add a named unit check for lcl_pressure.
@check_units('[pressure]', '[temperature]', '[temperature]', '[temperature]')
def el(pressure, temperature, dewpoint, parcel_temperature_profile=None, which='top',
       lcl_pressure=None):

src/metpy/calc/thermo.py:974

  • When the caller supplies lcl_pressure/lcl_temperature, lfc returns them verbatim if the LFC is floored at the LCL. If they’re provided in different units than the input pressure/temperature, the return units become inconsistent with the rest of the function (and with the default code path where lcl() is called with input units). Coerce supplied LCL values to pressure.units/temperature.units before using/returning them.
    if lcl_pressure is None:
        this_lcl = lcl(pressure[0], parcel_temperature_profile[0], dewpoint_start)
    else:
        this_lcl = (lcl_pressure, lcl_temperature)

src/metpy/calc/thermo.py:850

  • New keyword arguments lcl_pressure/lcl_temperature on lfc aren’t covered by @check_units, so callers can pass unitless or wrong-dimensionality values and get confusing runtime failures later. Add named checks for these new parameters to keep behavior consistent with the rest of MetPy’s unit-validated API.

This issue also appears in the following locations of the same file:

  • line 971
  • line 1088
@check_units('[pressure]', '[temperature]', '[temperature]', '[temperature]')
def lfc(pressure, temperature, dewpoint, parcel_temperature_profile=None, dewpoint_start=None,
        which='top', lcl_pressure=None, lcl_temperature=None):

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.

Virtual temperature being incorrectly used in lfc and el functions

3 participants