2.3 Stability & Convection

Atmospheric stability determines whether air parcels rise or sink. Unstable conditions lead to convection, clouds, and thunderstorms; stable conditions suppress vertical motion.

Static Stability

Absolutely Stable

Environmental lapse rate < moist adiabatic rate

Γ < Γm ≈ 6°C/km: Displaced parcel returns to original position

Absolutely Unstable

Environmental lapse rate > dry adiabatic rate

Γ > Γd = 9.8°C/km: Parcel accelerates away from original position

Conditionally Unstable

Γm < Γ < Γd

Most common state: stable for dry air, unstable for saturated air

CAPE & CIN

CAPE (Convective Available Potential Energy)

$$CAPE = \int_{LFC}^{EL} g \frac{T_{parcel} - T_{env}}{T_{env}} dz$$

Positive buoyancy: energy available for convection (J/kg)

  • • <1000: Weak instability
  • • 1000-2500: Moderate
  • • >2500: Strong (severe weather)

CIN (Convective Inhibition)

$$CIN = \int_{SFC}^{LFC} g \frac{T_{parcel} - T_{env}}{T_{env}} dz$$

Negative buoyancy: energy barrier to convection (J/kg)

  • • <50: Weak cap
  • • 50-200: Moderate cap
  • • >200: Strong cap (inhibits storms)

Python: Stability Analysis

#!/usr/bin/env python3
"""
stability_convection.py - Atmospheric stability and CAPE calculation

Run: python3 stability_convection.py
Requires: pip install numpy matplotlib
"""
import numpy as np
import matplotlib.pyplot as plt

# Constants
g = 9.81        # m/s²
R_d = 287.0     # J/(kg·K)
c_p = 1005.0    # J/(kg·K)
L_v = 2.5e6     # J/kg

# Lapse rates
Gamma_d = g / c_p * 1000  # dry adiabatic lapse rate (K/km) ≈ 9.8
Gamma_m = 6.0             # typical moist adiabatic (K/km)

def saturation_vapor_pressure(T_celsius):
    """Tetens formula (hPa)"""
    return 6.11 * np.exp(17.27 * T_celsius / (T_celsius + 237.3))

def parcel_ascent(T_surface, p_surface, Td_surface, z_max=15000, dz=100):
    """
    Lift a parcel and calculate temperatures
    Returns: z, T_parcel, T_env, LCL, LFC, EL
    """
    z = np.arange(0, z_max, dz)
    T_parcel = np.zeros_like(z, dtype=float)
    T_env = np.zeros_like(z, dtype=float)

    # Initial conditions
    T_parcel[0] = T_surface
    T_env[0] = T_surface
    Td = Td_surface

    # Environmental lapse rate (conditionally unstable)
    Gamma_env = 7.0  # K/km

    lcl_z = None
    lfc_z = None
    el_z = None
    saturated = False

    for i in range(1, len(z)):
        # Environmental temperature
        T_env[i] = T_surface - Gamma_env * z[i] / 1000

        # Parcel temperature
        if not saturated:
            # Dry adiabatic ascent
            T_parcel[i] = T_parcel[i-1] - Gamma_d * dz / 1000
            # Check for saturation (rough approximation)
            # LCL: T_parcel ≈ Td
            Td_at_z = Td_surface - 1.8 * z[i] / 1000  # dew point lapse
            if T_parcel[i] <= Td_at_z + 273.15 and lcl_z is None:
                lcl_z = z[i]
                saturated = True
        else:
            # Moist adiabatic ascent
            T_parcel[i] = T_parcel[i-1] - Gamma_m * dz / 1000

        # Find LFC (Level of Free Convection)
        if saturated and T_parcel[i] > T_env[i] and lfc_z is None:
            lfc_z = z[i]

        # Find EL (Equilibrium Level)
        if lfc_z is not None and T_parcel[i] < T_env[i] and el_z is None:
            el_z = z[i]

    return z, T_parcel, T_env, lcl_z, lfc_z, el_z

# Surface conditions
T_sfc = 30 + 273.15  # K
p_sfc = 1013.25      # hPa
Td_sfc = 20          # °C

