Compute the LCL from actual temperature in lfc/el used by cape_cin - #4067
Compute the LCL from actual temperature in lfc/el used by cape_cin#4067gaoflow wants to merge 3 commits into
Conversation
…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.
36034a6 to
2f566ac
Compare
There was a problem hiding this comment.
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(andlcl_temperatureforlfc) parameters so callers can supply an LCL computed from actual temperatures. - Update
cape_cinto compute the LCL before virtual-temperature substitution and pass it through tolfc/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.
| # 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) |
| 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.
|
Addressed both review notes: |
There was a problem hiding this comment.
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
lfcdocuments thatlcl_pressuremust be given together withlcl_temperature, but the implementation does not enforce this. If a caller supplies onlylcl_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)
There was a problem hiding this comment.
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
elintroduces a newlcl_pressurekeyword 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 forlcl_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,lfcreturns them verbatim if the LFC is floored at the LCL. If they’re provided in different units than the inputpressure/temperature, the return units become inconsistent with the rest of the function (and with the default code path wherelcl()is called with input units). Coerce supplied LCL values topressure.units/temperature.unitsbefore 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_temperatureonlfcaren’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):
Description
Fixes #3857.
cape_cinapplies the virtual temperature correction by replacing the environmentaland parcel temperature profiles with their virtual temperatures before calling
lfcand
el:lfcandelthen recompute the LCL from those arrays —lfcfromparcel_temperature_profile[0],elfromtemperature[0]— i.e. they treat thevirtual 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
lfcandeloptionallcl_pressure(and, forlfc,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_cinalready computes the LCL from the actual temperatures (for itsbelow_lclmixing-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_errorthe surface-based CAPE goes from the previously-reported2161.9 J/kg to 2206.7 J/kg once the LFC is floored at the correct LCL.
Tests
test_lfc_el_lcl_from_actual_temperature: reconstructs the virtual-temperatureprofiles
cape_cinbuilds and shows that, without the override,lfcfloors at thespuriously-low virtual LCL, while supplying the actual-temperature LCL floors it at
the correct LCL (and that
elhonors the keyword).test_cape_cin_value_error(whose LFC is floored at theLCL) with a comment explaining the change.
The full
tests/calc/test_thermo.pysuite passes (228 tests);ruffis clean.