advection.advect_detect¶
Functions for detecting horizontal and vertical advection.
- advection.advect_detect.apply_advection_correction(main_data, H_adv, V_adv, HA_Q=None, rn_min=75.0)[source]
Fold measured advective fluxes into the surface energy balance.
Energy-balance bookkeeping (Moderow et al. 2021; Wang et al. 2024)¶
The advection-augmented surface energy balance, written in the Moderow et al. (2021) OUT-positive convention (positive flux = energy out of the control volume), places the advective terms on the turbulent-sum side alongside
HandLE– the available-energy side(Rn - G)is left untouched:Rn - G = H + LE + HA_T + HA_Q + VAT
so the corrected turbulent + advective sum is:
(H + LE)_corrected = H + LE + HA_T + HA_Q + VAT
Each advective term carries its own sign from the upstream computation (
compute_advection_fluxes()): a negative term is energy advected INTO the field (the oasis fingerprint), a positive term is energy advected OUT. This function neither re-signs nor re-derives the terms – it simply adds them on the turbulent side. The residual is reported in theCLAUDE.mdconventionResidual = Rn - G - H - LE(positive = available energy exceeds the turbulent sum), so the correction moves a gated step’s residual toward zero.Conditional inclusion gate (Wang et al. 2024)¶
The advective fluxes are folded in only at timesteps where both Wang’s conditions hold (this is what lifted closure from 89 % to 97 % in the alfalfa study):
Rn > rn_min(default 75 W/m^2) – sufficient daytime radiative forcing, AND(H + LE) < (Rn - G)– the (spectrally-corrected) turbulent sum is below the available energy, i.e. there is an under-closure gap for the advected energy to fill.
Where the gate fails (night, low
Rn, or already-over-closed steps) the timestep is left exactly uncorrected:(H + LE)_corrected == H + LE. The boolean'included'mask records which timesteps were corrected.NaNhandling¶A
NaNin any ofRn/G/H/LEmakes the gate comparisonsFalsefor that step, so it is left uncorrected (never silently forced closed). ANaNin an advective term contributes 0 to the corrected sum (an unmeasured advective component adds nothing) rather than poisoning the whole corrected sum withNaN.- param main_data:
Main-tower energy balance. Required keys
'H','LE','Rn','G'[all W/m^2], each a length-nseries.- type main_data:
dict
- param H_adv:
Horizontal heat advection
HA_T[W/m^2] (Wang Eq. 5a; the'H_adv'/'HA_T'output ofcompute_advection_fluxes()).- type H_adv:
array-like
- param V_adv:
Vertical heat advection
VAT[W/m^2] (Wang Eq. 6; the'V_adv'/'VAT'output ofcompute_advection_fluxes()).None(e.g. a horizontal-only call) is treated as all-zero (no vertical advection).- type V_adv:
array-like or None
- param HA_Q:
Horizontal moisture advection
HA_Q[W/m^2] (Wang Eq. 5b).None(default) is treated as all-zero, preserving the legacy three-argument call signature.- type HA_Q:
array-like or None, optional
- param rn_min:
Net-radiation threshold [W/m^2] for Wang’s gate condition (1). Default 75 W/m^2.
- type rn_min:
float, optional
- returns:
Keys:
'Rn','G','H','LE': the input series (float arrays).'HA_T'/'H_adv': horizontal heat advection [W/m^2].'HA_Q': horizontal moisture advection [W/m^2].'VAT'/'V_adv': vertical heat advection [W/m^2].'H_plus_LE_orig': uncorrected turbulent sumH + LE[W/m^2].'H_plus_LE_corrected': turbulent sum with the gated advective terms folded in [W/m^2].'available_energy':Rn - G[W/m^2] (unchanged by the correction; the target the turbulent sum is compared against).'residual_orig':Rn - G - H - LE[W/m^2] before correction.'residual_corrected':Rn - G - (H + LE)_corrected[W/m^2] after correction.'included': boolean mask,Truewhere Wang’s gate passed and the advective terms were applied.
- rtype:
dict
References
Moderow et al. (2021), OUT-positive sign convention and advection terms. Wang et al. (2024), conditional-inclusion rule and Eqs. 5a/5b/6.
- advection.advect_detect.compute_advection_fluxes(main_data, upwind_data=None, detect_horizontal=None, detect_vertical=None, tower_distance=None)[source]
Compute horizontal and vertical advection flux series for closure.
The horizontal terms are the physical gradient-based advection of Wang et al. (2024), Eqs. 5a/5b (Moderow et al. 2021, Term IV) – not a difference of two sensible-heat fluxes:
HA_T = rho * Cp * u_bar * (dT/dx) * (zm - h) # W/m^2 (Eq. 5a) HA_Q = rho * lambda * u_bar * (dq/dx) * (zm - h) # W/m^2 (Eq. 5b)
where
rhois moist-air density (air_density()),Cpthe moist-air specific heat (specific_heat_moist_air()),lambdathe latent heat of vaporization (latent_heat_vaporization()) – all evaluated from the main-tower state(P, T, q)–u_barthe mean horizontal wind speed [m/s], and(zm - h)the depth [m] of the air layer between the canopy/lower heighthand the measurement heightzm.Vertical mean-advection term (replaces the old residual fallback)¶
When the caller supplies the planar-fit mean vertical velocity
w_bar(or adetect_verticalmask), the vertical heat advection is computed as the measured Lee (1998) / Wang et al. (2024) Eq. 6 term:VAT = rho * Cp * w_bar * (T_zm - <T>) # W/m^2 (Eq. 6)
where
T_zmis the air temperature at the measurement height (the main tower'T') and<T> = (1/zm) integral T dzis the column-mean temperature ('T_col'or a'T_profile'/'z_profile'pair). This is not a closure residual:w_barmust be the planar-fit / tilt-corrected mean vertical velocity, never the raw sonicw(which is tilt-biased). If vertical advection is requested butw_baror the column-mean temperature is missing, the function raises rather than falling back to the energy-balance residual (CLAUDE.mdhard rule: never compute an advection term as the energy-balance residual).Optionally, when two-level kinematic heat fluxes
'wT_zm'and'wT_h'[K m/s] are provided, the vertical heat-flux divergence (Wang Eq. 12) is also returned:VFD_T = -rho * Cp * (wT|zm - wT|h) # W/m^2 (Eq. 12)
Diagnostic residual (NOT an advective flux)¶
The energy-balance closure imbalance
residual = (H + LE) - (Rn - G)is still computed and returned, but only as a closure diagnostic – it is not an advective flux and must never be added to the budget as one.Sign derivation (read carefully)¶
Let the streamwise coordinate
xincrease downwind, i.e. point from the upwind tower toward the main tower. The along-wind temperature gradient is approximated by the two-tower finite difference:dT/dx ~= (T_main - T_upwind) / tower_distance # K/m
In the oasis case warm dry air sits upwind of a cool transpiring field, so
T_upwind > T_mainand thereforedT/dx < 0. Withu_bar > 0and(zm - h) > 0this makesHA_T < 0. Under the Moderow et al. (2021) convention (positive = energy OUT of the control volume), a negativeHA_Tmeans sensible-heat energy is advected INTO the field – exactly the expected oasis behaviour. DefiningdT/dxas(T_main - T_upwind)/tower_distancetherefore yields the correct into-field (negative) sign with no extra negation; warmer upwind air givesHA_T < 0. The moisture gradient uses the identical downwind-positive convention,dq/dx = (q_main - q_upwind)/tower_distance, so drier upwind air (q_upwind < q_main) givesdq/dx > 0andHA_Q > 0– the drying signal of dry-air advection.For the vertical term the canonical oasis has a stable internal boundary layer (cool wet surface, warm air aloft), so the measurement-height temperature exceeds the column mean (
T_zm > <T>, i.e.T_zm - <T> > 0) while the planar-fit mean motion is subsidence (w_bar < 0). ThenVAT = rho*Cp*w_bar*(T_zm - <T>) < 0: warm air is carried DOWN into the control volume, i.e. heat INTO the field – negative under the OUT-positive convention, the expected oasis vertical-advection sign.Finally the diagnostic
residualis defined here as(H + LE) - (Rn - G)– the negative of theRn - G - H - LEimbalance written in CLAUDE.md, chosen so a positive value flags net energy advected IN (the legacyadv_inreading). In the oasis the latent flux exceeds the available energy (EF = LE/(Rn-G) > 1, the advective-input fingerprint), soH + LE > Rn - Gandresidual > 0. It is a closure diagnostic, never an advective flux.Required inputs (a missing one RAISES, never silently returns zero)¶
Both
main_dataand each upwind tower must carry the fields the gradient terms need;tower_distance(or a per-tower'distance') is required. If any required field is missing aValueErroris raised rather than producing a meaningless zero or a flux difference.- param main_data:
Main-tower series. Required keys:
'H': sensible heat flux [W/m^2]'LE': latent heat flux [W/m^2]. Open-pathLEis assumed already WPL (Webb-Pearman-Leuning 1980) density-corrected — a mandatory separate pre-step, not an advection fix (seeadvection.advection.wpl_latent_heat_flux()).'Rn': net radiation [W/m^2]'G': ground heat flux [W/m^2]'T': air temperature [°C or K]'q'(or'RH'[%]) : specific humidity [kg/kg]'u'(or'wind_speed') : mean horizontal wind speedu_bar[m/s]'zm': measurement height [m]'h': canopy / lower height [m] (must satisfyzm > h)
Optional keys:
'P'ambient pressure [Pa] (default 101325),'wind_dir'[deg from north] for multi-tower fetch selection.Optional vertical-advection keys (needed only when
w_barordetect_verticalis supplied; missing required ones then raise):'w_bar'(aliases'w_planar','w_mean') : planar-fit / tilt-corrected mean vertical velocity [m/s]. Never the raw sonicw. Required to computeVAT.'T_col'(aliases'T_column_mean','T_mean_col') : column-mean temperature<T>[°C or K]; or'T_profile'+'z_profile'(temperatures at heights [m]) from which<T>is approximated by trapezoidal integration. Required to computeVAT.'wT_zm'and'wT_h': two-level kinematic heat fluxes [K m/s] at the measurement and canopy heights. When both are present the optional vertical heat-flux divergenceVFD_T(Eq. 12) is returned.
'T','q'/'RH','u','zm','h','P','w_bar','T_col','wT_zm','wT_h'may each be a scalar (broadcast) or a length-nseries.- type main_data:
dict
- param upwind_data:
Upwind tower(s). Each must carry
'T'and'q'(or'RH') for the gradients; may carry'bearing'[deg from north] for fetch selection and a per-tower'distance'[m] overridingtower_distance. Required – horizontal advection cannot be computed without an upwind reference.- type upwind_data:
dict or list of dicts
- param detect_horizontal:
Boolean mask from
detect_horizontal_advection(). WhereFalse,HA_TandHA_Qare set to 0 for that step (no advection event). WhereNone(default), the gradient terms are computed at every step.- type detect_horizontal:
np.ndarray, optional
- param detect_vertical:
Boolean mask from
detect_vertical_advection(). Supplying it engages the vertical term:VATis then required (sow_barand the column-mean temperature must be present, else aValueErroris raised), and where the mask isFalseVATis set to 0 for that step (no vertical-advection event).- type detect_vertical:
np.ndarray, optional
- param tower_distance:
Separation [m] between the main and upwind tower, used as
dxin the gradients. Required unless every upwind dict supplies its own'distance'.- type tower_distance:
float, optional
- returns:
Keys:
'HA_T': horizontal heat advection [W/m^2] (Eq. 5a). Negative = INTO the field (oasis, warm upwind air). length-narray.'HA_Q': horizontal moisture (latent-energy) advection [W/m^2] (Eq. 5b). length-narray.'H_adv': alias of'HA_T'[W/m^2], kept for backward compatibility.'VAT': measured vertical heat advectionrho*Cp*w_bar*(T_zm - <T>)[W/m^2] (Lee 1998; Wang Eq. 6) as a length-narray, orNonewhen no vertical inputs were supplied (horizontal-only call). This is the proper mean-advection term, not a closure residual.'V_adv': backward-compatible alias of'VAT'. (Previously a closure-residual estimate; it is now the measured Eq. 6 term.)'VFD_T': vertical heat-flux divergence-rho*Cp*(wT|zm - wT|h)[W/m^2] (Wang Eq. 12) as a length-narray when both'wT_zm'and'wT_h'were supplied; otherwiseNone.'residual': diagnostic energy-balance closure imbalance(H + LE) - (Rn - G)[W/m^2], length-narray. This is a closure diagnostic, NOT an advective flux – never add it to the budget as advection.'adv_in': deprecated alias of'residual'(the old, misleadingly named key). Identical values.
- rtype:
dict
- raises ValueError:
If
upwind_datais omitted, iftower_distance(and a per-tower'distance') is missing, if any required gradient field ('T','q'/'RH','u','zm','h') is absent, or if the layer depthzm - his not strictly positive. Additionally, when the vertical term is engaged (w_barordetect_verticalsupplied), if the planar-fitw_baror the column-mean temperature (T_col/T_profile) is missing – the vertical term is never back-filled from the energy-balance residual.
References
Wang et al. (2024), Eqs. 5a, 5b, 6 and 12. Moderow et al. (2021), Term IV and the OUT-positive sign convention. Lee (1998), vertical mean-advection term.
- advection.advect_detect.detect_horizontal_advection(main_flux, *, rn=None, g=None, le_main=None, temp_main=None, temp_upwind=None, humidity_main=None, humidity_upwind=None, wind_dir=None, upwind_dir=None, upwind_flux=None, rn_high=50.0, h_neg_threshold=0.0, ef_tol=1.05, temp_diff_threshold=1.0, humidity_diff_threshold=0.001, wind_sector_deg=45.0, upwind_h_excess=20.0, return_components=False)[source]
Detect periods of horizontal advection influencing the main tower.
Fully vectorized (no Python per-timestep loop) and built from the documented oasis-regime detection signals. Each signal is an independent, parameterized boolean flag; the returned mask is their logical OR. Missing data is carried as
np.nanand excluded from every comparison vianumpy.isnan()(anx is Nonetest, used by the previous version, never fires on a floatndarrayand so silently failed to mask gaps).Detection signals¶
Negative midday sensible heat / negative Bowen ratio (oasis fingerprint; Wang 2024 §2.2). Fires where
H < h_neg_thresholdwhileRn > rn_high– warm dry air advected onto a cool transpiring surface drives a downward (negative)Hin full daytime. Needsmain_fluxandrn.Evaporative fraction ``EF = LE / (Rn - G) > 1`` (advective input; synthesis §2.3). Fires where
LE > (Rn - G) * ef_toland the available energyRn - Gis positive (EFis only meaningful by day). Needsle_main,rnandg.Horizontal temperature / humidity gradient between the main and an upwind tower (warm and/or dry air upwind). The warm-upwind flag fires where
temp_upwind > temp_main + temp_diff_threshold; the dry-upwind flag wherehumidity_main - humidity_upwind > humidity_diff_threshold(main moister than upwind). Both are evaluated only when the wind is blowing from the upwind tower – i.e.wind_diris within±wind_sector_degof the bearingupwind_dir(a ±45° sector by default; ±20° is a common stricter choice). If eitherwind_dirorupwind_diris omitted the sector gate is not applied.
In addition, when an upwind sensible-heat series
upwind_fluxis supplied, an upwind sensible-heat excess flag fires whereupwind_flux > main_flux + upwind_h_excess(also subject to the wind-sector gate).upwind_h_excessis an absolute W/m^2 difference – this is the single, named, documented threshold that replaces the previous version’s contradiction between a docstring that said “20% higher” and code that added an absolute20 W/m^2. The absolute reading is the one kept.- param main_flux:
Sensible heat flux
Hat the main tower [W/m^2].- type main_flux:
array-like
- param rn:
Net radiation
Rnat the main site [W/m^2].- type rn:
array-like, optional
- param g:
Soil/ground heat flux
Gat the main site [W/m^2].- type g:
array-like, optional
- param le_main:
Latent heat flux
LEat the main tower [W/m^2].- type le_main:
array-like, optional
- param temp_main:
Air temperature at the main and upwind towers [°C or K] (same units).
- type temp_main:
array-like, optional
- param temp_upwind:
Air temperature at the main and upwind towers [°C or K] (same units).
- type temp_upwind:
array-like, optional
- param humidity_main:
Specific humidity at the main and upwind towers [kg/kg].
- type humidity_main:
array-like, optional
- param humidity_upwind:
Specific humidity at the main and upwind towers [kg/kg].
- type humidity_upwind:
array-like, optional
- param wind_dir:
Wind direction at the main tower [deg from north].
- type wind_dir:
array-like, optional
- param upwind_dir:
Bearing from the main tower toward the upwind tower [deg from north]. Together with
wind_dirthis gates the gradient and upwind-flux signals to a±wind_sector_degsector about the fetch direction.- type upwind_dir:
float, optional
- param upwind_flux:
Sensible heat flux
Hat an upwind reference tower [W/m^2].- type upwind_flux:
array-like, optional
- param rn_high:
Net-radiation threshold [W/m^2] above which a period is treated as midday for the negative-
Hsignal.- type rn_high:
float, default 50.0
- param h_neg_threshold:
Sensible-heat threshold [W/m^2];
Hbelow this (whileRn > rn_high) flags the oasis negative-Hsignal.- type h_neg_threshold:
float, default 0.0
- param ef_tol:
Evaporative-fraction tolerance.
LE > (Rn - G) * ef_tolflags advective input; the 5 % margin guards against measurement noise.- type ef_tol:
float, default 1.05
- param temp_diff_threshold:
Minimum upwind-minus-main air-temperature excess [°C or K] for the warm-upwind gradient flag.
- type temp_diff_threshold:
float, default 1.0
- param humidity_diff_threshold:
Minimum main-minus-upwind specific-humidity excess [kg/kg] for the dry-upwind gradient flag.
- type humidity_diff_threshold:
float, default 0.001
- param wind_sector_deg:
Half-width [deg] of the wind-direction sector about
upwind_dirwithin which the gradient and upwind-flux signals are evaluated.- type wind_sector_deg:
float, default 45.0
- param upwind_h_excess:
Absolute amount [W/m^2] by which
upwind_fluxmust exceedmain_fluxto flag an upwind sensible-heat excess. Absolute W/m^2, not a percentage.- type upwind_h_excess:
float, default 20.0
- param return_components:
If True, also return a dict mapping each criterion name to its boolean mask (plus
'wind_aligned'), suitable forpandas.DataFrame(...).- type return_components:
bool, default False
- returns:
numpy.ndarray – Boolean mask (length
n); True where any signal detects advection.dict, optional – Returned only when
return_componentsis True, as(mask, components). Keys:'negative_H','ef_gt_1','warm_upwind','dry_upwind','upwind_H_excess'and'wind_aligned'(the sector gate), each a length-nboolean array.
References
Wang et al. (2024) §2.2 (negative Bowen / oasis fingerprint) and the EF > 1 advective-input synthesis §2.3. Moderow et al. (2021) OUT-positive sign convention.
- advection.advect_detect.detect_vertical_advection(vertical_w=None, *, temp_profile_lower=None, temp_profile_upper=None, main_H=None, rn=None, g=None, w_threshold=0.05, rn_g_threshold=50.0, temp_grad_threshold=0.5, h_anomaly_threshold=20.0, use_h_anomaly=True, return_components=False)[source]
Detect periods of energetically significant vertical heat advection.
Centred on the planar-fit mean vertical velocity
w_bar(Lee 1998), not on the ad hoc “inverted profile” heuristic the previous version used as a gate. Lee (1998) shows that as|w_bar|approaches ~0.05 m/s the vertical advection term\[VAT \approx \rho\, C_p\, \bar{w}\, (T_{zm} - \langle T\rangle)\]becomes energetically significant (of order
-100 W/m^2at midday in the oasis regime; CLAUDE.md “Vertical heat advection”, Wang 2024 Eq. 6). A small mean vertical velocity is therefore, on its own, sufficient grounds to flag a period – it no longer has to co-occur with a temperature inversion.Fully vectorized (no per-timestep Python loop). Missing data is carried as
np.nanand excluded from every comparison withnumpy.isnan()(anx is Nonetest, used by the previous version, never fires on a floatndarrayand so silently failed to mask gaps). The returned mask is the logical OR of the independent signals below.Warning
vertical_wmust be the planar-fit (or otherwise tilt-corrected) mean vertical velocityw_bar. The raw sonicwmust not be used: instrument tilt relative to the mean streamline biaseswby far more than the ~0.05 m/s threshold here, so an uncorrectedwmakes this detector fire on tilt, not on real subsidence/uplift.Detection signals¶
Primary – significant mean vertical velocity (Lee 1998). Fires where
|w_bar| > w_threshold(default 0.05 m/s). This is the dominant signal and the basis of the rewrite. Needsvertical_w.Supporting – daytime vertical temperature gradient of the advective sign. Fires where the period is daytime (
Rn - G > rn_g_threshold) and temperature increases with height (temp_profile_upper > temp_profile_lower + temp_grad_threshold). Warm air aloft over a cooler surface gives(T_zm - <T>) > 0; paired with the typical oasis subsidence (w_bar < 0) this yields a negative (downward, energy-IN)VAT– the oasis advective sign under the Moderow 2021 OUT-positive convention. Needs the two temperatures plusrnandg.Optional, weak/secondary – anomalous daytime sensible heat. Fires where it is daytime and
main_H < h_anomaly_threshold(very low or downwardHwhen it would normally be positive). This is a screening heuristic only: anomalously low daytimeHhas many possible causes (cloud, advection, instrument issues), so it is corroborating evidence at best, never a quantitativeVAT. Disable it withuse_h_anomaly=False.
- param vertical_w:
Planar-fit mean vertical velocity
w_bar[m/s] – see the warning above. Scalars are broadcast. The primary detection signal.- type vertical_w:
array-like, optional
- param temp_profile_lower:
Temperature near the surface/canopy [°C or K].
- type temp_profile_lower:
array-like, optional
- param temp_profile_upper:
Temperature at (or above) the measurement height [°C or K], same units as
temp_profile_lower.- type temp_profile_upper:
array-like, optional
- param main_H:
Sensible heat flux
Hat the main tower [W/m^2] (H-anomaly signal).- type main_H:
array-like, optional
- param rn:
Net radiation
Rn[W/m^2] (daytime gate).- type rn:
array-like, optional
- param g:
Soil/ground heat flux
G[W/m^2] (daytime gate).- type g:
array-like, optional
- param w_threshold:
Magnitude of
w_bar[m/s] above which vertical advection is treated as energetically significant (Lee 1998).- type w_threshold:
float, default 0.05
- param rn_g_threshold:
Available-energy threshold
Rn - G[W/m^2] separating daytime from nighttime/low-energy periods for the supporting and H-anomaly signals.- type rn_g_threshold:
float, default 50.0
- param temp_grad_threshold:
Minimum upper-minus-lower temperature difference [°C or K] counted as a gradient of the advective sign (warm air aloft).
- type temp_grad_threshold:
float, default 0.5
- param h_anomaly_threshold:
Daytime
H[W/m^2] below which the weak H-anomaly signal fires.- type h_anomaly_threshold:
float, default 20.0
- param use_h_anomaly:
Whether to include the weak/secondary H-anomaly signal in the mask.
- type use_h_anomaly:
bool, default True
- param return_components:
If
Truealso return the per-signal boolean arrays (see Returns).- type return_components:
bool, default False
- returns:
numpy.ndarray – Boolean mask of detected vertical-advection periods (length matches the input series).
tuple of (numpy.ndarray, dict) – If
return_componentsisTrue,(mask, components)with keys'w_significant','temp_gradient','supporting','h_anomaly'and'daytime'– each a length-nboolean array.
References
Lee, X. (1998), on the significance of planar-fit mean vertical velocity for vertical advection. Wang et al. (2024) Eq. 6 (
VAT). Moderow et al. (2021) OUT-positive sign convention.