z, T_parcel, T_env, lcl, lfc, el = parcel_ascent(T_sfc, p_sfc, Td_sfc)

# Calculate CAPE and CIN
cape = 0
cin = 0
for i in range(1, len(z)):
    dz = z[i] - z[i-1]
    buoyancy = g * (T_parcel[i] - T_env[i]) / T_env[i]

    if lfc and z[i] > lfc and (el is None or z[i] < el):
        if buoyancy > 0:
            cape += buoyancy * dz
    elif z[i] < (lfc or 1e10):
        if buoyancy < 0:
            cin += abs(buoyancy) * dz

# Plot
fig, axes = plt.subplots(1, 2, figsize=(14, 8))

# Skew-T style plot
ax1 = axes[0]
ax1.plot(T_env - 273.15, z/1000, 'r-', linewidth=2, label='Environment')
ax1.plot(T_parcel - 273.15, z/1000, 'b-', linewidth=2, label='Parcel')

# Fill CAPE and CIN regions
for i in range(1, len(z)):
    if lfc and z[i] > lfc and (el is None or z[i] < el):
        if T_parcel[i] > T_env[i]:
            ax1.fill_betweenx([z[i-1]/1000, z[i]/1000],
                             [T_env[i-1]-273.15, T_env[i]-273.15],
                             [T_parcel[i-1]-273.15, T_parcel[i]-273.15],
                             color='red', alpha=0.3)

if lcl:
    ax1.axhline(y=lcl/1000, color='green', linestyle='--', label=f'LCL: {lcl/1000:.1f} km')
if lfc:
    ax1.axhline(y=lfc/1000, color='orange', linestyle='--', label=f'LFC: {lfc/1000:.1f} km')
if el:
    ax1.axhline(y=el/1000, color='purple', linestyle='--', label=f'EL: {el/1000:.1f} km')

ax1.set_xlabel('Temperature (°C)', fontsize=12)
ax1.set_ylabel('Height (km)', fontsize=12)
ax1.set_title('Parcel Ascent and Stability', fontsize=14)
ax1.legend(loc='upper right')
ax1.grid(True, alpha=0.3)
ax1.set_xlim(-60, 40)
ax1.set_ylim(0, 15)

# Stability regimes
ax2 = axes[1]
gamma_env_range = np.linspace(4, 12, 100)
colors = []
for g in gamma_env_range:
    if g < Gamma_m:
        colors.append('green')   # Absolutely stable
    elif g > Gamma_d:
        colors.append('red')     # Absolutely unstable
    else:
        colors.append('yellow')  # Conditionally unstable

ax2.scatter(gamma_env_range, np.ones_like(gamma_env_range), c=colors, s=100)
ax2.axvline(x=Gamma_m, color='green', linestyle='--', linewidth=2, label=f'Γ_m = {Gamma_m} K/km')
ax2.axvline(x=Gamma_d, color='red', linestyle='--', linewidth=2, label=f'Γ_d = {Gamma_d:.1f} K/km')
ax2.set_xlabel('Environmental Lapse Rate (K/km)', fontsize=12)
ax2.set_title('Stability Regimes', fontsize=14)
ax2.set_xlim(4, 12)
ax2.set_yticks([])
ax2.legend()

# Add text labels
ax2.text(5, 1.1, 'Absolutely\nStable', ha='center', fontsize=10, color='green')
ax2.text(7.9, 1.1, 'Conditionally\nUnstable', ha='center', fontsize=10, color='orange')
ax2.text(11, 1.1, 'Absolutely\nUnstable', ha='center', fontsize=10, color='red')

plt.tight_layout()
plt.savefig('stability_convection.png', dpi=150, bbox_inches='tight')
plt.show()

print(f"Results:")
print(f"  CAPE: {cape:.0f} J/kg")
print(f"  CIN: {cin:.0f} J/kg")
print(f"  LCL: {lcl/1000 if lcl else 'N/A':.1f} km")
print(f"  LFC: {lfc/1000 if lfc else 'N/A':.1f} km")
print(f"  EL: {el/1000 if el else 'N/A':.1f} km")