WR_read_example.ipynb¶
Jupyter Notebook demonstrating reading WR data (IWR and LC) and doing simple analysis¶
Notebook description¶
This notebook intends to simplify the use of the 7 year-round weather regimes for the North-Atlantic European region introduced by Grams et al. (2017) doi:10.1038/nclimate3338 and updated on ERA5 as described by Hauser et al. (2024) doi:10.5194/wcd-5-633-2024. The dataset is published at Zenodo (doi:10.5281/zenodo.17080146) and supplementary data to the publication Grams (2025, in preparation for Weather Clim. Dynam.) which will contain an in-depth technical explanation and analysis of key characteristics and trend in these regimes.
Christian thanks Sera and Dominik for help with coding this ipynb and providing auxiliary functions.
Part 1 uses fct_wrera_db (V Nov 2020) and fct_wrlcera (V Sep 2019) provided by Dominik Büeler to read the WR data. The former reads the file containing IWR time series and the categorical maxIWR and LCattr attributions. The latter generates LC objects.
Part 2 generates an time series plots of IWR, with the active life cycles marked, and a marker for the life cycle attribution.
Part 3 computes a frequency climatology in a given period and plots the frequency absolute as well as an anomaly.
Part 4 computes the projection in each of the regimes (regime indices), using the netcdf files with the normalised regime patterns and an instantaneous current Lanczos filtered geopotential height anomaly. It provides and example how to normalise the data prior to computing the regime indices.
Part 5 reads the regime patterns and the climatology for plotting the original regime patterns.
author contact: Christian Grams weatherregimes@gmail.com
based on templates by: Seraphine Hauser and Dominik Büeler
date: 9 September 2025
part of Data set: Grams, C. M., S. Hauser, and D. Büeler, (2025). Year-round North Atlantic-European Weather Regimes in ERA5 reanalyses [Data set]. Zenodo. doi:10.5281/zenodo.17080146. Please refer to Readme.md in the Dataset for a technical description of the data files.
Part 1 Read the data¶
import matplotlib
import os
import numpy as np
import pylab
import time
from datetime import date
from scipy import stats
from scipy import interpolate
from scipy.ndimage import label
from pylab import *
import calendar
import datetime
import warnings
warnings.filterwarnings("ignore")
from matplotlib import pyplot as plt
import sys
from netCDF4 import Dataset
import cartopy.crs as ccrs
import cartopy
import matplotlib.ticker as mticker
from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
import matplotlib.patches as mpatches
import os, re
from fct_wrera_db import wrera
from fct_wrlcera_db import wrlcera
#---- function that creates a datelist with increments in hours (variable dt) between two dates d1 and d2 ----
def create_datelist(d1='19500101_00', d2 = '19500101_00',dt=int(3)):
s = datetime.datetime.strptime("%s" % (str(d1)), "%Y%m%d_%H")
e = datetime.datetime.strptime("%s" % (str(d2)), "%Y%m%d_%H")
diff = e - s
days = diff.days
days_to_hours = days * 24
diff_two_times = (diff.seconds) / 3600
overall_hours = int(days_to_hours + diff_two_times)
datelist = []
for m in range(0, int(overall_hours)+1, int(dt)):
date = s + datetime.timedelta(hours=m)
datelist.append(date.strftime("%Y%m%d_%H"))
return datelist
def str2bool(v):
return v.lower() in ("yes", "true", "t", "1")
## A lengthy cell facilitating access to NCL color tables for Part 5
# https://docs.dkrz.de/doc/visualization/sw/python/source_code/python-matplotlib-read-and-use-ncl-colormaps.html
# coding: utf-8
'''
DKRZ example
https://docs.dkrz.de/doc/visualization/sw/python/source_code/python-matplotlib-read-and-use-ncl-colormaps.html
Use NCL colormaps
NCL provides a large collection of colormaps that former NCL users would
like to continue using with Python.
The following function 'get_NCL_colormap' reads an NCL colormap either from
local disk or from an existing NCL installation or directly from the colormaps
URL of NCL. The RGB values are then converted to a Matplotlib Colormap
object that can be used to create colored plots.
The function 'display_colormap_indices' generates a raster plot using the
given colormap and add the index value to each color box.
-------------------------------------------------------------------------------
2022 copyright DKRZ licensed under CC BY-NC-SA 4.0
(https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)
-------------------------------------------------------------------------------
'''
#import os, re
#import numpy as np
import matplotlib.colors as mcolors
#import matplotlib.pyplot as plt
def get_NCL_colormap(cmap_name, extend='None'):
'''Read an NCL RGB colormap file and convert it to a Matplotlib colormap
object.
Parameter:
cmap_name NCL RGB colormap name, e.g. 'ncl_default'
extend use NCL behavior of color handling for the colorbar 'under'
and 'over' colors. 'None' or 'ncl', default 'None'
Description:
Read the NCL colormap and convert it to a Matplotlib Colormap object.
It checks if the colormap file is already available or use the
appropriate URL.
If NCL is installed the colormap will be searched in its colormaps
folder $NCARG_ROOT/lib/ncarg/colormaps.
Returns a Matplotlib Colormap object.
'''
from matplotlib.colors import ListedColormap
import requests
import errno
#-- NCL colormaps URL
NCARG_URL = 'https://www.ncl.ucar.edu/Document/Graphics/ColorTables/Files/'
#-- read the NCL colormap RGB file
colormap_file = cmap_name+'.rgb'
cfile = os.path.split(colormap_file)[1]
if os.path.isfile(colormap_file) == False:
#-- if NCL is already installed
if 'NCARG_ROOT' in os.environ:
cpath = os.environ['NCARG_ROOT']+'/lib/ncarg/colormaps/'
if os.path.isfile(cpath + cfile):
colormap_file = cpath + cfile
with open(colormap_file) as f:
lines = [re.sub('\s+',' ',l) for l in f.read().splitlines() if not (l.startswith('#') or l.startswith('n'))]
#-- use URL to read colormap
elif not 'NCARG_ROOT' in os.environ:
url_file = NCARG_URL+'/'+cmap_name+'.rgb'
res = requests.head(url_file)
if not res.status_code == 200:
print(f'{cmap_name} does not exist!')
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), cmap_name)
content = requests.get(url_file, stream=True).content
lines = [re.sub('\s+',' ',l) for l in content.decode('UTF-8').splitlines() if not (l.startswith('#') or l.startswith('n'))]
else:
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), colormap_file)
#-- use local colormap file
else:
with open(colormap_file) as f:
lines = [re.sub('\s+',' ',l) for l in f.read().splitlines() if not (l.startswith('#') or l.startswith('n'))]
#-- skip all possible header lines
tmp = [l.split('#', 1)[0] for l in lines]
tmp = [re.sub(r'\s+',' ', s) for s in tmp]
tmp = [ x for x in tmp if ';' not in x ]
tmp = [ x for x in tmp if x != '']
#-- get the RGB values
i = 0
for l in tmp:
new_array = np.array(l.split()).astype(float)
if i == 0:
color_list = new_array
else:
color_list = np.vstack((color_list, new_array))
i += 1
#-- make sure that the RGB values are within range 0 to 1
if (color_list > 1.).any(): color_list = color_list / 255
#-- add alpha-channel RGB -> RGBA
alpha = np.ones((color_list.shape[0],4))
alpha[:,:-1] = color_list
color_list = alpha
#-- convert to Colormap object
if extend == 'ncl':
cmap = ListedColormap(color_list[1:-1,:])
else:
cmap = ListedColormap(color_list)
#-- define the under, over, and bad colors
under = color_list[0,:]
over = color_list[-1,:]
bad = [0.5, 0.5, 0.5, 1.]
cmap.set_extremes(under=color_list[0], bad=bad, over=color_list[-1])
return cmap
Configurations for reading data¶
# path to raw data
path = "../"
basepath = path + "wr_data/"
# other variables for call of fct_wrera_db() and fct_wrlcera_db()
start = '19500111_00' # first date considered for reading data must include the period for which a reference climatology shall be computed in in Part 3 (default 1979-2019)
end = '20250726_21' # last date considered
# keep below the default settings for ERA5 (no changes!)
hours = ['00','03','06','09','12','15','18','21'] # 2-digit hours considered (3hourly for era5)
tformat = 'string'
dt = 3 # time step in hours
nwr=7
wrs_l = ['AT','ZO','ScTr','AR','EuBL','ScBL','GL','no'] # short WR labels in correct order
wrs_label = ["Atlantic trough", "Zonal regime",\
"Scandinavian trough","Atlantic ridge",\
"European blocking", "Scandinavian blocking",\
"Greenland blocking","no regime"] # long WR labels
# COLOUR names for handling full colorblindness here in HEX-code
wine = "#882255" #AT
rose = "#CC6677" #ZO
sand = "#DDCC77" #ScTr
olive = "#999933" #AR
teal = "#44AA99" #EuBL
green = "#117733" #ScBL
indigo = "#332288" #GL
purple ="#AA4499"
# original colours
colors_regimes = ["indigo", "red", "darkorange", "gold", "yellowgreen", "darkgreen", "blue", "grey"] # WR colors in correct order
# better distinction ScTr-EuBL and ZO-ScBL
#colors_regimes = ["indigo", rose, sand, "gold", teal, green, "blue", "grey"] # WR colors
#full color blindness
#colors_regimes = [wine, rose, sand, olive, teal, green, indigo, "grey"] # WR colors
Part 1: Read all WR data¶
Read wr life cycle data¶
Creates a dictionary of characteristics for each unique WR life cycle with the following structure: datawrlc[key=wr][wrlcID][0:19]
key=wr: 'AT','ZO','ScTr','AR','EuBL','ScBL','GL'
wrlcID: the ID of the life cycle for regime type wr (continous number)
0:19: encoded as below.
The following characteristic values are given for every individual life cycle:
[0]: 'number': life cycle id (only unique within the corresponding weather regime),
[1]: 'onset': date of onset,
[2]: 'sat_start': date of saturation start,
[3]: 'mx': date of maximum projection,
[4]: 'sat_end': date of saturation end,
[5]: 'decay': date of decay,
[6]: 'dcfr': of secondary importance; counterpart of onto
[7]: 'dcto': weather regime active in life cycle within a specified period
after decay of current weather regime (counterpart of onfr)
[8]: 'dctoID': id of life cycle of weather regime in dcto
[9]: 'dctoDATE': first date of life cycle of weather regime in dcto
[10]: 'onfr': weather regime active in life cycle within a specified period
before onset of current weather regime (counterpart of dcto)
[11]: 'onto': of secondary importance; counterpart of dcfr
[12]: 'onfrID': id of life cycle of weather regime in onfr
[13]: 'onfromDATE': last date of life cycle of weather regime in onfr
[14]: 'trfr': weather regime active in life cycle before current weather regime
becomes active in life cycle (= transition within life cycle vector);
difference to onfr: current weather regime does not need to have
onset at time of trfr but potentially already before (overlapping
life cycles); counterpart to trto
[15]: 'trfrID': id of life cycle of weather regime in trfr
[16]: 'trfromDATE': date of transition from trfr to current weather regime
[17]: 'trto': weather regime active in life cycle after current weather regime
ends being active in life cycle (= transition within life cycle vector);
difference to dcto: current weather regime does not need to have
decay at time of trto but potentially only afterward (overlapping
life cycles); counterpart to trfr
[18]: 'trtoID': id of life cycle of weather regime in trto
[19]: 'trtoDATE': date of transition from current to trto weather regime
for no regime, the following characteristic values are given for every individual no regime "life cycle":
[0]: 'number': life cycle id (only unique within the corresponding weather regime),
[1]: 'onset': date of onset,
[2]: 'decay': date of decay,
[3]: 'duration': duration of life cycle in hours,
[4]: 'comes_from': weather regime active in life cycle before current no regime
[5]: 'ID_from': id of weather regime in comes_from
[6]: 'transition_to': weather regime active in life cycle after current no regime
[7]: 'ID_to': id of weather regime in transition_to
# --------------------------------------------------------------------
# read wr life cycle data
# --------------------------------------------------------------------
print("Read WR LC data")
datawrlc = wrlcera(wrs_l, start, end, tformat, basepath)
Read WR LC data
Read WR time series data¶
Creates a dictionary datawr[key] which contains via key the following:
- 'IWR': weather regime index (standardised projection) after Michel & Rivière (2011) with the fields
time since 19790101_00 in hours ('tsince'), valid time ('time'),
cluster class index ('cci'), and the different
weather regime projections ('AT','ZO','ScTr','AR','EuBL','ScBL','GL');
note that order of weather regimes in files can change depending on dataset!
- 'MAXIWR': maximum weather regime index after Michel & Rivière (2011)
with the fields time since 19790101_00 in hours ('tsince'), valid time ('time'),
index of maximum weather regime projection ('wrindex'),
and name of maximum weather regime projection ('wrname');
note that assignment of wrindex to wrname can change depending on dataset!
- 'LC': full life cycle with the fields time since ('tsince') 19790101_00 in hours,
valid time ('time'), index of life cycle ('wrindex'), and name of life cycle ('wrname');
dtimes[:]: array of same length as datawr records, with times in tformat
# --------------------------------------------------------------------
# read wr projections
# --------------------------------------------------------------------
print("Read WR time series")
dtimes, datawr = wrera(start, end, hours, tformat, basepath)
# --------------------------------------------------------------------
times = [el.decode('UTF-8') for el in datawr['IWR']['time']] # times in string format
Read WR time series
Part 2: Plot weather regime indices IWR for all 7 regimes for a given period¶
select the time period to be plotted¶
# period to plot tseries, and to investigate anomalous WR frequencies in Part 3
start_date = "20241101_00"
end_date = "20250331_21"
#----- create indices for requested period ----
i1 = times.index(start_date)
i2 = times.index(end_date)
datelist_period=times[i1:i2]
# --------------------------------------------------------------------
# create an IWR timeseries with only the IWR for active life cycles otherwise undef
# needed for later plotting, based on a template by Seraphine Hauser
# --------------------------------------------------------------------
ntimes = len (times)
print(ntimes, " time steps considered from ", start, " to ",end)
# filtered IWR (only data for active LC)
IWRlc = np.zeros([nwr,ntimes])
IWRlc[:,:] = np.nan
# full IWRs
IWR = np.zeros([nwr,ntimes])
for w in range(0, nwr):
IWR[w,:] = datawr['IWR'][wrs_l[w]][:]
# assign IWR during active life cycles
for w in range(0,nwr):
n_lc = len(datawrlc[wrs_l[w]])
print("WR: ", wrs_l[w], " has ", n_lc, " life cycles.")
for i in range(0, n_lc):
#on=datawrlc[wrs_l[w]][i][1].decode()
ion = times.index(datawrlc[wrs_l[w]][i][1].decode())
idc = times.index(datawrlc[wrs_l[w]][i][5].decode())
IWRlc [w,ion:idc] = IWR[w,ion:idc]
220728 time steps considered from 19500111_00 to 20250726_21 WR: AT has 298 life cycles. WR: ZO has 253 life cycles. WR: ScTr has 330 life cycles. WR: AR has 315 life cycles. WR: EuBL has 316 life cycles. WR: ScBL has 309 life cycles. WR: GL has 315 life cycles.
#---- plot figure ------
plt.figure(figsize = (14, 5))
ymin = -4
ymax = 4
time = np.arange(0, len(datelist_period), 1)
# filtered IWR for active LC thick
for i in range(0, nwr):
plt.plot(time, IWRlc[i,i1:i2], color = colors_regimes[i], linewidth = 3.0, label = wrs_l[i])
plt.legend(loc = 'upper right', ncol = 3, fontsize=12)
# unfiltered IWR thin
for i in range(0, nwr):
plt.plot(time, IWR[i,i1:i2], color = colors_regimes[i], linewidth = 1.0, label = wrs_l[i])
plt.ylabel("Weather regime index (IWR)", fontsize = 15)
plt.xlabel("time", fontsize = 15)
# mark IWR=0,1 with a horizontal line
plt.hlines(0, len(datelist_period), 0, color = "black", linestyle = "solid", linewidth = 1)
plt.hlines(1, len(datelist_period), 0, color = "black", linestyle = "solid", linewidth = 0.5)
plt.ylim([ymin, ymax])
plt.xlim([0, len(datelist_period)])
plt.tick_params(axis="both", which='major', labelsize=13)
# plot filled polygons as a bar of maximum active WR (or none on bottom row) (LCattr)
plt.fill([0,i2-i1,i2-i1,0],[ymin+0.2, ymin+0.2, ymin, ymin],color = "grey")
x1 = i1
y = [ymin+0.2, ymin+0.2, ymin, ymin]
for i in range(i1,i2):
if (datawr['LC'][i+1][2]!= datawr['LC'][i][2]):
x = [x1-i1,i-i1,i-i1,x1-i1]
x1 = i+1
wr = datawr['LC'][i][3].decode()
colind = wrs_l.index(wr)
plt.fill(x,y,color = colors_regimes[colind])
# create ticks every first of a months
ticks = []
tick_labels = []
for i in range(i1,i2+1):
dd_hh=dtimes[i].strftime('%d_%H')
if dd_hh =='01_00':
ticks.append(i-i1)
tick_labels.append(dtimes[i].strftime('1 %b \n %Y'))
plt.vlines(i-i1, ymin, ymax, color = "black", linestyle = "dashed", linewidth = 0.5)
plt.xticks(ticks, labels = tick_labels, fontsize = 13)
pname = ('era5_tseries_%s_%s.png' % (start_date, end_date))
plt.savefig(pname,format="png",dpi=150, bbox_inches="tight",facecolor="white")
plt.show()
# define the climatological period MMDD_HH in each should reflect the reference season period you want
# to compare to, e.g. 19791201_00-20190228_21 for DJF climatological frequencies 1979-2019
start_clim = '19791101_00' # first date for reference climatology in Part 3 in format YYYYMMDD_HH
end_clim = '20190331_21' # last date climatology in Part 3
# based on a template by Seraphine Hauser
dstart = datetime.datetime.strptime(start_clim,'%Y%m%d_%H')
dend = datetime.datetime.strptime(end_clim,'%Y%m%d_%H')
nyears = int(dend.strftime('%Y'))-int(dstart.strftime('%Y'))+1
# annual frequencies
out_clim = np.zeros([nyears, nwr+2]) #[0]=no regime, 1-7 WR, 8 all time steps
# count time steps within mmdd_hh period for each year
for i in range(0 +int(dstart.strftime('%Y')), nyears +int(dstart.strftime('%Y'))):
i1 = times.index("%s%s" %(str(i), dstart.strftime('%m%d_%H')))
if (int(dstart.strftime('%m'))>int(dend.strftime('%m'))):
i2 = times.index("%s%s" %(str(i+1), dend.strftime('%m%d_%H')))
else:
i2 = times.index("%s%s" %(str(i), dend.strftime('%m%d_%H')))
#print(times[i1],times[i2])
n_counts = np.zeros([nwr+2]) #[0]=no regime, 1-7 WR, 8 all time steps
LCdata = datawr['LC']['wrindex'][i1:i2+1]
LCnamedata = [el.decode('UTF-8') for el in datawr['LC']['wrname'][i1:i2+1]]
# count all time steps in year
n_counts[nwr+1] = i2-i1+1
# count no regime time steps and compute frequency in year i
n_counts[0] = sum(np.where(LCdata==0,1,0))
out_clim[i-int(dstart.strftime('%Y')), 0] = (n_counts[0]*100.)/n_counts[nwr+1]
#print(i, n_counts[1], out_clim[i-int(dstart.strftime('%Y')),0])
for w in range(1,nwr+1): #iterates in wanted WR order as in wrs_l
# find first occurrence of wanted wr
if wrs_l[w-1] in LCnamedata:
wrind = LCdata[LCnamedata.index(wrs_l[w-1])]
n_counts[w] = sum(np.where(LCdata==wrind,1,0))
#print( wrs_l[w-1], " has index ", wrind)
else:
print(wrs_l[w-1], " did not occur in ", i)
# compute frequency in that year
out_clim[i -int(dstart.strftime('%Y')), w] = (n_counts[w]*100.)/n_counts[nwr+1]
out_clim[i -int(dstart.strftime('%Y')), nwr+1] = sum(out_clim[i -int(dstart.strftime('%Y')), 0:nwr+1])
#print(i, out_clim[i -int(dstart.strftime('%Y')),0:])
clim = np.mean(out_clim[1:int(dend.year-dstart.year),:], axis = 0)
print("climatological frequencies: ",clim)
AR did not occur in 1979 EuBL did not occur in 1981 ScBL did not occur in 1982 GL did not occur in 1982 GL did not occur in 1983 ScTr did not occur in 1984 AR did not occur in 1984 AR did not occur in 1985 ZO did not occur in 1987 ScTr did not occur in 1987 ScBL did not occur in 1987 ScBL did not occur in 1988 GL did not occur in 1988 AT did not occur in 1991 ScBL did not occur in 1991 GL did not occur in 1991 GL did not occur in 1992 AR did not occur in 1993 ZO did not occur in 1995 ScTr did not occur in 1995 ScBL did not occur in 1996 EuBL did not occur in 1998 ScBL did not occur in 1998 ScBL did not occur in 1999 GL did not occur in 1999 ZO did not occur in 2000 ScTr did not occur in 2000 AR did not occur in 2002 ZO did not occur in 2003 AT did not occur in 2004 ScTr did not occur in 2005 ScBL did not occur in 2006 GL did not occur in 2007 ZO did not occur in 2009 ScTr did not occur in 2009 EuBL did not occur in 2009 ScBL did not occur in 2010 GL did not occur in 2011 ZO did not occur in 2012 GL did not occur in 2013 GL did not occur in 2014 GL did not occur in 2016 EuBL did not occur in 2017 climatological frequencies: [ 25.86852104 11.84675544 13.81588723 12.75912495 10.7763359 8.54456011 7.27476964 9.1140457 100. ]
compute frequency in selected period¶
# counts and frequencies in period
n_counts = np.zeros([nwr+2]) #[0]=no regime, 1-7 WR, 8 all time steps
out_freq = np.zeros([nwr+2])
i1 = times.index(start_date)
i2 = times.index(end_date)
print("selected period:", times[i1],times[i2])
LCdata = datawr['LC']['wrindex'][i1:i2+1]
LCnamedata = [el.decode('UTF-8') for el in datawr['LC']['wrname'][i1:i2+1]]
# count all time steps in year
n_counts[nwr+1] = i2-i1+1
# count no regime time steps and compute frequency in year i
n_counts[0] = sum(np.where(LCdata==0,1,0))
out_freq[0] = (n_counts[0]*100.)/n_counts[nwr+1]
for w in range(1,nwr+1): #iterates in wanted WR order as in wrs_l
# find first occurrence of wanted wr
if wrs_l[w-1] in LCnamedata:
wrind = LCdata[LCnamedata.index(wrs_l[w-1])]
n_counts[w] = sum(np.where(LCdata==wrind,1,0))
else:
print(wrs_l[w-1], " did not occur in ", i)
# compute frequency in that year
out_freq[w] = (n_counts[w]*100.)/n_counts[nwr+1]
out_freq[nwr+1] = sum(out_freq[0:nwr+1])
#print(out_freq)
anomalies = np.zeros([9])
for k in range(0, 9):
anomalies[k] = out_freq[k] - clim[k]
#print(anomalies)
selected period: 20241101_00 20250331_21 ZO did not occur in 2019
# Create a figure with two subplots (2 rows, 1 column)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 6))
colors = ['grey']+colors_regimes[0:nwr]
wrs = ['no']+wrs_l[0:nwr]
fig.suptitle (("Regime frequency anomalies in period %s to %s" % (start_date, end_date)), fontsize=16)
# Plot positive values as vertical bars in the left subplot
ax1.bar(range(len(out_freq)-1), out_freq[0:nwr+1], color=colors)
ax1.set_xlabel('weather regime', fontsize = 13)
ax1.set_ylabel('regime frequency (in %)', fontsize = 13)
ax1.tick_params(axis='both', which='major', labelsize=13)
ax1.tick_params(axis='both', which='minor', labelsize=13)
ax2.tick_params(axis='both', which='major', labelsize=13)
ax2.tick_params(axis='both', which='minor', labelsize=13)
# Plot anomalies (positive and negative) as vertical bars in the right subplot
ax2.bar(range(len(anomalies)-1), anomalies[0:nwr+1], color=colors)
ax2.set_xlabel('weather regimes', fontsize = 13)
ax2.set_ylabel('deviation from climatological regime frequency (in %)', fontsize = 13)
ax2.hlines(0, -0.7, 9.5, linestyle = "solid", color = "black")
ax2.set_xlim(xmin=-0.7)
ax2.set_xlim(xmax=8)
ax1.set_xticks([0,1,2,3,4,5,6,7])
ax1.set_xticklabels(wrs, rotation=0, fontsize=13,ha='center')
ax2.set_xticks([0,1,2,3,4,5,6,7])
ax2.set_xticklabels(wrs, rotation=0, fontsize=13,ha='center')
ax1.set_title("(a)", fontsize = 14)
ax2.set_title("(b)", fontsize = 14)
# Adjust layout
plt.tight_layout()
pname = ('era5_freqano_%s_%s.png' % (start_date, end_date))
plt.savefig(pname,format="png",dpi=150, bbox_inches="tight",facecolor="white")
# Show plot
plt.show()
Part 4: Compute projection at an examplary time step¶
requires netcdf file with EOF-pattern (for normalization weights), netcdf file with regime pattern in Z0500, 10-day low-pass filtered geopotential height anomaly for the wanted time step
# -----------------------------------------------------------
# specify date string and datetime object
# -----------------------------------------------------------
time = "20250601_00"
dtime = datetime.datetime.strptime("%s" % (str(time)), "%Y%m%d_%H")
# -----------------------------------------------------------
# Open EOF-pattern file to retrieve normalization weights for normalising Z0500
# - can alternately be read from file 'wr_data/normweights_Z0500.txt'
# -----------------------------------------------------------
ncfile = Dataset("../wr_data/EOFs_WRs.nc")
normwgt = ncfile.variables['normwgt'][:]
nrmtimes = ncfile.variables['time'][:]
ncfile.close()
# find index of normwgt for calendar time corresponding to wanted time step
# - nrmwgt array index 0-1463 correspond to 6-hourly time step in a leap year (including 29 February)
# - thus array index 236 refers to 00 UTC 29 February, index 240 to 00 UTC 1 March.
# - 29 February has to be overstepped for a non-leap year
#nrmtimes = create_datelist(d1='19800101_00', d2='19801231_18', dt=int(6))
#print(nrmtimes[0],nrmtimes[236],nrmtimes[240],nrmtimes[1463])
# but in the following we will use hours since 19800101_00 instead
#nrmtimes = np.arange(0,len(normwgt)*6,6)
#print(nrmtimes[236])
# compute time difference of wanted date to 1st January 00 UTC
tdiff = dtime - datetime.datetime(dtime.year,1,1,0,0,0,0)
tdiff = int(tdiff.total_seconds()/3600)
tdiff = tdiff - tdiff%6 # adjust to a multiple of 6 hours (use 00 UTC weight for 03 UTC, 06 UTC for 09 UTC, ...)
if (tdiff >= nrmtimes[236]) and (not calendar.isleap(dtime.year)): # if date is beyond 28 February and not a leap year, skip one index
#print("time step > 28 February and not a leap year")
nj=nrmtimes.tolist().index(tdiff+24)
else: #leap year or prior to 29 February
#print("time step < 29 February or a leap year")
nj=nrmtimes.tolist().index(tdiff)
#print(nj)
# -----------------------------------------------------------
# Open 10-day low-pass filtered geopotential height anomaly
# -----------------------------------------------------------
ncfile = Dataset("../example_data/Z0500_20250601_00.nc")
# get indices for EOF Domain
lon = np.arange(ncfile.domxmin,ncfile.domxmax+0.5,0.5)
lat = np.arange(ncfile.domymin,ncfile.domymax+0.5,0.5)
i1 = lon.tolist().index(-80.0)
i2 = lon.tolist().index(40.0)+1
j1 = lat.tolist().index(30.0)
j2 = lat.tolist().index(90.0)+1
#print(lon[i1:i2])
#print(lat[j1:j2])
# Read data in EOF domain
data = ncfile.variables['Z0'][0,0,j1:j2,i1:i2]
ncfile.close()
# normalize data
data[:,:] = data[:,:]/ normwgt[nj]
# -----------------------------------------------------------
# Open regime patterns which are stored normalised 10-day low-pass filtered geopotential height anomaly in EOF Domain
# -----------------------------------------------------------
ncfile = Dataset("../wr_data/Normed_Z0500-patterns_EOFdomain.nc")
# Read normalized data in EOF domain
mean = ncfile.variables['Z0500_mean'][:,:,:]
wrnames = ncfile.ClassNames.split()
ncfile.close()
# -----------------------------------------------------------
# Compute projection and weather regime indices IWR
# -----------------------------------------------------------
proj = np.zeros([nwr,1])
test_IWR = np.zeros([nwr,1])
i = 0 # time iterator if it would be done over multiple time steps
d2r = 4.0*np.atan(1.0)/180. # pi/180°
# 2D field with cosine of the latitude at each Gridpoint in EOF domain for latitudinal weigthing
cos2d = np.column_stack([np.cos(lat[j1:j2]*d2r).tolist()]*len(lon[i1:i2]))
cos2d = cos2d / sum(cos2d)
for w in range(0,nwr): #iterates in saved order as in pattern file, and reorders in wanted order wrs_l
proj[wrs_l.index(wrnames[w]),i] = sum(cos2d[:,:]*mean[w,:,:]*data[:,:])
# read standardisation parameters
stdprm_file = open("../wr_data/WRI_std_params.txt","r")
stdnames = stdprm_file.readline() # skip one line
stdnames = stdprm_file.readline().split()
stdmean = np.array(stdprm_file.readline().split()[1:],dtype=float)
stdstdd = np.array(stdprm_file.readline().split()[1:],dtype=float)
print("weather regime indices for ", time)
for w in range(0,nwr):
test_IWR[w,i] = (proj[w,i]-stdmean[stdnames.index(wrs_l[w])])/stdstdd[stdnames.index(wrs_l[w])]
print(test_IWR[w,i], wrs_l[w])
# Numbers slightly differ from those saved in projection time series due to numerical differences
# between the original ncl-Routines and this independent python implementation
weather regime indices for 20250601_00 0.8218320408050166 AT 1.1691867614026132 ZO 1.0489664646400954 ScTr -0.6948883613466961 AR -0.5738999234610754 EuBL -0.9323144170607468 ScBL -0.6369122243737739 GL
Part 5: Plot WR Z500 pattern as of EOF attribution¶
plots the original regime patterns as normalised anomalies 10-day low-pass filtered 500hPa geopotential height anomaly and approximate absolute field using the year-round Z500 (unfiltered) climatology 1979-2019
uses NCL color tables, therefore a lengthy cell which facilitates access to these.
# -----------------------------------------------------------
# Open climatology
# -----------------------------------------------------------
ncfile = Dataset("../example_data/CLIM_Z@500_year_1979-2019.nc")
Z500_cli = ncfile.variables['Z@500'][:,:]
ncfile.close()
# -----------------------------------------------------------
# Open regime patterns as non-normalized global field of 10-day low-pass filtered geopotential height anomaly
# -----------------------------------------------------------
ncfile = Dataset("../wr_data/Clusters_WRs.nc")
Z0500_mean = ncfile.variables['Z0500_mean'][:,:,:]
lat = ncfile.variables['latitude'][:]
lon = ncfile.variables['longitude'][:]
wrnames = ncfile.ClassNames.split()
ncfile.close()
# -----------------------------------------------------------
# Prepare Plot
# -----------------------------------------------------------
cmap = get_NCL_colormap('BlueWhiteOrangeRed')
proj_ccrs = ccrs.Stereographic(central_longitude=-20.0,central_latitude=50, globe=None)
proj_ccrs._threshold=proj_ccrs._threshold/10.
fig, axs = plt.subplots(nrows=2,ncols=4,
subplot_kw={'projection': proj_ccrs},
figsize=(11,5.5))
data_ccrs = ccrs.PlateCarree()
cint = np.arange(-220,230,20)
cint2= np.arange(-220,230,40)
cint3= np.arange(5520,5600,80)
cint4= np.arange(4320,6720,80)
for w in range(0,nwr):
if w<3:
i=0
j=w
else:
i=1
j=w-3
axs[i,j].set_extent([-80, 40, 29, 90],crs = data_ccrs)
plotdata1 = axs[i,j].contourf(lon, lat , Z0500_mean[wrnames.index(wrs_l[w]),:,:], cint, \
transform=data_ccrs,cmap=cmap, extend='both')
#plotdata2 = axs[i,j].contour(lon, lat , Z0500_mean[wrnames.index(wrs_l[w]),:,:], cint, colors=["gray"], linewidths=0.5, \
# transform=data_ccrs)
#axs[i,j].clabel(plotdata2, levels = cint2, fontsize=8)
plotdata3 = axs[i,j].contour(lon, lat , Z0500_mean[wrnames.index(wrs_l[w]),:,:]+Z500_cli[:,:]/9.81, cint3, colors=["black"], \
linewidths=1.0, transform=data_ccrs)
axs[i,j].clabel(plotdata3, levels = cint3, fontsize=10)
plotdata4 = axs[i,j].contour(lon, lat , Z0500_mean[wrnames.index(wrs_l[w]),:,:]+Z500_cli[:,:]/9.81, cint4, colors=["black"], \
linewidths=0.5, transform=data_ccrs)
axs[i,j].set_title(wrs_l[w], fontsize=12)
gl = axs[i,j].gridlines(crs=data_ccrs, draw_labels=False, xlocs=[-90,-60,-30,0,30,60], ylocs=[30,60],linewidth=0.5, color='grey', \
alpha=1.0, linestyle='dotted')
axs[i,j].coastlines(resolution='50m', linewidth=0.5, color='0.2')
axs[i,j].add_feature(cartopy.feature.LAND,facecolor='0.95')
fig.delaxes(axs[0,3])
fig.subplots_adjust(bottom=0.2, top=0.8, left=0.1, right=0.9,
wspace=0.02, hspace=0.02)
cbar_ax = fig.add_axes([0.2, 0.2, 0.6, 0.02])
cbar = fig.colorbar(plotdata1, cax=cbar_ax, ticks = cint2[1:]-20, pad=0.01, aspect=30, shrink=0.97, orientation = 'horizontal', \
extendrect=False)
cbar.ax.tick_params(labelsize=12)
cbar.set_label(label='10-day low-pass filtered anomaly in geopotential metres (gpm)', size=12)
pname = ('Panel_7WR_EOFattr.png')
plt.savefig(pname,format="png",dpi=150, bbox_inches="tight",facecolor="white")
plt.show()