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¶

In [1]:
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")
In [2]:
## 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
In [ ]:
 

Configurations for reading data¶

In [3]:
# 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
In [4]:
# --------------------------------------------------------------------
# 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

In [5]:
# --------------------------------------------------------------------
# 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¶

In [6]:
# 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]
In [7]:
# --------------------------------------------------------------------
# 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.
In [8]:
#---- 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()
No description has been provided for this image

Part 3: Frequency anomaly in the given period¶

compute climatology¶

In [9]:
# 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¶

In [10]:
# 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
In [11]:
# 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()
No description has been provided for this image

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

In [12]:
# -----------------------------------------------------------
# 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.

In [13]:
# -----------------------------------------------------------
# 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()
In [14]:
# -----------------------------------------------------------
# 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()
No description has been provided for this image
In [ ]: