Coverage for src/CSET/operators/curvature.py: 11%
45 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-18 10:49 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-18 10:49 +0000
1# © Crown copyright, Met Office (2022-2026) and CSET contributors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
15"""A module containing different implementations of CURV.
17The diagnostics here are separate implementations of CURV for both
18global models and limited-area models.
19"""
21import logging
23import iris
24import numpy as np
26from CSET._common import iter_maybe
29def _lat_lon_identifier(original_lat, original_lon, distance, bearing):
30 """Find the latitude and longitude of new points based on great circle distance."""
31 R = 6371.0 # km.
32 lat_rad = np.radians(original_lat)
33 lon_rad = np.radians(original_lon)
34 bearing_rad = np.radians(bearing)
36 new_lat = np.arcsin(
37 np.sin(lat_rad) * np.cos(distance / R)
38 + np.cos(lat_rad) * np.sin(distance / R) * np.cos(bearing_rad)
39 )
40 new_lon = lon_rad + np.arctan2(
41 np.sin(bearing_rad) * np.sin(distance / R) * np.cos(lat_rad),
42 np.cos(distance / R) - np.sin(lat_rad) * np.sin(new_lat),
43 )
45 new_lat = np.degrees(new_lat)
46 new_lon = np.degrees(new_lon)
47 new_lon = np.where(new_lon > 180.0, new_lon - 360.0, new_lon)
49 return new_lat, new_lon
52def global_curv(central, radius, num_radial_points=16, tol=400.0):
53 """Calculate the CURV diagnostic for a global model VERSION 2 for faster calculation speed."""
54 if (num_radial_points == 8) or (num_radial_points == 16):
55 bearing = np.linspace(0.0, 360.0, num_radial_points + 1)[:-1]
56 else:
57 raise ValueError(
58 f"Number of radial points should be 8 or 16. You have provided {num_radial_points}."
59 )
61 logging.info(
62 f"CURV calculated base on {num_radial_points} radial points, a {radius} km radius and {tol} tolerance."
63 )
64 curv_cubes = iris.cube.CubeList([])
65 for cube in iter_maybe(central):
66 curv = cube.copy()
67 surroundings = []
68 lats, lons = np.meshgrid(
69 cube.coord("latitude").points, cube.coord("longitude").points
70 )
71 for b in bearing:
72 target_data = cube.copy()
73 new_lat, new_lon = _lat_lon_identifier(
74 lats,
75 lons,
76 radius,
77 b,
78 )
79 new_lat_points = new_lat[0, :]
80 new_lon_points = new_lon[:, 0]
81 surround = target_data.interpolate(
82 [("latitude", new_lat_points), ("longitude", new_lon_points)],
83 iris.analysis.Linear(),
84 )
85 surroundings.append(surround.data)
86 surroundings = np.array(surroundings)
87 curv_data = surroundings - cube.data
88 curv_data_binary = np.zeros(curv_data.shape)
89 curv_data_binary[curv_data > tol] = -1.0
90 curv_data_binary[curv_data < -tol] = 1.0
91 curv_data_collapsed = np.sum(curv_data_binary, 0)
92 curv.data = curv_data_collapsed
93 curv.rename(f"CURV_calculated_from_{num_radial_points}_radial_points")
94 curv.units = "1"
95 curv_cubes.append(curv)
96 if len(curv_cubes) == 1:
97 return curv_cubes[0]
98 else:
99 return curv_cubes