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 ADDED
@@ -0,0 +1,4 @@
1
+ from .compute import compute_cts, eleven_cts
2
+ from .plotting import plot_cts
3
+
4
+ __all__ = ["compute_cts", "eleven_cts", "plot_cts"]
@@ -0,0 +1,3 @@
1
+ from .core import compute_cts, eleven_cts
2
+
3
+ __all__ = ["compute_cts", "eleven_cts"]
@@ -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
@@ -0,0 +1,3 @@
1
+ from .core import plot_cts
2
+
3
+ __all__ = ["plot_cts"]
@@ -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
+ [![PyPI version fury.io](https://badge.fury.io/py/jcclass.svg)](https://pypi.python.org/pypi/jcclass/)
34
+ [![DOI](https://zenodo.org/badge/524934105.svg)](https://zenodo.org/badge/latestdoi/524934105)
35
+ [![downloads](https://img.shields.io/pypi/dm/jcclass.svg)](https://pypi.org/project/jcclass/)
36
+ [![PyPI license](https://img.shields.io/pypi/l/jcclass.svg)](https://pypi.python.org/pypi/jcclass/)
37
+ [![Twitter](https://badgen.net/badge/icon/twitter?icon=twitter&label)](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
+ ![](https://github.com/PedroLormendez/jc_module/blob/main/figs/Circulations_quick.gif)
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
+ ![](https://github.com/PedroLormendez/jc_module/blob/main/figs/Gridpoints.gif)
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
+ ![](https://github.com/PedroLormendez/jc_module/blob/main/figs/plot_cts.png)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (78.1.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ jcclass