Cyclone Evaluation¶
modverif can track cyclones through time using sea level pressure (SLP) minima, then evaluate model performance within the cyclone region.
Cyclone Tracking¶
Track a cyclone through a dataset by following the SLP minimum:
from modverif.cyclone import track_cyclone
positions = track_cyclone(
'model.cfdb',
start_lat=-40.0, # initial search latitude
start_lon=170.0, # initial search longitude
search_radius_km=500.0, # search radius around previous position
max_cyclone_radius_km=1000.0,
)
Each position in the returned list is a CyclonePosition containing:
time_index,y_index,x_index-- array indiceslatitude,longitude-- geographic positioncentral_pressure-- SLP at the cyclone center (Pa)radius_km-- estimated cyclone radiustime_str-- timestamp string
Multi-File Tracking¶
For datasets split across multiple files:
from modverif.cyclone import track_cyclone_multi_file
positions = track_cyclone_multi_file(
['run_day1.cfdb', 'run_day2.cfdb', 'run_day3.cfdb'],
start_lat=-40.0,
start_lon=170.0,
)
Tracking a Time Window¶
Pass start_time and/or end_time to track only part of a dataset -- useful when a cache spans
years but the storm of interest lasts days:
positions = track_cyclone(
'model.cfdb',
start_time='2023-02-10',
end_time='2023-02-15T23:59',
smoothing_sigma=2.0,
)
Two things to know about the windowed form:
time_indexremains the absolute index into the dataset's time axis, not the position within the window, so a position can still be passed straight toplot_cyclone_timestep.- With no
start_lat/start_lon, the initial global search happens on the first timestep inside the window.
A window is deliberately not accepted by track_cyclone_multi_file; see its docstring.
Projected Grids¶
Model output on a projected grid -- Lambert conformal, polar stereographic -- carries y/x
coordinates and a CRS rather than latitude and longitude. read_latlon_2d derives the geographic
coordinates that the tracking maths needs, so track_cyclone and evaluate_cyclones work
directly on raw WRF output:
from modverif.cyclone import read_latlon_2d
with cfdb.open_dataset('wrfout.cfdb') as ds:
xlat, xlong = read_latlon_2d(ds)
Note the longitude convention: the projected branch returns pyproj's [-180, 180), while
datasets that store their own longitude coordinate commonly use 0-360. Comparing positions
across two datasets therefore needs normalising -- plot_cyclone_comparison does this
internally. Distance calculations do not care, being periodic.
plot_cyclone_timestep raises NotImplementedError on projected grids: it plots through a
PlateCarree axis, which mangles a curvilinear domain and clips anything east of 180.
SLP Computation¶
If the dataset does not contain an mslp variable, SLP is computed from surface pressure, terrain height, and 2m temperature using the hypsometric equation. A humidity correction is applied if q2 (2m specific humidity) is available.
Tracking Parameters¶
| Parameter | Default | Description |
|---|---|---|
start_lat, start_lon |
None | Initial search location. If None, uses global minimum. |
search_radius_km |
500.0 | Search radius around previous position for next timestep. |
pressure_threshold_pa |
400.0 | Pressure difference threshold for radius estimation. |
max_cyclone_radius_km |
1000.0 | Maximum allowed cyclone radius. |
smoothing_sigma |
None | Gaussian smoothing sigma for SLP field (grid cells). |
start_time, end_time |
None | Inclusive time window. Either may be given alone. |
Comparing Two Tracks¶
Track the same storm independently in two datasets, then compare them at each track's own minimum rather than at a fixed coordinate. That is what separates "the model under-deepens the storm" from "the model has it slightly displaced, and a fixed-grid comparison smears it":
from modverif.cyclone import compare_cyclone_tracks, plot_cyclone_comparison
model = track_cyclone('model.cfdb', start_time=start, end_time=end, smoothing_sigma=2.0)
# Seed the second track from the first, so both follow the same physical storm
reanalysis = track_cyclone(
'era5.cfdb', start_lat=model[0].latitude, start_lon=model[0].longitude,
start_time=start, end_time=end, smoothing_sigma=2.0,
)
pairs, metrics = compare_cyclone_tracks(model, reanalysis, tolerance_min=90)
plot_cyclone_comparison('compare.png', model, reanalysis, pairs, metrics,
label_a='WRF', label_b='ERA5')
metrics reports, with all signs as a minus b:
| Key | Meaning |
|---|---|
min_slp_bias_hpa |
min(SLP_a) - min(SLP_b); positive means a is shallower |
a_min_hpa, b_min_hpa, a_min_time, b_min_time |
Each track's deepest point, and when |
timing_offset_h |
t(a_min) - t(b_min); positive means a lags |
mean_track_sep_km, max_track_sep_km |
Centre separation over matched steps; None if none matched |
n_matched_timesteps, n_a_steps, n_b_steps |
Counts, so a thin match is visible |
Pairing is by nearest timestamp within a tolerance, so datasets with different output cadences
still compare -- a 3-hourly run and hourly reanalysis share no exact timestamps at all. Use
evaluate_cyclones below instead when you want per-variable metrics inside the cyclone region;
it intersects exact timestamps.
Cyclone Visualization¶
Plot a single timestep with SLP field and cyclone position:
from modverif.cyclone import plot_cyclone_timestep
plot_cyclone_timestep(
'model.cfdb',
positions[0],
output_path='cyclone_t0.png',
)
Generate frames for all tracked positions:
from modverif.cyclone import plot_cyclone_track
png_files = plot_cyclone_track(
'model.cfdb',
positions,
output_dir='cyclone_frames/',
)
Cyclone-Region Evaluation¶
Compare two datasets within the tracked cyclone region using the convenience function:
from modverif.evaluate import evaluate_cyclones
evaluate_cyclones(
'source.cfdb', 'test.cfdb', 'cyclone_eval.cfdb',
variables=['air_temperature', 'precipitation'],
metrics=['bias', 'rmse'],
start_lat=-40.0,
start_lon=170.0,
)
This tracks cyclones independently in both datasets and computes metrics within each dataset's cyclone region, along with track position, pressure, and radius differences.