Estimate Turbulent Air-sea Fluxes

Estimate Turbulent Air-sea Fluxes#

AeroBulk is a FORTRAN90-based library and suite of tools that feature state of the art parameterizations to estimate turbulent air-sea fluxes by means of the traditional aerodynamic bulk formulae.

These turbulent fluxes, namely, wind stress, evaporation (latent heat flux) and sensible heat flux, are estimated using the sea surface temperature (bulk or skin), and the near-surface atmospheric surface state: wind speed, air temperature and humidity. If the cool-skin/warm-layer schemes need to be called to estimate the skin temperature, surface downwelling shortwave and longwave radiative fluxes are required.

Before proceeding with all the steps, first import some necessary libraries and packages

import xarray as xr
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import easyclimate as ecl

Load the NetCDF sample ERA5 dataset containing near-surface atmospheric variables (e.g., 2m temperature, dewpoint temperature, 10m wind components, mean sea level pressure) and sea surface temperature (SST), and display its metadata structure to verify the dimensions and attributes of input variables.

sample_data = xr.open_dataset("sample_data_N20.nc")
sample_data
<xarray.Dataset> Size: 411kB
Dimensions:  (time: 4, lon: 80, lat: 40)
Coordinates:
  * time     (time) datetime64[ns] 32B 2024-01-01 ... 2024-01-01T18:00:00
  * lon      (lon) float64 640B 0.0 4.5 9.0 13.5 ... 342.0 346.5 351.0 355.5
  * lat      (lat) float64 320B 86.6 82.19 77.76 73.32 ... -77.76 -82.19 -86.6
Data variables:
    u10      (time, lat, lon) float32 51kB ...
    v10      (time, lat, lon) float32 51kB ...
    d2m      (time, lat, lon) float32 51kB ...
    t2m      (time, lat, lon) float32 51kB ...
    msl      (time, lat, lon) float32 51kB ...
    sst      (time, lat, lon) float32 51kB ...
    ssrd     (time, lat, lon) float32 51kB ...
    strd     (time, lat, lon) float32 51kB ...
