linjonathan / tropical_cyclone_risk

A Physics-Based, Tropical Cyclone Downscaling Model
MIT License
23 stars 11 forks source link

Track anomalies explanation #4

Closed krober10nd closed 2 months ago

krober10nd commented 4 months ago

Hi, thank you for this open source work. I wanted to know if someone could perhaps explain why these odd looking trajectories are occurring?

I ran with 2016-2021 ERA5 with no changes to the codebase. You can see in the image below these almost satellite-esque swath-like trajectories that are clearly non-physical

tracks_NA_2016_2021

Thank you!

linjonathan commented 3 months ago

Are those tracks possibly ones with two points in time? If so, these probably could arise from too large of a covariance (in magnitude) that can result from not daily-averaging winds. I will push a new version in the coming days that should account for this.

Jonathan

krober10nd commented 3 months ago

Thanks Jonathan for the explanation. I will try with the latest release. For the record, those tracks were composed actually of many points in time. I replotted the same figure but with markers for each point along the trajectory. The anomalous tracks have many points. tracks_NA_2016_2021

linjonathan commented 3 months ago

Hm, interesting. I don't see this kinds of track when I run the model. Could you try the new version (v1.1) and let me know if you have the same issues?

krober10nd commented 3 months ago

Hi Jonathan, I've now installed the latest version that you've recently posted and I'm running with python3.10.

I still experience a similar problem.

I do receive a warning now that I did not prior

_/Users/keithroberts/Areas/OpenSourceTCDownscaling/tropical_cyclone_risk/track/bamtrack.py:91: UserWarning: Converting non-nanosecond precision datetime values to nanosecond precision. This behavior can eventually be relaxed in xarray, as it is an artifact from pandas which is now beginning to support non-nanosecond precision values. This warning is caused by passing non-nanosecond np.datetime64 or np.timedelta64 values to the DataArray or Variable constructor; it can be silenced by converting the values to nanosec

In addition, I've tested now basin = GL and basin=NA for the same time interval (2016-2021) and got quite significantly different quantity of synthetic tracks.

For example, the GL basin and plotting only tracks labeled in the NA basin.

tracks_GL_2016_2021 output_netcdf_files_v1.zip

versus using the NA basin and plotting everything

tracks_NA_2016_2021_v2

For reference here is the track visualization code and I'm provided the output NetCDF files.

output_netcdf_files_v1.zip

'''
Visualize produced tracks
'''
import xarray as xr 
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature

ds = xr.open_dataset('data/era5/test2/tracks_NA_era5_201601_202112.nc')
print(ds)

# select tracks for 2016 
#ds_2016_NA = ds.copy()
# select only North Atlantic tracks
ds_2016_NA = ds.sel(basin='NA')
print(ds_2016_NA) 
# make longitude -180 to 180 from 0 to 360
#ds_2016_NA['lon_trks'] = (ds_2016_NA['lon_trks'] + 180) % 360 - 180

fig, ax = plt.subplots(subplot_kw={'projection': ccrs.PlateCarree()})
# loop over all tracks using the n_trk dimension
for i in range(ds_2016_NA['n_trk'].size):
    # select the i-th track
    trk = ds_2016_NA.sel(n_trk=i)
    print(trk.time.values[-1]-trk.time.values[0])
    # plot the track
    ax.plot(trk['lon_trks'], trk['lat_trks'], transform=ccrs.PlateCarree(),marker='o',markersize=0.3,linewidth=0.1)
ax.coastlines()
# show the ocean and land features
ax.add_feature(cfeature.OCEAN)
ax.add_feature(cfeature.LAND)
# zoom into the North Atlantic
ax.set_extent([-100, 0, 0, 60])
# label the title of the plot
plt.title('Tracks of North Atlantic, 2016-2021')
plt.savefig('data/era5/test2/tracks_NA_2016_2021_v2.png', dpi=300, bbox_inches='tight')
linjonathan commented 3 months ago

FYI, the basin variable is actually 'tc_basins', so that you should access NA tracks only using the following code:

lon_trks = ds['lon_trks'].where(ds['tc_basins'] == 'NA', drop = True)
lat_trks = ds['lat_trks'].where(ds['tc_basins'] == 'NA', drop = True)

And the following to plot:

ax.plot(lon_trks[i, :], lat_trks[i, :], transform=ccrs.PlateCarree(),marker='o',markersize=0.3,linewidth=0.1)

I've traced the issue to the wind steering. In your dataset, any tracks that occur during the years of 2020 and 2021 have this issue. You can check this using the 'tc_years' variable. I would suggest looking at your data files for the 2020 and 2021 period, and make sure they follow the file formats. For example, does each year have (at-least) daily level winds? My directory for 2020, for example, looks like the following:

era5_q_monthly_2020.nc   era5_sst_monthly_2020.nc  era5_u_daily_2020.nc
era5_sp_monthly_2020.nc  era5_t_monthly_2020.nc    era5_v_daily_2020.nc

You can also take a look at the _env_wnd_era5_201601202112.nc file that is output by the post-processing. Do the mean u250/u850/v250/v850 wind fields look reasonable?

I haven't been able to reproduce this issue, and given that this only occurs for tracks in the years 2020/2021 from the dataset you've given me, I don't think this is an issue with the code.

krober10nd commented 3 months ago

Thanks, I'll try this out and follow back. I can confirm that for 2021 the u-component of the daily winds did not download so this is indeed likely the issue.

krober10nd commented 3 months ago

