Skip to content

Storm Composite Plots

Storm composite plots overlay three meteorological fields on a geographic map:

  • PWAT (Total Precipitable Water) as color-filled contours
  • MSLP (Mean Sea Level Pressure) as contour lines with labels
  • VIMF (Vertically Integrated Moisture Flux) as wind barbs or quiver arrows

These plots are useful for assessing cyclone and major storm development.

Single Timestep

from modverif.composite import plot_storm_composite_timestep

fig, ax = plot_storm_composite_timestep(
    'model.cfdb',
    time_index=0,
)

# Or save directly
plot_storm_composite_timestep(
    'model.cfdb',
    time_index=0,
    output_path='composite_t0.png',
)

Custom Variable Names

If your dataset uses different variable names:

plot_storm_composite_timestep(
    'model.cfdb',
    time_index=0,
    vimf_u_var='vimf_u',
    vimf_v_var='vimf_v',
    pwat_var='pwat',
    mslp_var='mslp',
)

Animated Sequence

Generate PNG frames for all timesteps plus an animated WebP:

from modverif.composite import plot_storm_composite

png_files, webp_path = plot_storm_composite(
    'model.cfdb',
    output_dir='composite_frames/',
)

Time Filtering

png_files, webp_path = plot_storm_composite(
    'model.cfdb',
    output_dir='composite_frames/',
    start_time='2023-02-12T06:00',
    end_time='2023-02-13T00:00',
)

Animation Options

png_files, webp_path = plot_storm_composite(
    'model.cfdb',
    output_dir='composite_frames/',
    webp_duration=500,   # ms per frame
    webp_quality=80,     # 1-100
    webp_loop=0,         # 0 = infinite
)

Side-by-Side Model Comparison

Compare two models (e.g., WRF vs ERA5) with synchronized timesteps:

from modverif.composite import plot_storm_composite_comparison

png_files, webp_path = plot_storm_composite_comparison(
    'wrf.cfdb',
    'era5.cfdb',
    output_dir='comparison_frames/',
    label_a='WRF',
    label_b='ERA5',
)

Only timesteps that exist in both datasets are plotted. The color scale is computed from both datasets for consistent comparison.

Single Comparison Timestep

from modverif.composite import plot_storm_composite_comparison_timestep

fig, (ax_a, ax_b) = plot_storm_composite_comparison_timestep(
    'wrf.cfdb',
    'era5.cfdb',
    time_index=0,           # index into matched timesteps
    label_a='WRF',
    label_b='ERA5',
)

The time_index refers to the index into the intersection of both datasets' time arrays, not either dataset's raw time array.

Different Variable Names Per Dataset

When datasets use different naming conventions:

png_files, webp_path = plot_storm_composite_comparison(
    'wrf.cfdb',
    'era5.cfdb',
    output_dir='comparison_frames/',
    label_a='WRF',
    label_b='ERA5',
    # WRF variable names
    vimf_u_var_a='vimf_u',
    vimf_v_var_a='vimf_v',
    pwat_var_a='pwat',
    mslp_var_a='mslp',
    # ERA5 variable names
    vimf_u_var_b='vimf_u',
    vimf_v_var_b='vimf_v',
    pwat_var_b='tcwv',
    mslp_var_b='sp',
)

Different Projections

Each panel automatically uses its own map projection based on the dataset's CRS. For example, a WRF dataset in Lambert Conformal and an ERA5 dataset in PlateCarree will each render correctly in their respective panels.

Plot Customization

All composite functions accept these keyword arguments:

Parameter Default Description
vector_type 'barbs' 'barbs' or 'quiver' for VIMF display
thin_factor auto Stride for vector thinning (auto-computed from grid size)
pwat_cmap 'YlGnBu' Colormap for PWAT fill
pwat_levels auto Contour levels for PWAT (auto-computed from data)
mslp_levels 960--1040 by 4 Contour levels for MSLP in hPa
mslp_color 'black' Color for MSLP contour lines
vector_color 'black' Color for barbs or quiver arrows
figsize auto Figure size (auto-computed from domain aspect ratio)
dpi 150 Output resolution

Cartopy Projection Notes

modverif uses cartopy for geographic map projections. Cartopy is optional -- plots fall back to plain matplotlib axes if it is not installed -- but there are several non-obvious behaviours to be aware of when working with projected WRF domains.

Lambert Conformal cutoff and Southern Hemisphere domains

Cartopy's LambertConformal projection has a cutoff parameter (default -30) that silently limits how far the map extends from the projection centre. The default boundary sits at 30°S, so any data north of 30°S is simply not displayed -- with no error or warning.

This is a common issue for Southern Hemisphere WRF domains. For example, a domain centred on New Zealand at 34°S with an outer domain extending to 15°S will have its northern portion clipped.

modverif handles this automatically by setting cutoff=30 for SH projections in pyproj_to_cartopy. If you create your own cartopy axes for a SH Lambert Conformal domain, remember to pass an appropriate cutoff:

import cartopy.crs as ccrs

proj = ccrs.LambertConformal(
    central_longitude=178, central_latitude=-34,
    standard_parallels=[-41, -41],
    cutoff=30,  # default -30 clips SH domains at 30°S
)

Antimeridian (180° longitude)

Domains that cross the 180° meridian (e.g., New Zealand) require careful handling of longitude conventions:

  • ERA5 / NCAR archive data typically uses 0--360° longitude convention, where values near the antimeridian are stored as 180--190° rather than -180° to -170°.
  • pyproj returns longitudes in -180/180° convention when transforming projected coordinates (e.g., Lambert Conformal) to geographic coordinates.
  • This mismatch means that a naive regridding interpolation will produce NaN for grid points near the antimeridian, because -175° falls outside an ERA5 source range of [144, 190].

The fix is to convert target longitudes to 0--360° before interpolation:

from scipy.interpolate import RegularGridInterpolator

# Transform WRF projected coords to lat/lon
wrf_lons, wrf_lats = transformer.transform(xx, yy)

# Convert to 0-360 to match ERA5 convention
wrf_lons_360 = wrf_lons % 360

# Interpolate with explicit convention match
interp = RegularGridInterpolator(
    (era5_lats, era5_lons), data,
    method='linear', bounds_error=False, fill_value=np.nan,
)
target_points = np.column_stack([wrf_lats.ravel(), wrf_lons_360.ravel()])
regridded = interp(target_points).reshape(ny, nx)

Note that cfdb's GridInterp.to_grid() transforms target coordinates internally via pyproj (returning -180/180°), so it cannot resolve the convention mismatch on its own. Use scipy.interpolate.RegularGridInterpolator directly for antimeridian-crossing domains.

Longitude values > 180° in WRF latitude/longitude auxiliary variables are also masked out in modverif's composite plots to prevent cartopy from wrapping the map around the full globe. This only applies to datasets with geographic (lat/lon) coordinates, not projected (y/x) grids.

Comparison panel spacing

Side-by-side comparison plots auto-compute the figure size and subplot spacing from the domain's aspect ratio. Cartopy GeoAxes maintain the map's native aspect ratio regardless of how much space the subplot allocates, so different domain shapes need different spacing to avoid gaps or overlaps. If you need manual control, pass an explicit figsize to the comparison functions.