jcclass 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jcclass/__init__.py +4 -0
- jcclass/compute/__init__.py +3 -0
- jcclass/compute/core.py +74 -0
- jcclass/compute/functions/__init__.py +1 -0
- jcclass/compute/functions/computation.py +192 -0
- jcclass/compute/functions/constants.py +64 -0
- jcclass/compute/functions/data_extraction.py +145 -0
- jcclass/compute/functions/data_preparation.py +114 -0
- jcclass/compute/functions/format_data.py +49 -0
- jcclass/compute/functions/main.py +53 -0
- jcclass/plotting/__init__.py +3 -0
- jcclass/plotting/core.py +108 -0
- jcclass/plotting/functions/__init__.py +0 -0
- jcclass/plotting/functions/plot_utils.py +161 -0
- jcclass/plotting/functions/tools.py +88 -0
- jcclass/utils/__init__.py +0 -0
- jcclass/utils/logging_config.py +22 -0
- jcclass-0.0.1.dist-info/METADATA +148 -0
- jcclass-0.0.1.dist-info/RECORD +21 -0
- jcclass-0.0.1.dist-info/WHEEL +5 -0
- jcclass-0.0.1.dist-info/top_level.txt +1 -0
jcclass/__init__.py
ADDED
jcclass/compute/core.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import xarray as xr
|
|
2
|
+
from .functions.main import jc_classification
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def compute_cts(data_mslp: xr.DataArray) -> xr.DataArray:
|
|
6
|
+
"""
|
|
7
|
+
Computes the Jenkinson and Collison Circulation Types (CTs) based on
|
|
8
|
+
Mean Sea Level Pressure (MSLP) data.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
data_mslp (xr.DataArray): Input MSLP data as an xarray DataArray.
|
|
12
|
+
- Dimensions: Typically includes "time", "latitude", and "longitude".
|
|
13
|
+
- Units: Should be in Pascals (Pa) or Hectopascals (hPa).
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
xr.DataArray: Computed circulation types as an xarray DataArray.
|
|
17
|
+
- Dimensions: Same as input, with "time", "latitude", and "longitude".
|
|
18
|
+
- Values: Integer codes representing circulation types.
|
|
19
|
+
- Codes range from 0 to 28, with -1 indicating unclassified flows.
|
|
20
|
+
- Attributes: Includes metadata describing the circulation type calculation.
|
|
21
|
+
|
|
22
|
+
Notes:
|
|
23
|
+
- The classification is derived using a gridded version of the Lamb Weather Types.
|
|
24
|
+
- Ensure the input dataset has global or regional coverage with appropriate
|
|
25
|
+
spatial and temporal resolution.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> import xarray as xr
|
|
29
|
+
>>> from jcclass.compute.core import compute_cts
|
|
30
|
+
>>> data_mslp = xr.open_dataset("mslp_data.nc").msl
|
|
31
|
+
>>> cts = compute_cts(data_mslp)
|
|
32
|
+
>>> print(cts)
|
|
33
|
+
"""
|
|
34
|
+
ds = jc_classification(data_mslp)
|
|
35
|
+
return ds
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def eleven_cts(cts: xr.DataArray) -> xr.DataArray:
|
|
39
|
+
"""
|
|
40
|
+
Reduces the 27 Lamb Weather Types (LWT) circulation types to 11 types
|
|
41
|
+
based on their advective characteristics.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
cts (xr.DataArray): DataArray containing the 27 circulation types.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
xr.DataArray: DataArray with reduced 11 circulation types.
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
>>> import xarray as xr
|
|
51
|
+
>>> from jcclass.compute.core import eleven_cts
|
|
52
|
+
>>> cts = xr.open_dataset("cts_data.nc").lwt
|
|
53
|
+
>>> cts_11 = eleven_cts(cts)
|
|
54
|
+
>>> print(cts_11)
|
|
55
|
+
"""
|
|
56
|
+
mapping = {
|
|
57
|
+
1: [11, 21, 1], # NE
|
|
58
|
+
2: [12, 22, 2], # E
|
|
59
|
+
3: [13, 23, 3], # SE
|
|
60
|
+
4: [14, 24, 4], # S
|
|
61
|
+
5: [15, 25, 5], # SW
|
|
62
|
+
6: [16, 26, 6], # W
|
|
63
|
+
7: [17, 27, 7], # NW
|
|
64
|
+
8: [18, 28, 8], # N
|
|
65
|
+
9: [20], # Cyclonic
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Apply the mapping
|
|
69
|
+
for reduced_type, original_types in mapping.items():
|
|
70
|
+
cts = xr.where(cts.isin(original_types), reduced_type, cts)
|
|
71
|
+
|
|
72
|
+
return cts
|
|
73
|
+
|
|
74
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import xarray as xr
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def flows(gridpoints, sc, zwa, zsc, zwb, latitude, longitude, time, mslp_data):
|
|
6
|
+
"""
|
|
7
|
+
Computes indices associated with the direction and vorticity of geostrophic flow
|
|
8
|
+
given a reanalysis or GCM dataset.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
gridpoints (tuple): A tuple containing the 16 gridded MSLP values (p1 to p16).
|
|
12
|
+
sc (xr.DataArray): Longitudinal scaling factor.
|
|
13
|
+
zwa (xr.DataArray): Zonal weighting factor (latitude - 5 degrees).
|
|
14
|
+
zwb (xr.DataArray): Zonal weighting factor (latitude + 5 degrees).
|
|
15
|
+
zsc (xr.DataArray): Shear constant.
|
|
16
|
+
latitude (xr.DataArray): Latitude values of the dataset.
|
|
17
|
+
longitude (xr.DataArray): Longitude values of the dataset.
|
|
18
|
+
time (xr.DataArray): Time values of the dataset.
|
|
19
|
+
mslp_data (xr.DataArray): Mean sea level pressure dataset.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
tuple: (W, S, F, ZW, ZS, Z)
|
|
23
|
+
W: Westerly flow
|
|
24
|
+
S: Southerly flow
|
|
25
|
+
F: Resultant flow
|
|
26
|
+
ZW: Westerly shear vorticity
|
|
27
|
+
ZS: Southerly shear vorticity
|
|
28
|
+
Z: Total shear vorticity
|
|
29
|
+
"""
|
|
30
|
+
# Unpack gridpoints
|
|
31
|
+
(p1, p2, p3, p4, p5, p6, p7, p8,
|
|
32
|
+
p9, p10, p11, p12, p13, p14, p15, p16) = gridpoints
|
|
33
|
+
|
|
34
|
+
# Ensure sc, zwa, zsc, and zwb match the dimensions of MSLP
|
|
35
|
+
sc = sc.expand_dims(dim={"time": time}, axis=0)
|
|
36
|
+
zwa = zwa.expand_dims(dim={"time": time}, axis=0)
|
|
37
|
+
zsc = zsc.expand_dims(dim={"time": time}, axis=0)
|
|
38
|
+
zwb = zwb.expand_dims(dim={"time": time}, axis=0)
|
|
39
|
+
|
|
40
|
+
# Westerly Flow
|
|
41
|
+
W = (0.5 * (p12 + p13)) - (0.5 * (p4 + p5))
|
|
42
|
+
if mslp_data.dims[1] == "latitude":
|
|
43
|
+
W = xr.DataArray(W, coords={"time": time, "latitude": latitude, "longitude": longitude}, dims=["time", "latitude", "longitude"])
|
|
44
|
+
elif mslp_data.dims[1] == "number":
|
|
45
|
+
W = xr.DataArray(W, coords={"time": time, "number": mslp_data.number, "latitude": latitude, "longitude": longitude}, dims=["time", "number", "latitude", "longitude"])
|
|
46
|
+
|
|
47
|
+
# Southerly Flow
|
|
48
|
+
S = sc * ((0.25 * (p5 + 2 * p9 + p13)) - (0.25 * (p4 + 2 * p8 + p12)))
|
|
49
|
+
if mslp_data.dims[1] == "latitude":
|
|
50
|
+
S = xr.DataArray(S, coords={"time": time, "latitude": latitude, "longitude": longitude}, dims=["time", "latitude", "longitude"])
|
|
51
|
+
elif mslp_data.dims[1] == "number":
|
|
52
|
+
S = xr.DataArray(S, coords={"time": time, "number": mslp_data.number, "latitude": latitude, "longitude": longitude}, dims=["time", "number", "latitude", "longitude"])
|
|
53
|
+
|
|
54
|
+
# Resultant Flow
|
|
55
|
+
F = np.sqrt(S**2 + W**2)
|
|
56
|
+
|
|
57
|
+
# Westerly Shear Vorticity
|
|
58
|
+
ZW = (zwa * (0.5 * (p15 + p16) - 0.5 * (p8 + p9))) - (zwb * (0.5 * (p8 + p9) - 0.5 * (p1 + p2)))
|
|
59
|
+
if mslp_data.dims[1] == "latitude":
|
|
60
|
+
ZW = xr.DataArray(ZW, coords={"time": time, "latitude": latitude, "longitude": longitude}, dims=["time", "latitude", "longitude"])
|
|
61
|
+
elif mslp_data.dims[1] == "number":
|
|
62
|
+
ZW = xr.DataArray(ZW, coords={"time": time, "number": mslp_data.number, "latitude": latitude, "longitude": longitude}, dims=["time", "number", "latitude", "longitude"])
|
|
63
|
+
|
|
64
|
+
# Southerly Shear Vorticity
|
|
65
|
+
ZS = zsc * ((0.25 * (p6 + 2 * p10 + p14)) - (0.25 * (p5 + 2 * p9 + p13)) - (0.25 * (p4 + 2 * p8 + p12)) + (0.25 * (p3 + 2 * p7 + p11)))
|
|
66
|
+
if mslp_data.dims[1] == "latitude":
|
|
67
|
+
ZS = xr.DataArray(ZS, coords={"time": time, "latitude": latitude, "longitude": longitude}, dims=["time", "latitude", "longitude"])
|
|
68
|
+
elif mslp_data.dims[1] == "number":
|
|
69
|
+
ZS = xr.DataArray(ZS, coords={"time": time, "number": mslp_data.number, "latitude": latitude, "longitude": longitude}, dims=["time", "number", "latitude", "longitude"])
|
|
70
|
+
|
|
71
|
+
# Total Shear Vorticity
|
|
72
|
+
Z = ZW + ZS
|
|
73
|
+
|
|
74
|
+
return W, S, F, ZW, ZS, Z
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def compute_direction(deg, latitude):
|
|
78
|
+
"""
|
|
79
|
+
Assigns wind direction labels based on wind direction degrees and hemisphere.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
deg (xr.DataArray): Wind direction values in degrees.
|
|
83
|
+
latitude (xr.DataArray): Latitude values to determine the hemisphere.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
xr.DataArray: Wind direction labels as strings.
|
|
87
|
+
"""
|
|
88
|
+
# Define the direction labels for both hemispheres
|
|
89
|
+
nh_labels = {
|
|
90
|
+
"W": (247, 292),
|
|
91
|
+
"NW": (292, 337),
|
|
92
|
+
"N": (337, 22),
|
|
93
|
+
"NE": (22, 67),
|
|
94
|
+
"E": (67, 112),
|
|
95
|
+
"SE": (112, 157),
|
|
96
|
+
"S": (157, 202),
|
|
97
|
+
"SW": (202, 247),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
sh_labels = {
|
|
101
|
+
"E": (247, 292),
|
|
102
|
+
"SE": (292, 337),
|
|
103
|
+
"S": (337, 22),
|
|
104
|
+
"SW": (22, 67),
|
|
105
|
+
"W": (67, 112),
|
|
106
|
+
"NW": (112, 157),
|
|
107
|
+
"N": (157, 202),
|
|
108
|
+
"NE": (202, 247),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Initialize direction as NaN
|
|
112
|
+
direction = xr.full_like(deg, fill_value=np.nan, dtype=object)
|
|
113
|
+
|
|
114
|
+
# Determine Northern Hemisphere directions
|
|
115
|
+
for label, (lower, upper) in nh_labels.items():
|
|
116
|
+
if lower <= upper:
|
|
117
|
+
direction = xr.where((latitude >= 0) & (deg > lower) & (deg <= upper), label, direction)
|
|
118
|
+
else: # Handle wrap-around (e.g., N: 337-22)
|
|
119
|
+
direction = xr.where((latitude >= 0) & ((deg > lower) | (deg <= upper)), label, direction)
|
|
120
|
+
|
|
121
|
+
# Determine Southern Hemisphere directions
|
|
122
|
+
for label, (lower, upper) in sh_labels.items():
|
|
123
|
+
if lower <= upper:
|
|
124
|
+
direction = xr.where((latitude < 0) & (deg > lower) & (deg <= upper), label, direction)
|
|
125
|
+
else: # Handle wrap-around (e.g., S: 337-22)
|
|
126
|
+
direction = xr.where((latitude < 0) & ((deg > lower) | (deg <= upper)), label, direction)
|
|
127
|
+
|
|
128
|
+
return direction
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def assign_lwt(F_i, Z_i, direction_i):
|
|
132
|
+
"""
|
|
133
|
+
Assigns circulation type codes (Lamb Weather Types) based on flow and vorticity.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
F_i (xr.DataArray): Total flow term (F).
|
|
137
|
+
Z_i (xr.DataArray): Total shear vorticity term (Z).
|
|
138
|
+
direction_i (xr.DataArray): Flow direction.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
xr.DataArray: Circulation type classification for each grid point.
|
|
142
|
+
- Codes range from 0 (purely anticyclonic) to 28 (directional flows).
|
|
143
|
+
- -1 indicates weak/unclassified flow.
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> lwt = assign_lwt(F, Z, direction)
|
|
147
|
+
>>> print(lwt)
|
|
148
|
+
"""
|
|
149
|
+
if not isinstance(F_i, xr.DataArray) or not isinstance(Z_i, xr.DataArray) or not isinstance(direction_i, xr.DataArray):
|
|
150
|
+
raise TypeError("F_i, Z_i, and direction_i must all be xarray.DataArray objects.")
|
|
151
|
+
|
|
152
|
+
# Hybrid Anticyclonic flows
|
|
153
|
+
lwt = xr.where((Z_i < 0) & (direction_i == 'NE'), 1, np.nan)
|
|
154
|
+
lwt = xr.where((Z_i < 0) & (direction_i == 'E'), 2, lwt)
|
|
155
|
+
lwt = xr.where((Z_i < 0) & (direction_i == 'SE'), 3, lwt)
|
|
156
|
+
lwt = xr.where((Z_i < 0) & (direction_i == 'S'), 4, lwt)
|
|
157
|
+
lwt = xr.where((Z_i < 0) & (direction_i == 'SW'), 5, lwt)
|
|
158
|
+
lwt = xr.where((Z_i < 0) & (direction_i == 'W'), 6, lwt)
|
|
159
|
+
lwt = xr.where((Z_i < 0) & (direction_i == 'NW'), 7, lwt)
|
|
160
|
+
lwt = xr.where((Z_i < 0) & (direction_i == 'N'), 8, lwt)
|
|
161
|
+
|
|
162
|
+
# Hybrid Cyclonic flows
|
|
163
|
+
lwt = xr.where((np.abs(Z_i) < F_i) & (direction_i == 'NE'), 11, lwt)
|
|
164
|
+
lwt = xr.where((np.abs(Z_i) < F_i) & (direction_i == 'E'), 12, lwt)
|
|
165
|
+
lwt = xr.where((np.abs(Z_i) < F_i) & (direction_i == 'SE'), 13, lwt)
|
|
166
|
+
lwt = xr.where((np.abs(Z_i) < F_i) & (direction_i == 'S'), 14, lwt)
|
|
167
|
+
lwt = xr.where((np.abs(Z_i) < F_i) & (direction_i == 'SW'), 15, lwt)
|
|
168
|
+
lwt = xr.where((np.abs(Z_i) < F_i) & (direction_i == 'W'), 16, lwt)
|
|
169
|
+
lwt = xr.where((np.abs(Z_i) < F_i) & (direction_i == 'NW'), 17, lwt)
|
|
170
|
+
lwt = xr.where((np.abs(Z_i) < F_i) & (direction_i == 'N'), 18, lwt)
|
|
171
|
+
|
|
172
|
+
# Purely Cyclonic
|
|
173
|
+
lwt = xr.where((np.abs(Z_i) > (2 * F_i)) & (Z_i > 0), 20, lwt)
|
|
174
|
+
|
|
175
|
+
# Purely Anticyclonic
|
|
176
|
+
lwt = xr.where((np.abs(Z_i) > (2 * F_i)) & (Z_i < 0), 0, lwt)
|
|
177
|
+
|
|
178
|
+
# Directional flows
|
|
179
|
+
lwt = xr.where((np.abs(Z_i) > F_i) & (np.abs(Z_i) < (2 * F_i)) & (Z_i > 0) & (direction_i == 'NE'), 21, lwt)
|
|
180
|
+
lwt = xr.where((np.abs(Z_i) > F_i) & (np.abs(Z_i) < (2 * F_i)) & (Z_i > 0) & (direction_i == 'E'), 22, lwt)
|
|
181
|
+
lwt = xr.where((np.abs(Z_i) > F_i) & (np.abs(Z_i) < (2 * F_i)) & (Z_i > 0) & (direction_i == 'SE'), 23, lwt)
|
|
182
|
+
lwt = xr.where((np.abs(Z_i) > F_i) & (np.abs(Z_i) < (2 * F_i)) & (Z_i > 0) & (direction_i == 'S'), 24, lwt)
|
|
183
|
+
lwt = xr.where((np.abs(Z_i) > F_i) & (np.abs(Z_i) < (2 * F_i)) & (Z_i > 0) & (direction_i == 'SW'), 25, lwt)
|
|
184
|
+
lwt = xr.where((np.abs(Z_i) > F_i) & (np.abs(Z_i) < (2 * F_i)) & (Z_i > 0) & (direction_i == 'W'), 26, lwt)
|
|
185
|
+
lwt = xr.where((np.abs(Z_i) > F_i) & (np.abs(Z_i) < (2 * F_i)) & (Z_i > 0) & (direction_i == 'NW'), 27, lwt)
|
|
186
|
+
lwt = xr.where((np.abs(Z_i) > F_i) & (np.abs(Z_i) < (2 * F_i)) & (Z_i > 0) & (direction_i == 'N'), 28, lwt)
|
|
187
|
+
|
|
188
|
+
# Low Flow / Unclassified / Weak Flow
|
|
189
|
+
lwt = xr.where((F_i < 6) & (np.abs(Z_i) < 6), -1, lwt)
|
|
190
|
+
|
|
191
|
+
return lwt
|
|
192
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import xarray as xr
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def compute_constants(phi: xr.DataArray, lon: xr.DataArray) -> tuple:
|
|
6
|
+
"""
|
|
7
|
+
Computes constants dependent on latitude and longitude for grid-point spacing.
|
|
8
|
+
These constants represent relative differences in the E-W and N-S grid-point spacing.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
phi (xr.DataArray): Central latitude grid points (1D array).
|
|
12
|
+
lon (xr.DataArray): Longitude values (1D array).
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
tuple:
|
|
16
|
+
sc (xr.DataArray): Longitudinal scaling factor.
|
|
17
|
+
zwa (xr.DataArray): Zonal weighting factor (latitude - 5 degrees).
|
|
18
|
+
zwb (xr.DataArray): Zonal weighting factor (latitude + 5 degrees).
|
|
19
|
+
zsc (xr.DataArray): Shear constant.
|
|
20
|
+
"""
|
|
21
|
+
if not isinstance(phi, xr.DataArray) or not isinstance(lon, xr.DataArray):
|
|
22
|
+
raise TypeError("Both phi (latitude) and lon (longitude) must be xarray.DataArray objects.")
|
|
23
|
+
|
|
24
|
+
# Validate dimensions
|
|
25
|
+
if "latitude" not in phi.dims or len(phi.dims) != 1:
|
|
26
|
+
raise ValueError("phi must be a 1D xarray.DataArray with 'latitude' as its dimension.")
|
|
27
|
+
if "longitude" not in lon.dims or len(lon.dims) != 1:
|
|
28
|
+
raise ValueError("lon must be a 1D xarray.DataArray with 'longitude' as its dimension.")
|
|
29
|
+
|
|
30
|
+
# Longitudinal scaling factor
|
|
31
|
+
sc = 1 / np.cos(np.deg2rad(phi))
|
|
32
|
+
sc = xr.DataArray(
|
|
33
|
+
np.repeat(sc.values[:, None], len(lon), axis=1),
|
|
34
|
+
coords={"latitude": phi, "longitude": lon},
|
|
35
|
+
dims=["latitude", "longitude"],
|
|
36
|
+
name="sc"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Zonal weighting factors
|
|
40
|
+
zwa = np.sin(np.deg2rad(phi)) / np.sin(np.deg2rad(phi - 5))
|
|
41
|
+
zwb = np.sin(np.deg2rad(phi)) / np.sin(np.deg2rad(phi + 5))
|
|
42
|
+
zwa = xr.DataArray(
|
|
43
|
+
np.repeat(zwa.values[:, None], len(lon), axis=1),
|
|
44
|
+
coords={"latitude": phi, "longitude": lon},
|
|
45
|
+
dims=["latitude", "longitude"],
|
|
46
|
+
name="zwa"
|
|
47
|
+
)
|
|
48
|
+
zwb = xr.DataArray(
|
|
49
|
+
np.repeat(zwb.values[:, None], len(lon), axis=1),
|
|
50
|
+
coords={"latitude": phi, "longitude": lon},
|
|
51
|
+
dims=["latitude", "longitude"],
|
|
52
|
+
name="zwb"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Shear constant
|
|
56
|
+
zsc = 1 / (2 * (np.cos(np.deg2rad(phi)) ** 2))
|
|
57
|
+
zsc = xr.DataArray(
|
|
58
|
+
np.repeat(zsc.values[:, None], len(lon), axis=1),
|
|
59
|
+
coords={"latitude": phi, "longitude": lon},
|
|
60
|
+
dims=["latitude", "longitude"],
|
|
61
|
+
name="zsc"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return sc, zwa, zwb, zsc
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import xarray as xr
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def extract_lat_lon_points(mslp_data: xr.DataArray) -> tuple:
|
|
6
|
+
"""
|
|
7
|
+
Extracts latitude and longitude points from the MSLP data within the
|
|
8
|
+
latitude range [-80, 80] degrees.
|
|
9
|
+
Args:
|
|
10
|
+
mslp_data (xr.DataArray): Input MSLP data with standardized coordinates
|
|
11
|
+
Returns:
|
|
12
|
+
tuple: Latitude and Longitude points within the range [-80, 80] degrees
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
min_lat, max_lat = (-80.0, 80.0)
|
|
16
|
+
psl_area = mslp_data.where((mslp_data.latitude >= min_lat) & (mslp_data.latitude <= max_lat), drop=True)
|
|
17
|
+
|
|
18
|
+
lat = psl_area.latitude
|
|
19
|
+
lon = psl_area.longitude
|
|
20
|
+
|
|
21
|
+
return lat, lon
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def extracting_gridpoints_area(mslp, latitude, longitude):
|
|
25
|
+
"""
|
|
26
|
+
Extracts 16 gridded points over a defined area for a Reanalysis or Global Climate Model dataset.
|
|
27
|
+
These grid points are required for computing terms related to circulation classification.
|
|
28
|
+
|
|
29
|
+
The function computes grid points based on predefined latitude and longitude offsets from a central
|
|
30
|
+
point, ensuring that the dataset is not assumed to cover the entire globe.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
mslp (xr.DataArray): Mean sea level pressure data with latitude and longitude coordinates.
|
|
34
|
+
latitude (xr.DataArray): Latitude values of the dataset.
|
|
35
|
+
longitude (xr.DataArray): Longitude values of the dataset.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
tuple: A tuple of 16 `numpy.ndarray` objects, each corresponding to a grid point value of `mslp`
|
|
39
|
+
at the calculated latitude and longitude offsets.
|
|
40
|
+
|
|
41
|
+
Latitude and Longitude Offsets:
|
|
42
|
+
Grid Point 1: (+10, -5)
|
|
43
|
+
Grid Point 2: (+10, +5)
|
|
44
|
+
Grid Point 3: (+5, -15)
|
|
45
|
+
Grid Point 4: (+5, -5)
|
|
46
|
+
Grid Point 5: (+5, +5)
|
|
47
|
+
Grid Point 6: (+5, +15)
|
|
48
|
+
Grid Point 7: (+0, -15)
|
|
49
|
+
Grid Point 8: (+0, -5)
|
|
50
|
+
Grid Point 9: (+0, +5)
|
|
51
|
+
Grid Point 10: (+0, +15)
|
|
52
|
+
Grid Point 11: (-5, -15)
|
|
53
|
+
Grid Point 12: (-5, -5)
|
|
54
|
+
Grid Point 13: (-5, +5)
|
|
55
|
+
Grid Point 14: (-5, +15)
|
|
56
|
+
Grid Point 15: (-10, -5)
|
|
57
|
+
Grid Point 16: (-10, +5)
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> latitude = xr.DataArray(np.linspace(-90, 90, 181), dims="latitude", name="latitude")
|
|
61
|
+
>>> longitude = xr.DataArray(np.linspace(-180, 180, 361), dims="longitude", name="longitude")
|
|
62
|
+
>>> mslp = xr.DataArray(np.random.rand(181, 361), coords={"latitude": latitude, "longitude": longitude}, dims=["latitude", "longitude"])
|
|
63
|
+
>>> gridpoints = extracting_gridpoints_area(mslp, latitude, longitude)
|
|
64
|
+
>>> print(gridpoints[0]) # First grid point value
|
|
65
|
+
"""
|
|
66
|
+
offsets = [
|
|
67
|
+
(10, -5), (10, 5), (5, -15), (5, -5), (5, 5), (5, 15), (0, -15), (0, -5),
|
|
68
|
+
(0, 5), (0, 15), (-5, -15), (-5, -5), (-5, 5), (-5, 15), (-10, -5), (-10, 5)
|
|
69
|
+
]
|
|
70
|
+
gridpoints = []
|
|
71
|
+
|
|
72
|
+
for lat_offset, lon_offset in offsets:
|
|
73
|
+
lat_point = latitude + lat_offset
|
|
74
|
+
lon_point = longitude + lon_offset
|
|
75
|
+
|
|
76
|
+
lat_point = latitude.sel(latitude=lat_point, method="nearest")
|
|
77
|
+
lon_point = longitude.sel(longitude=lon_point, method="nearest")
|
|
78
|
+
|
|
79
|
+
gridpoints.append(np.array(mslp.sel(latitude=lat_point, longitude=lon_point)))
|
|
80
|
+
|
|
81
|
+
return tuple(gridpoints)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def extracting_gridpoints_globe(mslp, latitude, longitude):
|
|
85
|
+
"""
|
|
86
|
+
Extracts 16 gridded points over the entire globe for a Reanalysis or Global Climate Model dataset.
|
|
87
|
+
These grid points are required for computing terms related to circulation classification.
|
|
88
|
+
|
|
89
|
+
The function dynamically adjusts longitude values to account for the globe's circular nature
|
|
90
|
+
(longitude wrapping), ensuring that grid points are correctly identified near the 180°E/-180°W boundary.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
mslp (xr.DataArray): Mean sea level pressure data with latitude and longitude coordinates.
|
|
94
|
+
latitude (xr.DataArray): Latitude values of the dataset.
|
|
95
|
+
longitude (xr.DataArray): Longitude values of the dataset.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
tuple: A tuple of 16 `numpy.ndarray` objects, each corresponding to a grid point value of `mslp`
|
|
99
|
+
at the calculated latitude and longitude offsets.
|
|
100
|
+
|
|
101
|
+
Latitude and Longitude Offsets:
|
|
102
|
+
Grid Point 1: (+10, -5)
|
|
103
|
+
Grid Point 2: (+10, +5)
|
|
104
|
+
Grid Point 3: (+5, -15)
|
|
105
|
+
Grid Point 4: (+5, -5)
|
|
106
|
+
Grid Point 5: (+5, +5)
|
|
107
|
+
Grid Point 6: (+5, +15)
|
|
108
|
+
Grid Point 7: (+0, -15)
|
|
109
|
+
Grid Point 8: (+0, -5)
|
|
110
|
+
Grid Point 9: (+0, +5)
|
|
111
|
+
Grid Point 10: (+0, +15)
|
|
112
|
+
Grid Point 11: (-5, -15)
|
|
113
|
+
Grid Point 12: (-5, -5)
|
|
114
|
+
Grid Point 13: (-5, +5)
|
|
115
|
+
Grid Point 14: (-5, +15)
|
|
116
|
+
Grid Point 15: (-10, -5)
|
|
117
|
+
Grid Point 16: (-10, +5)
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
>>> latitude = xr.DataArray(np.linspace(-90, 90, 181), dims="latitude", name="latitude")
|
|
121
|
+
>>> longitude = xr.DataArray(np.linspace(-180, 180, 361), dims="longitude", name="longitude")
|
|
122
|
+
>>> mslp = xr.DataArray(np.random.rand(181, 361), coords={"latitude": latitude, "longitude": longitude}, dims=["latitude", "longitude"])
|
|
123
|
+
>>> gridpoints = extracting_gridpoints_globe(mslp, latitude, longitude)
|
|
124
|
+
>>> print(gridpoints[0]) # First grid point value
|
|
125
|
+
"""
|
|
126
|
+
offsets = [
|
|
127
|
+
(10, -5), (10, 5), (5, -15), (5, -5), (5, 5), (5, 15), (0, -15), (0, -5),
|
|
128
|
+
(0, 5), (0, 15), (-5, -15), (-5, -5), (-5, 5), (-5, 15), (-10, -5), (-10, 5)
|
|
129
|
+
]
|
|
130
|
+
gridpoints = []
|
|
131
|
+
|
|
132
|
+
for lat_offset, lon_offset in offsets:
|
|
133
|
+
lat_point = latitude + lat_offset
|
|
134
|
+
lon_point = xr.where(
|
|
135
|
+
longitude < -175, 360 + longitude + lon_offset,
|
|
136
|
+
xr.where(longitude > 175, longitude + lon_offset - 360, longitude + lon_offset)
|
|
137
|
+
)
|
|
138
|
+
lon_point = xr.where(lon_point == 180, -180, lon_point)
|
|
139
|
+
|
|
140
|
+
lat_point = latitude.sel(latitude=lat_point, method="nearest")
|
|
141
|
+
lon_point = longitude.sel(longitude=lon_point, method="nearest")
|
|
142
|
+
|
|
143
|
+
gridpoints.append(np.array(mslp.sel(latitude=lat_point, longitude=lon_point)))
|
|
144
|
+
|
|
145
|
+
return tuple(gridpoints)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import xarray as xr
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def read_mslp_file(mslp_data: xr.DataArray) -> xr.DataArray:
|
|
6
|
+
"""
|
|
7
|
+
Validates that the input is an xarray.DataArray containing Mean Sea Level Pressure (MSLP) data,
|
|
8
|
+
and ensures the coordinates are named 'time', 'latitude', and 'longitude'.
|
|
9
|
+
Args:
|
|
10
|
+
mslp_data (xr.DataArray): Input MSLP data as an xarray.DataArray
|
|
11
|
+
Returns:
|
|
12
|
+
xr.DataArray: Validated and standardized MSLP data with proper coordinate names
|
|
13
|
+
Raises:
|
|
14
|
+
TypeError: If the input is not an xarray.DataArray
|
|
15
|
+
ValueError: If the DataArray does not have valid dimensions
|
|
16
|
+
"""
|
|
17
|
+
if not isinstance(mslp_data, xr.DataArray):
|
|
18
|
+
raise TypeError("The input must be an xarray.DataArray containing MSLP data.")
|
|
19
|
+
|
|
20
|
+
# Coordinate mapping to enforce naming conventions
|
|
21
|
+
coord_mapping = {
|
|
22
|
+
"time": "time", # Ensure time is named 'time'
|
|
23
|
+
"valid_time": "time", # Rename valid_time -> time
|
|
24
|
+
"lat": "latitude", # Rename lat -> latitude
|
|
25
|
+
"latitude": "latitude", # Ensure latitude stays as latitude
|
|
26
|
+
"lon": "longitude", # Rename lon -> longitude
|
|
27
|
+
"longitude": "longitude", # Ensure longitude stays as longitude
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Rename coordinates to enforce naming conventions
|
|
31
|
+
for old_coord, new_coord in coord_mapping.items():
|
|
32
|
+
if old_coord in mslp_data.coords and new_coord != old_coord:
|
|
33
|
+
mslp_data = mslp_data.rename({old_coord: new_coord})
|
|
34
|
+
|
|
35
|
+
# Ensure required dimensions
|
|
36
|
+
required_dims = {"time", "latitude", "longitude"}
|
|
37
|
+
if not required_dims.issubset(set(coord_mapping.get(dim, dim) for dim in mslp_data.dims)):
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"The DataArray must have dimensions: {', '.join(required_dims)}. "
|
|
40
|
+
f"Found: {', '.join(mslp_data.dims)}."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return mslp_data
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def checking_lat_coords(mslp_data: xr.DataArray) -> xr.DataArray:
|
|
47
|
+
"""
|
|
48
|
+
Ensures the latitude coordinate values are in ascending order (e.g., -90 to 90º).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
mslp_data (xr.DataArray): Input MSLP data with standardized coordinates
|
|
52
|
+
Returns:
|
|
53
|
+
xr.DataArray: MSLP data with latitude coordinate in ascending order
|
|
54
|
+
"""
|
|
55
|
+
if mslp_data.latitude[0] > mslp_data.latitude[-1]:
|
|
56
|
+
mslp_data = mslp_data.reindex(latitude=list(reversed(mslp_data.latitude)))
|
|
57
|
+
else:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
return mslp_data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def checking_lon_coords(mslp_data: xr.DataArray) -> xr.DataArray:
|
|
64
|
+
"""
|
|
65
|
+
Ensures the longitude coordinate values are within the range [-180, 180].
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
mslp_data (xr.DataArray): Input MSLP data with standardized coordinates
|
|
69
|
+
Returns:
|
|
70
|
+
xr.DataArray: MSLP data with longitude coordinate adjusted to [-180, 180]
|
|
71
|
+
"""
|
|
72
|
+
if mslp_data.longitude[-1] > 180:
|
|
73
|
+
# Adjust longitude values
|
|
74
|
+
mslp_data['_longitude_adjusted'] = xr.where(
|
|
75
|
+
mslp_data.longitude > 180,
|
|
76
|
+
mslp_data.longitude - 360,
|
|
77
|
+
mslp_data.longitude
|
|
78
|
+
)
|
|
79
|
+
# Swap dimensions and sort
|
|
80
|
+
mslp_data = (
|
|
81
|
+
mslp_data
|
|
82
|
+
.swap_dims({"longitude": "_longitude_adjusted"})
|
|
83
|
+
.sel(_longitude_adjusted=sorted(mslp_data._longitude_adjusted))
|
|
84
|
+
.drop_vars("longitude")
|
|
85
|
+
.rename({"_longitude_adjusted": "longitude"})
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
return mslp_data
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_world(mslp_data: xr.DataArray) -> bool:
|
|
94
|
+
"""
|
|
95
|
+
Checks if the dataset covers the entire globe based on longitude and latitude coverage.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
mslp_data (xr.DataArray or xr.Dataset): Input data with 'longitude' and 'latitude' coordinates.
|
|
99
|
+
Returns:
|
|
100
|
+
bool: True if the dataset covers the entire globe, False otherwise.
|
|
101
|
+
"""
|
|
102
|
+
# Calculate the difference between the first two longitude and latitude values
|
|
103
|
+
dif_lon = np.abs(mslp_data.longitude[0] - mslp_data.longitude[1])
|
|
104
|
+
dif_lat = np.abs(mslp_data.latitude[0] - mslp_data.latitude[1])
|
|
105
|
+
|
|
106
|
+
# Define conditions for global coverage
|
|
107
|
+
condition_east = mslp_data.longitude[-1] >= (180 - dif_lon) # Covers up to 180°E
|
|
108
|
+
condition_west = mslp_data.longitude[0] <= (-180 + dif_lon) # Covers up to 180°W
|
|
109
|
+
|
|
110
|
+
is_global = condition_west and condition_east
|
|
111
|
+
|
|
112
|
+
return bool(is_global)
|
|
113
|
+
|
|
114
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import xarray as xr
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def enhance_and_validate_dataarray(lwt: xr.DataArray) -> xr.DataArray:
|
|
5
|
+
"""
|
|
6
|
+
Enhances metadata and validates coordinates for an atmospheric dataset.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
lwt (xr.DataArray): The circulation type DataArray.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
xr.DataArray: Enhanced DataArray with added metadata.
|
|
13
|
+
"""
|
|
14
|
+
# Set a variable name
|
|
15
|
+
lwt.name = "cts" # Circulation types (CTS)
|
|
16
|
+
|
|
17
|
+
# Add metadata
|
|
18
|
+
lwt.attrs["description"] = "Jenkinson and Collison / Lamb Weather Types (LWT) Classification"
|
|
19
|
+
lwt.attrs["units"] = "categorical"
|
|
20
|
+
lwt.attrs["long_name"] = "Lamb Weather Types"
|
|
21
|
+
|
|
22
|
+
# Add metadata to coordinates
|
|
23
|
+
if "latitude" in lwt.coords:
|
|
24
|
+
lwt["latitude"].attrs["units"] = "degrees_north"
|
|
25
|
+
lwt["latitude"].attrs["long_name"] = "Latitude"
|
|
26
|
+
if "longitude" in lwt.coords:
|
|
27
|
+
lwt["longitude"].attrs["units"] = "degrees_east"
|
|
28
|
+
lwt["longitude"].attrs["long_name"] = "Longitude"
|
|
29
|
+
if "time" in lwt.coords:
|
|
30
|
+
lwt["time"].attrs["long_name"] = "Time"
|
|
31
|
+
lwt["time"].attrs["calendar"] = "gregorian"
|
|
32
|
+
|
|
33
|
+
# Validate latitude range and order
|
|
34
|
+
if "latitude" in lwt.coords:
|
|
35
|
+
latitude = lwt["latitude"]
|
|
36
|
+
if not (-90 <= latitude.min() <= 90) or not (-90 <= latitude.max() <= 90):
|
|
37
|
+
raise ValueError("Latitude values must range between -90 and 90 degrees.")
|
|
38
|
+
if not (latitude.values[1:] >= latitude.values[:-1]).all():
|
|
39
|
+
raise ValueError("Latitude values must increase monotonically.")
|
|
40
|
+
|
|
41
|
+
# Validate longitude range and order
|
|
42
|
+
if "longitude" in lwt.coords:
|
|
43
|
+
longitude = lwt["longitude"]
|
|
44
|
+
if not (-180 <= longitude.min() <= 180) or not (-180 <= longitude.max() <= 180):
|
|
45
|
+
raise ValueError("Longitude values must range between -180 and 180 degrees.")
|
|
46
|
+
if not (longitude.values[1:] >= longitude.values[:-1]).all():
|
|
47
|
+
raise ValueError("Longitude values must increase monotonically.")
|
|
48
|
+
|
|
49
|
+
return lwt
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
import numpy as np
|
|
3
|
+
import xarray as xr
|
|
4
|
+
from .data_preparation import read_mslp_file, checking_lon_coords, \
|
|
5
|
+
checking_lat_coords, is_world
|
|
6
|
+
from .data_extraction import extract_lat_lon_points, extracting_gridpoints_area, extracting_gridpoints_globe
|
|
7
|
+
from .constants import compute_constants
|
|
8
|
+
from .computation import flows, compute_direction, assign_lwt
|
|
9
|
+
from .format_data import enhance_and_validate_dataarray
|
|
10
|
+
|
|
11
|
+
from jcclass.utils.logging_config import setup_logger
|
|
12
|
+
|
|
13
|
+
logger = setup_logger("jcclass")
|
|
14
|
+
|
|
15
|
+
def jc_classification(mslp_data: xr.DataArray) -> xr.DataArray:
|
|
16
|
+
logger.info("Starting the computation of the Jenkinson and Collison Circulation Types.")
|
|
17
|
+
# Step 1: Data preparation
|
|
18
|
+
logger.info("Preparing the MSLP data for computation.")
|
|
19
|
+
mslp_data = read_mslp_file(mslp_data)
|
|
20
|
+
mslp_data = checking_lat_coords(mslp_data)
|
|
21
|
+
mslp_data = checking_lon_coords(mslp_data)
|
|
22
|
+
time_data = mslp_data.time
|
|
23
|
+
is_global = is_world(mslp_data)
|
|
24
|
+
|
|
25
|
+
logger.info("Extracting grid points.")
|
|
26
|
+
# Step 2: Compute constants
|
|
27
|
+
latitude, longitude = extract_lat_lon_points(mslp_data)
|
|
28
|
+
sc, zwa, zwb, zsc = compute_constants(latitude, longitude)
|
|
29
|
+
|
|
30
|
+
if is_global:
|
|
31
|
+
gridpoints = extracting_gridpoints_globe(mslp_data, latitude, longitude)
|
|
32
|
+
else:
|
|
33
|
+
gridpoints = extracting_gridpoints_area(mslp_data, latitude, longitude)
|
|
34
|
+
|
|
35
|
+
logger.info("Computing equations of flows and vorticity.")
|
|
36
|
+
# Step 3: Compute equations of flows and vorticity
|
|
37
|
+
W, S, F, ZW, ZS, Z = flows(gridpoints, sc, zwa, zsc, zwb, latitude, longitude, time_data, mslp_data)
|
|
38
|
+
deg = np.mod(180 + np.rad2deg(np.arctan2(W, S)), 360)
|
|
39
|
+
|
|
40
|
+
logger.info("Computing flow directions.")
|
|
41
|
+
# Step 4: Compute flow directions
|
|
42
|
+
direction = compute_direction(deg, latitude)
|
|
43
|
+
|
|
44
|
+
logger.info("Assigning Lamb Weather Types.")
|
|
45
|
+
# Step 5: Assign Lamb Weather Types
|
|
46
|
+
lwt = assign_lwt(F, Z, direction)
|
|
47
|
+
|
|
48
|
+
logger.info("Validating and creating DataArray.")
|
|
49
|
+
# Step 6: Enhance and validate DataArray
|
|
50
|
+
lwt = enhance_and_validate_dataarray(lwt)
|
|
51
|
+
logger.info("Success!")
|
|
52
|
+
|
|
53
|
+
return lwt
|
jcclass/plotting/core.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import xarray as xr
|
|
2
|
+
import numpy as np
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import cartopy.crs as ccrs
|
|
5
|
+
|
|
6
|
+
from .functions.tools import ensure_2d, crop_area
|
|
7
|
+
from .functions.plot_utils import get_cmap_and_norm, add_legend, get_fig_size, \
|
|
8
|
+
configure_gridlines, format_time_string
|
|
9
|
+
from jcclass.compute.core import eleven_cts
|
|
10
|
+
from jcclass.utils.logging_config import setup_logger
|
|
11
|
+
logger = setup_logger("jcclass")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def plot_cts(ds: xr.DataArray,
|
|
15
|
+
lat_south: int = -80,
|
|
16
|
+
lat_north: int = 80,
|
|
17
|
+
lon_west: int = -180,
|
|
18
|
+
lon_east: int = 180,
|
|
19
|
+
show: bool = True):
|
|
20
|
+
"""
|
|
21
|
+
Plot the 27 circulation types on a map for a single time step.
|
|
22
|
+
|
|
23
|
+
This function visualizes one time slice of circulation types (typically 27 classes)
|
|
24
|
+
using a predefined colormap, with optional geographic cropping. It also masks out
|
|
25
|
+
the equatorial region and converts values to the 11 reduced CT categories.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
ds : xr.DataArray
|
|
30
|
+
A 2D `xarray.DataArray` with latitude and longitude dimensions.
|
|
31
|
+
Must represent a single time step of 27 circulation types.
|
|
32
|
+
|
|
33
|
+
lat_south : int, optional
|
|
34
|
+
Southern latitude boundary of the map (default: -80).
|
|
35
|
+
|
|
36
|
+
lat_north : int, optional
|
|
37
|
+
Northern latitude boundary of the map (default: 80).
|
|
38
|
+
|
|
39
|
+
lon_west : int, optional
|
|
40
|
+
Western longitude boundary of the map (default: -180).
|
|
41
|
+
|
|
42
|
+
lon_east : int, optional
|
|
43
|
+
Eastern longitude boundary of the map (default: 180).
|
|
44
|
+
|
|
45
|
+
show : bool, optional
|
|
46
|
+
Whether to display the plot immediately using `plt.show()`.
|
|
47
|
+
If False, the figure is returned silently (default: True).
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
fig : matplotlib.figure.Figure
|
|
52
|
+
The generated figure containing the circulation type map.
|
|
53
|
+
|
|
54
|
+
Notes
|
|
55
|
+
-----
|
|
56
|
+
- The equatorial band between -10 and 10 degrees latitude is masked out.
|
|
57
|
+
- A legend showing the 11 reduced CTs is included.
|
|
58
|
+
- The title includes the date (and hour, if available) from the time coordinate.
|
|
59
|
+
|
|
60
|
+
Examples
|
|
61
|
+
--------
|
|
62
|
+
>>> from jcclass.plotting import plot_cts
|
|
63
|
+
>>> cts_day = cts_27.sel(time="1979-01-01")
|
|
64
|
+
>>> fig = plot_cts(cts_day, lat_south=-60, lat_north=60, lon_west=-100, lon_east=20)
|
|
65
|
+
>>> fig.savefig("my_cts_map.png")
|
|
66
|
+
"""
|
|
67
|
+
logger.info("Plotting the circulation types to a map.")
|
|
68
|
+
# Checking the xr.DataArray is 2D
|
|
69
|
+
ensure_2d(ds)
|
|
70
|
+
# Cropping the area
|
|
71
|
+
ds = crop_area(ds, lat_north, lat_south, lon_west, lon_east)
|
|
72
|
+
# Redefining longitude and latitude limit points
|
|
73
|
+
lat_north = ds.latitude.max()
|
|
74
|
+
lat_south = ds.latitude.min()
|
|
75
|
+
lon_west = ds.longitude.min()
|
|
76
|
+
lon_east = ds.longitude.max()
|
|
77
|
+
|
|
78
|
+
# Mask the data to remove the equatorial region
|
|
79
|
+
ds = xr.where((ds.latitude < 10) & (ds.latitude > -10), np.nan, ds)
|
|
80
|
+
# Convert to 11 CTs
|
|
81
|
+
ds = eleven_cts(ds)
|
|
82
|
+
|
|
83
|
+
# Get the colormap and normalization
|
|
84
|
+
cmap, norm = get_cmap_and_norm()
|
|
85
|
+
# Compute the size of the figure
|
|
86
|
+
size_x, size_y = get_fig_size(ds)
|
|
87
|
+
# Get the x and y coordinate values
|
|
88
|
+
lons, lats = ds.longitude, ds.latitude
|
|
89
|
+
|
|
90
|
+
# Plotting
|
|
91
|
+
proj = ccrs.PlateCarree()
|
|
92
|
+
fig, ax = plt.subplots(figsize=(size_x, size_y), subplot_kw={'projection': proj})
|
|
93
|
+
ax.set_extent([lon_west, lon_east, lat_south, lat_north], crs=proj)
|
|
94
|
+
|
|
95
|
+
ax.pcolor(lons, lats, ds, transform=proj, norm=norm, cmap=cmap)
|
|
96
|
+
|
|
97
|
+
ax.coastlines('50m')
|
|
98
|
+
configure_gridlines(ax)
|
|
99
|
+
add_legend(fig, ax)
|
|
100
|
+
|
|
101
|
+
# Add date title if available
|
|
102
|
+
time_string = format_time_string(ds)
|
|
103
|
+
ax.set_title(time_string, size=12, loc='left')
|
|
104
|
+
|
|
105
|
+
plt.tight_layout()
|
|
106
|
+
if show:
|
|
107
|
+
plt.show()
|
|
108
|
+
return fig
|
|
File without changes
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import xarray as xr
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import cartopy.crs as ccrs
|
|
6
|
+
from matplotlib.colors import ListedColormap, BoundaryNorm
|
|
7
|
+
from matplotlib.lines import Line2D
|
|
8
|
+
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
|
|
9
|
+
import matplotlib.ticker as mticker
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_cmap_and_norm():
|
|
13
|
+
"""
|
|
14
|
+
Returns the colormap and normalization used to plot 11 circulation types.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
cmap (ListedColormap): Colormap for circulation types.
|
|
18
|
+
norm (BoundaryNorm): Boundary norm to map values to colors.
|
|
19
|
+
"""
|
|
20
|
+
cmap = ListedColormap([
|
|
21
|
+
"#7C7C77", "#17344F", "#0255F4", "#0F78ED", "#9E09EE", "#F6664C",
|
|
22
|
+
"#F24E64", "#D3C42D", "#2FC698", "#20E1D7", "#BD0000"
|
|
23
|
+
])
|
|
24
|
+
norm = BoundaryNorm(boundaries=np.arange(-1, 11, 1), ncolors=12)
|
|
25
|
+
|
|
26
|
+
return cmap, norm
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_fig_size(ds: xr.DataArray) -> tuple:
|
|
30
|
+
"""
|
|
31
|
+
Calculate the size of the figure based on the area of interest.
|
|
32
|
+
parameters:
|
|
33
|
+
ds (xarray.DataArray): DataArray containing latitude and longitude coordinates.
|
|
34
|
+
returns:
|
|
35
|
+
tuple: Size of the figure in inches (width, height).
|
|
36
|
+
"""
|
|
37
|
+
lat_north = ds.latitude.max()
|
|
38
|
+
lat_south = ds.latitude.min()
|
|
39
|
+
lon_west = ds.longitude.min()
|
|
40
|
+
lon_east = ds.longitude.max()
|
|
41
|
+
# Calculate the size of the figure based on the area of interest
|
|
42
|
+
dif_x = lon_east - lon_west
|
|
43
|
+
dif_y = lat_north - lat_south
|
|
44
|
+
size_x, size_y = 12, 10
|
|
45
|
+
if dif_x > dif_y:
|
|
46
|
+
size_y = size_x * (dif_y / dif_x)
|
|
47
|
+
elif dif_x < dif_y:
|
|
48
|
+
size_x = size_y * (dif_x / dif_y)
|
|
49
|
+
return size_x, size_y
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def configure_gridlines(ax: plt.Axes) -> None:
|
|
53
|
+
"""
|
|
54
|
+
Adds formatted geographic gridlines to a Cartopy axis.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
ax : matplotlib.axes._subplots.AxesSubplot
|
|
59
|
+
The axis (with Cartopy projection) to which the gridlines will be added.
|
|
60
|
+
|
|
61
|
+
Notes
|
|
62
|
+
-----
|
|
63
|
+
- Latitude labels are shown on the left.
|
|
64
|
+
- Longitude labels are shown at the bottom.
|
|
65
|
+
- Gridlines themselves are hidden (no xlines or ylines), only labels are displayed.
|
|
66
|
+
- Gridline locators and formatters are fixed to common global coordinates.
|
|
67
|
+
"""
|
|
68
|
+
gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True)
|
|
69
|
+
gl.top_labels = False
|
|
70
|
+
gl.bottom_labels = True
|
|
71
|
+
gl.left_labels = True
|
|
72
|
+
gl.right_labels = False
|
|
73
|
+
gl.xlines = False # disables grid lines
|
|
74
|
+
gl.ylines = False
|
|
75
|
+
gl.ylocator = mticker.FixedLocator([-80, -60, -40, -20, 0, 20, 40, 60, 80])
|
|
76
|
+
gl.xlocator = mticker.FixedLocator([-180, -120, -60, 0, 60, 120, 180])
|
|
77
|
+
gl.xformatter = LONGITUDE_FORMATTER
|
|
78
|
+
gl.yformatter = LATITUDE_FORMATTER
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def add_legend(fig: plt.Figure, ax: plt.Axes) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Adds a custom legend for 11 circulation types to the plot.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
fig : matplotlib.figure.Figure
|
|
88
|
+
The figure object where the legend will be attached.
|
|
89
|
+
|
|
90
|
+
ax : matplotlib.axes.Axes
|
|
91
|
+
The axis object to which the legend belongs.
|
|
92
|
+
|
|
93
|
+
Notes
|
|
94
|
+
-----
|
|
95
|
+
- The legend includes 11 circulation types labeled:
|
|
96
|
+
['LF', 'A', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N', 'C']
|
|
97
|
+
- Colors correspond to the colormap used in the circulation plot.
|
|
98
|
+
- The legend is placed outside the plot at the center right.
|
|
99
|
+
"""
|
|
100
|
+
legend_labels = ['LF', 'A', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW', 'N', 'C']
|
|
101
|
+
legend_colors = [
|
|
102
|
+
"#7c7c77", "#1c2c4b", "#123FDD", "#245cdc", "#802fcd", "#d98b4f",
|
|
103
|
+
"#D17860", "#d0c742", "#4d9f9e", "#46afd8", "#973000"
|
|
104
|
+
]
|
|
105
|
+
legend_styles = ['None'] * 11 # Only markers, no lines
|
|
106
|
+
|
|
107
|
+
legend_elements = [
|
|
108
|
+
Line2D([0], [0], marker='o', color=color, linestyle=style, markersize=16, label=label)
|
|
109
|
+
for color, style, label in zip(legend_colors, legend_styles, legend_labels)
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
ax.legend(
|
|
113
|
+
handles=legend_elements,
|
|
114
|
+
loc='center right',
|
|
115
|
+
bbox_to_anchor=(1.0, 0.5),
|
|
116
|
+
frameon=False,
|
|
117
|
+
prop={'size': 14},
|
|
118
|
+
bbox_transform=fig.transFigure
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def format_time_string(ds: xr.DataArray) -> str:
|
|
123
|
+
"""
|
|
124
|
+
Returns a formatted time string from the 'time' or 'valid_time' coordinate.
|
|
125
|
+
Includes hours if present (e.g., for hourly data).
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
ds : xr.DataArray
|
|
130
|
+
DataArray that contains a 'time' or 'valid_time' coordinate.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
str
|
|
135
|
+
A formatted time string, e.g., '2025-04-06' or '2025-04-06 18:00'.
|
|
136
|
+
|
|
137
|
+
Raises
|
|
138
|
+
------
|
|
139
|
+
ValueError
|
|
140
|
+
If neither 'time' nor 'valid_time' coordinate is found.
|
|
141
|
+
"""
|
|
142
|
+
# Try to find time coordinate
|
|
143
|
+
for time_coord in ['time', 'valid_time']:
|
|
144
|
+
if time_coord in ds.coords:
|
|
145
|
+
time_val = ds[time_coord].values
|
|
146
|
+
break
|
|
147
|
+
else:
|
|
148
|
+
raise ValueError("No 'time' or 'valid_time' coordinate found.")
|
|
149
|
+
|
|
150
|
+
# Convert to pandas datetime (supporting scalar or array)
|
|
151
|
+
time_val = pd.to_datetime(time_val)
|
|
152
|
+
|
|
153
|
+
if isinstance(time_val, (np.ndarray, pd.DatetimeIndex)):
|
|
154
|
+
time_val = time_val[0]
|
|
155
|
+
|
|
156
|
+
# Include hour if not 00:00
|
|
157
|
+
if time_val.hour != 0 or time_val.minute != 0:
|
|
158
|
+
return time_val.strftime('%Y-%m-%d %H:%M')
|
|
159
|
+
else:
|
|
160
|
+
return time_val.strftime('%Y-%m-%d')
|
|
161
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import xarray as xr
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
from matplotlib.colors import ListedColormap
|
|
4
|
+
|
|
5
|
+
import cartopy.crs as ccrs
|
|
6
|
+
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
|
|
7
|
+
import matplotlib.ticker as mticker
|
|
8
|
+
from jcclass.utils.logging_config import setup_logger
|
|
9
|
+
logger = setup_logger("jcclass")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ensure_2d(data: xr.DataArray) -> xr.DataArray:
|
|
13
|
+
"""
|
|
14
|
+
Ensure the DataArray is 2D (after squeezing singleton dims).
|
|
15
|
+
If not, raise an error and stop plotting.
|
|
16
|
+
|
|
17
|
+
Parameters:
|
|
18
|
+
data (xr.DataArray): The input DataArray.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
xr.DataArray: A squeezed 2D DataArray ready for plotting.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
ValueError: If the resulting DataArray is not 2D.
|
|
25
|
+
"""
|
|
26
|
+
squeezed = data.squeeze()
|
|
27
|
+
|
|
28
|
+
if squeezed.ndim != 2:
|
|
29
|
+
msg = (
|
|
30
|
+
f"The DataArray has {squeezed.ndim} dimensions after squeezing "
|
|
31
|
+
f"({list(squeezed.dims)}). Only 2D DataArrays can be plotted."
|
|
32
|
+
)
|
|
33
|
+
logger.error(msg)
|
|
34
|
+
raise ValueError(msg)
|
|
35
|
+
|
|
36
|
+
return squeezed
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def crop_area(data: xr.DataArray,
|
|
40
|
+
lat_north: float,
|
|
41
|
+
lat_south: float,
|
|
42
|
+
lon_west: float,
|
|
43
|
+
lon_east: float) -> xr.DataArray:
|
|
44
|
+
"""
|
|
45
|
+
Crop the DataArray to the specified area.
|
|
46
|
+
Parameters:
|
|
47
|
+
data (xr.DataArray): The DataArray to crop.
|
|
48
|
+
lat_north (str): The northern latitude boundary.
|
|
49
|
+
lat_south (str): The southern latitude boundary.
|
|
50
|
+
lon_west (str): The western longitude boundary.
|
|
51
|
+
lon_east (str): The eastern longitude boundary.
|
|
52
|
+
"""
|
|
53
|
+
lat_north = data.latitude.sel(latitude=lat_north, method='nearest')
|
|
54
|
+
lat_south = data.latitude.sel(latitude=lat_south, method='nearest')
|
|
55
|
+
lon_west = data.longitude.sel(longitude=lon_west, method='nearest')
|
|
56
|
+
lon_east = data.longitude.sel(longitude=lon_east, method='nearest')
|
|
57
|
+
data = data.sel(latitude=slice(lat_south, lat_north), longitude=slice(lon_west, lon_east))
|
|
58
|
+
return data
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def calculate_size(dif_x: float, dif_y: float) -> tuple:
|
|
62
|
+
size_x, size_y = 12, 10
|
|
63
|
+
if dif_x > dif_y:
|
|
64
|
+
size_y = size_x * (dif_y / dif_x)
|
|
65
|
+
elif dif_x < dif_y:
|
|
66
|
+
size_x = size_y * (dif_x / dif_y)
|
|
67
|
+
return size_x, size_y
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def define_colormap() -> ListedColormap:
|
|
71
|
+
return ListedColormap([
|
|
72
|
+
"#7C7C77", "#17344F", "#0255F4", "#0F78ED", "#9E09EE", "#F6664C",
|
|
73
|
+
"#F24E64", "#D3C42D", "#2FC698", "#20E1D7", "#BD0000"
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def configure_gridlines(ax: plt.Axes) -> None:
|
|
78
|
+
gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=True)
|
|
79
|
+
gl.top_labels = False
|
|
80
|
+
gl.bottom_labels = True
|
|
81
|
+
gl.left_labels = True
|
|
82
|
+
gl.right_labels = False
|
|
83
|
+
gl.xlines = False
|
|
84
|
+
gl.ylines = False
|
|
85
|
+
gl.ylocator = mticker.FixedLocator([-80, -60, -40, -20, 0, 20, 40, 60, 80])
|
|
86
|
+
gl.xlocator = mticker.FixedLocator([-180, -120, -60, 0, 60, 120, 180])
|
|
87
|
+
gl.xformatter = LONGITUDE_FORMATTER
|
|
88
|
+
gl.yformatter = LATITUDE_FORMATTER
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def setup_logger(name: str, level=logging.INFO) -> logging.Logger:
|
|
5
|
+
"""
|
|
6
|
+
Sets up a logger with a given name and level.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
name (str): Name of the logger.
|
|
10
|
+
level (int): Logging level (default: logging.INFO).
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
logging.Logger: Configured logger instance.
|
|
14
|
+
"""
|
|
15
|
+
logger = logging.getLogger(name)
|
|
16
|
+
if not logger.hasHandlers(): # Avoid adding duplicate handlers
|
|
17
|
+
logger.setLevel(level)
|
|
18
|
+
handler = logging.StreamHandler() # Output to console
|
|
19
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
20
|
+
handler.setFormatter(formatter)
|
|
21
|
+
logger.addHandler(handler)
|
|
22
|
+
return logger
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jcclass
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Jenkinson and Collison automated gridded classification
|
|
5
|
+
Home-page: https://github.com/PedroLormendez/jcclass
|
|
6
|
+
Author: Pedro Herrera-Lormendez
|
|
7
|
+
Author-email: peth31@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: circulations,CTs,WTs,synoptic
|
|
10
|
+
Requires-Python: >=3.7
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: numpy>=1.19.5
|
|
13
|
+
Requires-Dist: xarray>=0.16.2
|
|
14
|
+
Requires-Dist: matplotlib>=3.2.0
|
|
15
|
+
Requires-Dist: pyproj
|
|
16
|
+
Requires-Dist: cartopy>=0.17.0
|
|
17
|
+
Requires-Dist: cftime
|
|
18
|
+
Requires-Dist: netCDF4
|
|
19
|
+
Requires-Dist: pytest
|
|
20
|
+
Dynamic: author
|
|
21
|
+
Dynamic: author-email
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: keywords
|
|
26
|
+
Dynamic: license
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
# Jenkinson - Collison automated gridded classification for Python
|
|
32
|
+
|
|
33
|
+
[](https://pypi.python.org/pypi/jcclass/)
|
|
34
|
+
[](https://zenodo.org/badge/latestdoi/524934105)
|
|
35
|
+
[](https://pypi.org/project/jcclass/)
|
|
36
|
+
[](https://pypi.python.org/pypi/jcclass/)
|
|
37
|
+
[](https://twitter.com/PedroLormendez)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
This is an adapted version for python of the __Jenkinson - Collison__ automated classfication based on the original Lamb Weather Types. This gridded version is based on the application made by [Otero](https://link.springer.com/article/10.1007/s00382-017-3705-y) (2018) using a moving central gridded point with that allows to compute the synoptic circulation types on a gridded Mean Sea Level Pressure (MSLP) domain.
|
|
41
|
+

|
|
42
|
+
## How does it work?
|
|
43
|
+
The method uses grid-point MSLP data to obtain numerical values of wind flow and vorticity which can be used to determine Cyclonic and Anticyclonic patterns as well as their dominant advective (direction of wind flow) characteristics. The 16 gridded points are moved along the region in reference to a central point where the dominant circulation type will be designated.
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
## The Circulation Types (CTs)
|
|
47
|
+
The application of the automated classification allows to derive 27 synoptic circulations. 26 of them based on the dominant pressure pattern and wind direction plus a Low Flow (LF) type which is characterised by days when pressure gradients are to weak and a dominant circulation or advective direction can not be assigned.
|
|
48
|
+
|
|
49
|
+
|__Name__ | __Abreviation__| __Coding__|__Name__| __Abreviation__| __Coding__|__Name__| __Abreviation__| __Coding__|
|
|
50
|
+
| :- | :-: | :-: | :- | :-: | :-: | :- | :-: | :-:
|
|
51
|
+
|Low Flow | LF | -1
|
|
52
|
+
|Anticyclonic | A | 0 | | | |Cyclonic | C | 20
|
|
53
|
+
|Anticyclonic Northeasterly | ANE | 1 |Northeasterly| NE| 11|Cyclonic Northeasterly| CNE | 21
|
|
54
|
+
|Anticyclonic Easterly | AE | 2 |Easterly | E | 12|Cyclonic Easterly | CE | 22
|
|
55
|
+
|Anticyclonic Southeasterly | ASE | 3 |Southeasterly| SE| 13|Cyclonic Southeasterly| CSE | 23
|
|
56
|
+
|Anticyclonic Southerly | AS | 4 |Southerly | S | 14|Cyclonic Southerly | CS | 24
|
|
57
|
+
|Anticyclonic Southwesterly | ASW | 5 |Southwesterly| SW| 15|Cyclonic Southwesterly| CSW | 25
|
|
58
|
+
|Anticyclonic Westerly | AW | 6 |Westerly | W | 16|Cyclonic Westerly | CW | 26
|
|
59
|
+
|Anticyclonic Northwesterly | ANW | 7 |Northwesterly| NW| 17|Cyclonic Northwesterly| CNW | 27
|
|
60
|
+
|Anticyclonic Northerly | AN | 8 |Northerly | N | 18|Cyclonic Northerly | CN | 28
|
|
61
|
+
|
|
62
|
+
The original 27 circulations can be reduced to a set of 11 patterns based on their dominant advection.
|
|
63
|
+
|
|
64
|
+
|Name | Abreviation | Coding
|
|
65
|
+
| :- | :-: | :-:
|
|
66
|
+
|Low Flow | LF | -1
|
|
67
|
+
|Anticyclonic | A | 0
|
|
68
|
+
|Northeasterly | NE | 1
|
|
69
|
+
|Easterly | E | 2
|
|
70
|
+
|Southeasterly | SE | 3
|
|
71
|
+
|Southerly | S | 4
|
|
72
|
+
|Southwesterly | SW | 5
|
|
73
|
+
|Westerly | W | 6
|
|
74
|
+
|Northwesterly | NW | 7
|
|
75
|
+
|Northerly | N | 8
|
|
76
|
+
|Cyclonic | C | 9
|
|
77
|
+
|
|
78
|
+
## Working datasets
|
|
79
|
+
|
|
80
|
+
The current code has been has been tested for the following datasets:
|
|
81
|
+
- [ERA5](https://www.ecmwf.int/en/forecasts/datasets/reanalysis-datasets/era5) Reanalysis
|
|
82
|
+
-[NOAA](https://psl.noaa.gov/data/gridded/data.20thC_ReanV3.html) 20th Century Reanalysis (V3)
|
|
83
|
+
- Global Climate Models from the Coupled Model Intercomparison Project ([CMIP6](https://esgf-node.llnl.gov/projects/cmip6/))
|
|
84
|
+
|
|
85
|
+
The method can be applied for any other netcdf files with latitude coordinates names as "latitude" or "lat", or longitudes coordinates as "longitude" or "lon" and MSLP coordinate names as "msl" or "psl".
|
|
86
|
+
|
|
87
|
+
Sample datasets from ERA5 is provided and available [here](https://github.com/PedroLormendez/jc_module/tree/main/sample_data)
|
|
88
|
+
## Installation
|
|
89
|
+
Simply run in the terminal
|
|
90
|
+
```
|
|
91
|
+
pip install jcclass
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## How to use?
|
|
95
|
+
__Importing the module__
|
|
96
|
+
```python
|
|
97
|
+
from jcclass.compute import compute_cts, eleven_cts
|
|
98
|
+
from jcclass.plotting import plot_cts
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
__Computing the automated circulation types based on gridded MSLP__
|
|
102
|
+
|
|
103
|
+
Sample datasets available [here](https://github.com/PedroLormendez/jc_module/tree/main/sample_data).
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import xarray as xr
|
|
107
|
+
filename = 'era5_daily_lowres.nc'
|
|
108
|
+
ds_mslp = xr.open_dataset("sample_data/era5_daily_lowres.nc").msl
|
|
109
|
+
cts_27 = compute_cts(ds_mslp)
|
|
110
|
+
```
|
|
111
|
+
__Computing the reduced eleven circulation types__
|
|
112
|
+
```python
|
|
113
|
+
cts_11 = eleven_cts(cts_27)
|
|
114
|
+
```
|
|
115
|
+
__Ploting the circulation types on a map__
|
|
116
|
+
```python
|
|
117
|
+
# Select a single day
|
|
118
|
+
date = "1979-01-03"
|
|
119
|
+
cts_2d = cts_27.sel(time = date) # selecting one time
|
|
120
|
+
fig = plot_cts(cts_2d, *args)
|
|
121
|
+
```
|
|
122
|
+
- *cts : a 2D xarray.DataArray of the 27 CTs*
|
|
123
|
+
- **args :
|
|
124
|
+
|
|
125
|
+
- float, __optional__ (lat_south, lat_north, lon_west, lon_east)*
|
|
126
|
+
- bool, __optional__ (show = True)* False to not show the figure
|
|
127
|
+

|
|
128
|
+
|
|
129
|
+
__Saving the figures__
|
|
130
|
+
|
|
131
|
+
You can save anytime any of the figures using ``fig.savefig``.
|
|
132
|
+
|
|
133
|
+
```py
|
|
134
|
+
fig.savefig('figname.png', dpi = 150)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Acknowledging this work
|
|
138
|
+
The code can be used and modified freely without any restriction. If you use it for your own research, I would appreciate if you cite this work as follows:
|
|
139
|
+
|
|
140
|
+
Herrera-Lormendez P., 2022: PedroLormendez/jcclass: version x.y.z [doi:10.5281/zenodo.7025220](https://zenodo.org/record/7025220#.YwjIKexByWg)
|
|
141
|
+
|
|
142
|
+
Reports on errors are welcomed by [raising an issue](https://github.com/PedroLormendez/JC-Classification/issues)
|
|
143
|
+
|
|
144
|
+
## Further literature on the method
|
|
145
|
+
- Jenkinson AF, Collison FP. 1977. An Initial Climatology of Gales over the North Sea. Synoptic Climatology Branch Memorandum, No. 62., Meteorological Office, Bracknell.
|
|
146
|
+
- Lamb HH. 1972. British Isles weather types and a register of daily sequence of circulation patterns, 1861-1971: Geophysical Memoir. HMSO.
|
|
147
|
+
- Jones PD, Hulme M, Briffa KR. 1993. A comparison of Lamb circulation types with an objective classification scheme. International Journal of Climatology. John Wiley & Sons, Ltd, 13(6): 655–663. https://doi.org/10.1002/joc.3370130606.
|
|
148
|
+
- Otero N, Sillmann J, Butler T. 2018. Assessment of an extended version of the Jenkinson–Collison classification on CMIP5 models over Europe. Climate Dynamics. Springer Verlag, 50(5–6): 1559–1579. https://doi.org/10.1007/s00382-017-3705-y.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
jcclass/__init__.py,sha256=00F4MrlU6_vBR02AdrYpqHcWGzrRe2zm5jzO4S5WMYk,129
|
|
2
|
+
jcclass/compute/__init__.py,sha256=xXvi0FMN23bILEynpwQW2jSSHfwSA5FKG9FnipHPlkw,83
|
|
3
|
+
jcclass/compute/core.py,sha256=lU7_sL8uOXX_ysQQOyCJjz_4Y1T9P1kSPf6UISSJ92c,2457
|
|
4
|
+
jcclass/compute/functions/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
5
|
+
jcclass/compute/functions/computation.py,sha256=bWJyp5xKwMPfNvxGaN_tETQAL1K4isjPJBX8uhJmFBs,8649
|
|
6
|
+
jcclass/compute/functions/constants.py,sha256=-QKkxvDt5ImiIr6wbhp8-ljePkfPEd71XrBg69M6mGM,2374
|
|
7
|
+
jcclass/compute/functions/data_extraction.py,sha256=6zhaoIQn01KpeGEgGa5j8QTZsrR0sL5msnJuN_6ugPQ,5957
|
|
8
|
+
jcclass/compute/functions/data_preparation.py,sha256=KziZpEhJ8HGyFntc9RzPWF0-TkdPfvbWSoo3my6Vk1o,4217
|
|
9
|
+
jcclass/compute/functions/format_data.py,sha256=dim7AqPyafxEtQSigQd82AcDp_ptf-0V2vrYs-6uL5s,1952
|
|
10
|
+
jcclass/compute/functions/main.py,sha256=EL4MlENtOZV9AMChTgftFDErLB8vymD907MyDezIiqg,2084
|
|
11
|
+
jcclass/plotting/__init__.py,sha256=tTH6ZQCE2ypbcfzT5-e4aS-N5NJBZLbID3iNHeGOqMg,51
|
|
12
|
+
jcclass/plotting/core.py,sha256=AornbUVDY5prI_8KAA_YL-LP3F59Qh2ZTkKmQzK3J1M,3635
|
|
13
|
+
jcclass/plotting/functions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
jcclass/plotting/functions/plot_utils.py,sha256=XOCwN9-ikAlS2K-rlzb4j5FusdRztf0LrssR9pPMgOg,5183
|
|
15
|
+
jcclass/plotting/functions/tools.py,sha256=Ix7rDbi-D_cBzugKwrQYynsUvIqqbAwfHKmMzsBWqhg,2923
|
|
16
|
+
jcclass/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
jcclass/utils/logging_config.py,sha256=QH-a4bZhX_-G9JawGAAfeBSP91Pt7Z8iOHYNTM11FTk,713
|
|
18
|
+
jcclass-0.0.1.dist-info/METADATA,sha256=NBFWU8bC-fICltVG_3mAEN2CvcC0niT0K_JhodHxMtg,7996
|
|
19
|
+
jcclass-0.0.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
20
|
+
jcclass-0.0.1.dist-info/top_level.txt,sha256=-9-rcwn8C0pQ5oeHIYv_V5p9yYhCBkTWgGU7FLZ1HOk,8
|
|
21
|
+
jcclass-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jcclass
|