Okay this checks out. Thanks.

I'm curious though how you can get the inter annual track frequency variability if you have to specify the number of tracks per year in the namelist.py? Each year produced exactly 20 tracks. How do you approach this in practice?

tracks_2015 tracks_2016 tracks_2017 tracks_2018 tracks_2019

linjonathan commented 3 months ago

The model is set so that each year has a fixed number of tracks. You can convert this to interannual variability of TC frequency under the assumption of constant seeding rate (per year) by normalizing the number of TCs per year by the number of seeds. In the below code, _seeds_peryear is the number of seeds per year.

drop_vars = ["lon_trks", "lat_trks", "u250_trks", "v250_trks", "u850_trks", "v850_trks",
                       "v_trks", "m_trks", "vmax_trks", "tc_month", "tc_years"]
ds_seeds = xr.open_mfdataset(fn_tracks, concat_dim = "year", combine = "nested",
                             data_vars="minimal", drop_variables = drop_vars)
seeds_per_month = ds_seeds['seeds_per_month']
seeds_per_year = seeds_per_month.groupby('year').mean(['year', 'month']
krober10nd commented 3 months ago

Thanks for the response. Sorry for the confusion but I think you meant to say if the seeding rate is variable (per year) then you can normalize otherwise you are dividing two constants (e.g., number of tracks and seeding rate per year). Still however how is that ratio used? Do you drop modeled tracks randomly somehow?

It could be helpful to have a helper function that reads in the dataset and normalizes the modeled tracks to produce realistic inter annual frequencies.

linjonathan commented 3 months ago

If you assume that in the real world the seeding rate is constant ever year, you can divide the number of TC events you have by the total number of seeds every year, to get a survival rate (# events per seed). Then, you have to multiple by an arbitrary constant (which is the total number of seeds every year), to get a TC frequency.

Yes, that's something in plan for the future.

krober10nd commented 3 months ago

Okay thanks but I think this is worth going into more detail..

As an example, I simulated 2016 for the North Atlantic basin. I have specified 20 TC events per the namelist. Following your logic, I divide 20 TC events by the total number of seeds every year (2,232 seeds in 2016 in my case), this gives me an average survival rate 0.00896057347 TC events per seed, then I'd multiply the seed rate per month to get the number of TC events in each month. This however produces the following figure which shows TC events occurring during far out of season months and the numbers are floats (number of events per month) which I guess you round to the nearest integer? I feel like something is missing.

events_per_month

import xarray as xr
import matplotlib.pyplot as plt

# Define file path and variables to drop
file_path = "data/era5/test4/tracks_NA_era5_201501_202012.nc"
variables_to_drop = ["lon_trks", "lat_trks", "u250_trks", "v250_trks", "u850_trks", "v850_trks",
                     "v_trks", "m_trks", "vmax_trks", "tc_month", "tc_years"]

# Load the dataset, concatenating along the 'year' dimension
ds_tracks = xr.open_mfdataset(file_path, concat_dim="year", combine="nested",
                              data_vars="minimal", drop_variables=variables_to_drop)

# Select seed data for the North Atlantic basin and year 2016
seeds_per_month_2016 = ds_tracks.sel(basin='NA', year=2016)['seeds_per_month']
seeds_per_year_2016 = seeds_per_month_2016.sum(dim='month')

# Define the number of events per year and calculate survival rate
events_per_year = 20
survival_rate = events_per_year / seeds_per_year_2016

# Calculate the number of events per month
events_per_month_2016 = seeds_per_month_2016 * survival_rate

# Convert to a pandas Series for plotting
events_per_month_series = events_per_month_2016.to_series()

# Plot the number of synthetic TC events per month
events_per_month_series.plot(kind='bar', color='skyblue')
plt.xticks(range(0, 12), ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])
plt.ylabel('Number of TC Events')
plt.xlabel('Month in 2016')
plt.title('Number of Synthetic TC Events per Month\nNorth Atlantic 2016')

# Save and show the figure
plt.savefig('events_per_month.png', dpi=300, bbox_inches='tight')
plt.show()

tracks_NA_era5_201501_202012.nc.zip

krober10nd commented 3 months ago

I see now there is a tc_month variables produce a reasonable inter monthly variability.

krober10nd commented 3 months ago

Could you clarify your previous comment? Based on my previous posts, I have followed the instructions step by step as you suggested. It's unclear what you are trying to communicate.

linjonathan commented 3 months ago

The variable seeds_per_year in the above code gives the number of seeds needed to obtain the specified number of tracks, or n_tracks (in your case, 20). The quantity n_tracks / seeds_per_year gives the number of tracks per number of seeds every year. Since we don't know what the true number of seeds is per year, we multiply by an arbitrary constant with units [number of seeds] to yield a storm count every year. That constant is chosen such that the average of n_tracks / seeds_per_year is equal to the observed number of storms. The seed survival rate varies by basin, but in my experience is on average around 5-10%.

If you want to look at the seasonal cycle of seed survival rate, you need to accumulate the number of storms for each month, and divide that by the number of seeds for that given month.

H = np.histogram(ds_tracks['tc_month'], bins = np.arange(0.5, 12.6, 1))
plt.plot(H[0] / seeds_per_month.sum('year'))
krober10nd commented 2 months ago

Thanks! So then I assume you just select randomly N tracks per year based on the chosen constant multiplied by the n_tracks / seeds_per_year to get a realistic synthetic climatology (in terms of frequency).

linjonathan commented 2 months ago

That sounds about right.