Attributes:
    CDI:          Climate Data Interface version 2.4.4 (https://mpimet.mpg.de...
    Conventions:  CF-1.6
    history:      Fri May 02 15:46:46 2025: cdo -f nc4 -z zip5 remapbil,N20 d...
    CDO:          Climate Data Operators version 2.4.4 (https://mpimet.mpg.de...


Convert 2m dewpoint temperature (d2m) and mean sea level pressure (msl) data to near-surface specific humidity (q) using easyclimate.transfer_dewpoint_2_specific_humidity, leveraging thermodynamic relationships; this specific humidity is a critical humidity parameter for calculating turbulent heat fluxes (latent heat flux).

q_data = ecl.transfer_dewpoint_2_specific_humidity(
    dewpoint_data = sample_data.d2m,
    pressure_data = sample_data.msl,
    dewpoint_data_units = "K",
    pressure_data_units = "Pa"
)
q_data
<xarray.DataArray 'specific_humidity' (time: 4, lat: 40, lon: 80)> Size: 51kB
array([[[0.00071787, 0.00076128, 0.00079259, ..., 0.00052519,
         0.00058627, 0.00065293],
        [0.00129225, 0.00162815, 0.00196439, ..., 0.00068812,
         0.00073899, 0.00094276],
        [0.0019641 , 0.00253472, 0.00247658, ..., 0.00117425,
         0.0014484 , 0.00163637],
        ...,
        [0.00029264, 0.00026416, 0.00021988, ..., 0.00051915,
         0.00040085, 0.00033248],
        [0.00030657, 0.00027718, 0.00025185, ..., 0.00038103,
         0.00034929, 0.00032894],
        [0.00023752, 0.00023008, 0.0002227 , ..., 0.00026972,
         0.00025686, 0.00024608]],

       [[0.00068884, 0.00072928, 0.00076127, ..., 0.00056283,
         0.00060715, 0.00064756],
        [0.0011774 , 0.00152747, 0.00190248, ..., 0.00066314,
         0.00070415, 0.00087111],
        [0.0023589 , 0.00248486, 0.00281265, ..., 0.00113027,
         0.00114993, 0.00155959],
...
        [0.00035771, 0.00032354, 0.00031127, ..., 0.00061581,
         0.00052843, 0.00040916],
        [0.00036641, 0.00033005, 0.0003105 , ..., 0.00044219,
         0.00041493, 0.00039646],
        [0.00027146, 0.00025957, 0.00025106, ..., 0.00033539,
         0.00030939, 0.00028785]],

       [[0.00054111, 0.00054275, 0.0005398 , ..., 0.00053521,
         0.00053701, 0.00053964],
        [0.00073773, 0.00088102, 0.00115099, ..., 0.00054648,
         0.00055368, 0.00060174],
        [0.00175801, 0.00260998, 0.0029205 , ..., 0.00089693,
         0.00087427, 0.00103874],
        ...,
        [0.00034639, 0.00030798, 0.00027416, ..., 0.0006007 ,
         0.00053428, 0.00043464],
        [0.00035812, 0.00032369, 0.00030032, ..., 0.00043612,
         0.00040482, 0.00038519],
        [0.00030552, 0.00028482, 0.00026813, ..., 0.00038573,
         0.00034988, 0.00032437]]], shape=(4, 40, 80), dtype=float32)
Coordinates:
  * time     (time) datetime64[ns] 32B 2024-01-01 ... 2024-01-01T18:00:00
  * lon      (lon) float64 640B 0.0 4.5 9.0 13.5 ... 342.0 346.5 351.0 355.5
  * lat      (lat) float64 320B 86.6 82.19 77.76 73.32 ... -77.76 -82.19 -86.6
Attributes:
    units:    g/g


Compute turbulent air-sea fluxes without sea surface skin temperature correction using easyclimate.field.boundary_layer.calc_turbulent_fluxes_without_skin_correction (employing the NCAR parameterization scheme). Inputs include SST, 2m air temperature, specific humidity, 10m wind components, and mean sea level pressure, yielding outputs of latent heat flux (ql), sensible heat flux (qh), wind stress components (taux, tauy), and evaporation (evap) as an xarray Dataset.

flux_no_skin = ecl.field.boundary_layer.calc_turbulent_fluxes_without_skin_correction(
    sst_data = sample_data.sst,
    sst_data_units = 'K',
    absolute_temperature_data = sample_data.t2m,
    absolute_temperature_data_units = 'degK',
    specific_humidity_data = q_data,
    specific_humidity_data_units = 'g/g',
    zonal_wind_speed_data = sample_data.u10,
    meridional_wind_speed_data = sample_data.v10,
    mean_sea_level_pressure_data = sample_data.msl,
    mean_sea_level_pressure_data_units = 'Pa',
    algorithm = 'ncar',
)
flux_no_skin
<xarray.Dataset> Size: 257kB
Dimensions:  (time: 4, lon: 80, lat: 40)
Coordinates:
  * time     (time) datetime64[ns] 32B 2024-01-01 ... 2024-01-01T18:00:00
  * lon      (lon) float64 640B 0.0 4.5 9.0 13.5 ... 342.0 346.5 351.0 355.5
  * lat      (lat) float64 320B 86.6 82.19 77.76 73.32 ... -77.76 -82.19 -86.6
Data variables:
    ql       (time, lat, lon) float32 51kB -63.44 -62.87 -63.89 ... nan nan nan
    qh       (time, lat, lon) float32 51kB -156.2 -152.3 -153.7 ... nan nan nan
    taux     (time, lat, lon) float32 51kB 0.009611 0.003981 ... nan nan
    tauy     (time, lat, lon) float32 51kB -0.02646 -0.02885 ... nan nan
    evap     (time, lat, lon) float32 51kB -2.532e-05 -2.51e-05 ... nan nan
Attributes:
    method:                                    aerobulk without skin correction
    algorithm:                                 ncar
    height_for_temperature_specific_humidity:  2
    height_for_wind:                           10
    iteration:                                 8


Visualize the turbulent flux results without skin correction using Cartopy and Matplotlib. A 2x2 subplot layout displays the initial time-step latent heat flux (filled contour), sensible heat flux (filled contour), wind stress vectors (Quiver plot), and evaporation (filled contour). The Plate Carrée projection is applied with coastlines and gridlines to enhance geographic referencing.

proj = ccrs.PlateCarree(central_longitude = 200)
proj_trans = ccrs.PlateCarree()

fig, ax = plt.subplots(2, 2, figsize = (10, 7), subplot_kw={"projection": proj})

# -----------------------------------------------
axi = ax[0, 0]

draw_data = flux_no_skin["ql"].isel(time = 0)
draw_data.plot.contourf(
    ax = axi, vmax = 600, levels = 21,
    transform = proj_trans,
    cbar_kwargs={"location": "bottom", "aspect": 50, "pad" : 0.1}
)

axi.coastlines()
axi.gridlines(draw_labels=["left", "bottom"], color="grey", alpha=0.5, linestyle="--")
axi.set_title("Latent Heat Flux")

# -----------------------------------------------
axi = ax[0, 1]

draw_data = flux_no_skin["qh"].isel(time = 0)
draw_data.plot.contourf(
    ax = axi, vmax = 200, levels = 21,
    transform = proj_trans,
    cbar_kwargs={"location": "bottom", "aspect": 50, "pad" : 0.1}
)

axi.coastlines()
axi.gridlines(draw_labels=["left", "bottom"], color="grey", alpha=0.5, linestyle="--")
axi.set_title("Sensible Heat Flux")

# -----------------------------------------------
axi = ax[1, 0]

draw_data = flux_no_skin[["taux", "tauy"]].isel(time = 0)
draw_data.plot.quiver(
    x = "lon", y = "lat", u = "taux", v = "tauy",
    transform = proj_trans, ax = axi,
)

axi.coastlines()
axi.gridlines(draw_labels=["left", "bottom"], color="grey", alpha=0.5, linestyle="--")
axi.set_title("Zonal/Meridional wind stress")

# -----------------------------------------------
axi = ax[1, 1]

draw_data = flux_no_skin["evap"].isel(time = 0)
draw_data.plot.contourf(
    ax = axi, vmax = 0.0002, levels = 21,
    transform = proj_trans,
    cbar_kwargs={"location": "bottom", "aspect": 50, "pad" : 0.1}
)

axi.coastlines()
axi.gridlines(draw_labels=["left", "bottom"], color="grey", alpha=0.5, linestyle="--")
axi.set_title("Evaporation")

# -----------------------------------------------
fig.suptitle("Flux no skin", size = 20)
Flux no skin, Latent Heat Flux, Sensible Heat Flux, Zonal/Meridional wind stress, Evaporation
Text(0.5, 0.98, 'Flux no skin')

Compute turbulent air-sea fluxes with sea surface skin temperature correction using easyclimate.field.boundary_layer.calc_turbulent_fluxes_skin_correction (utilizing the COARE3.0 parameterization scheme). In addition to base inputs, time-normalized (divided by 3600 seconds) downwelling shortwave and longwave radiation fluxes (converted to \(\mathrm{W/m^2}\)) are included to account for sea surface skin temperature effects, yielding an xarray Dataset of corrected fluxes.

Warning

For the ERA5 reanalysis, the processing period is over the 1 hour ending at the validity date and time. To convert to watts per square metre ( \(\mathrm{W/m^2}\) ), the accumulated values should be divided by the accumulation period expressed in seconds.

flux_skin = ecl.field.boundary_layer.calc_turbulent_fluxes_skin_correction(
    sst_data = sample_data.sst,
    sst_data_units = 'K',
    absolute_temperature_data = sample_data.t2m,
    absolute_temperature_data_units = 'degK',
    specific_humidity_data = q_data,
    specific_humidity_data_units = 'g/g',
    zonal_wind_speed_data = sample_data.u10,
    meridional_wind_speed_data = sample_data.v10,
    mean_sea_level_pressure_data = sample_data.msl,
    mean_sea_level_pressure_data_units = 'Pa',
    downwelling_shortwave_radiation = sample_data.ssrd/(3600),
    downwelling_shortwave_radiation_units = "W/m^2",
    downwelling_longwave_radiation = sample_data.ssrd/(3600),
    downwelling_longwave_radiation_units = "W/m^2",
    algorithm = 'coare3p0',
)
flux_skin
/home/runner/work/easyclimate/easyclimate/src/easyclimate/field/boundary_layer/aerobulk.py:151: UserWarning: The varable rad_lw of values 1189.8050537109375 is beyond [0, 750]。
  warnings.warn(
<xarray.Dataset> Size: 615kB
Dimensions:  (time: 4, lon: 80, lat: 40)
Coordinates:
  * time     (time) datetime64[ns] 32B 2024-01-01 ... 2024-01-01T18:00:00
  * lon      (lon) float64 640B 0.0 4.5 9.0 13.5 ... 342.0 346.5 351.0 355.5
  * lat      (lat) float64 320B 86.6 82.19 77.76 73.32 ... -77.76 -82.19 -86.6
Data variables:
    ql       (time, lat, lon) float64 102kB -60.25 -59.51 -60.23 ... nan nan nan
    qh       (time, lat, lon) float64 102kB -166.4 -161.4 -161.9 ... nan nan nan
    taux     (time, lat, lon) float64 102kB 0.009765 0.004058 ... nan nan
    tauy     (time, lat, lon) float64 102kB -0.02688 -0.02941 ... nan nan
    t_s      (time, lat, lon) float64 102kB 270.5 270.5 270.5 ... nan nan nan
    evap     (time, lat, lon) float64 102kB -2.403e-05 -2.374e-05 ... nan nan
Attributes:
    method:                                    aerobulk with skin correction
    algorithm:                                 coare3p0
    height_for_temperature_specific_humidity:  2
    height_for_wind:                           10
    iteration:                                 8


Visualize the turbulent flux results with skin correction, maintaining the same 2x2 subplot layout and visualization parameters (projection, contour ranges, coastlines, etc.) as the non-skin-corrected plots to facilitate direct comparison of the impacts of skin temperature correction on latent heat flux, sensible heat flux, wind stress, and evaporation.

proj = ccrs.PlateCarree(central_longitude = 200)
proj_trans = ccrs.PlateCarree()

fig, ax = plt.subplots(2, 2, figsize = (10, 7), subplot_kw={"projection": proj})

# -----------------------------------------------
axi = ax[0, 0]

draw_data = flux_skin["ql"].isel(time = 0)
draw_data.plot.contourf(
    ax = axi, vmax = 600, levels = 21,
    transform = proj_trans,
    cbar_kwargs={"location": "bottom", "aspect": 50, "pad" : 0.1}
)

axi.coastlines()
axi.gridlines(draw_labels=["left", "bottom"], color="grey", alpha=0.5, linestyle="--")
axi.set_title("Latent Heat Flux")

# -----------------------------------------------
axi = ax[0, 1]

draw_data = flux_skin["qh"].isel(time = 0)
draw_data.plot.contourf(
    ax = axi, vmax = 200, levels = 21,
    transform = proj_trans,
    cbar_kwargs={"location": "bottom", "aspect": 50, "pad" : 0.1}
)

axi.coastlines()
axi.gridlines(draw_labels=["left", "bottom"], color="grey", alpha=0.5, linestyle="--")
axi.set_title("Sensible Heat Flux")

# -----------------------------------------------
axi = ax[1, 0]

draw_data = flux_skin[["taux", "tauy"]].isel(time = 0)
draw_data.plot.quiver(
    x = "lon", y = "lat", u = "taux", v = "tauy",
    transform = proj_trans, ax = axi,
)

axi.coastlines()
axi.gridlines(draw_labels=["left", "bottom"], color="grey", alpha=0.5, linestyle="--")
axi.set_title("Zonal/Meridional wind stress")

# -----------------------------------------------
axi = ax[1, 1]

draw_data = flux_skin["evap"].isel(time = 0)
draw_data.plot.contourf(
    ax = axi, vmax = 0.0002, levels = 21,
    transform = proj_trans,
    cbar_kwargs={"location": "bottom", "aspect": 50, "pad" : 0.1}
)

axi.coastlines()
axi.gridlines(draw_labels=["left", "bottom"], color="grey", alpha=0.5, linestyle="--")
axi.set_title("Evaporation")

# -----------------------------------------------
fig.suptitle("Flux skin", size = 20)
Flux skin, Latent Heat Flux, Sensible Heat Flux, Zonal/Meridional wind stress, Evaporation
Text(0.5, 0.98, 'Flux skin')

Total running time of the script: (0 minutes 12.278 seconds)