Coverage for src / CSET / operators / curvature.py: 11%
38 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 15:22 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 15:22 +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
27def _lat_lon_identifier(original_lat, original_lon, distance, bearing):
28 """Find the latitude and longitude of new points based on great circle distance."""
29 R = 6371.0 # km.
30 lat_rad = np.radians(original_lat)
31 lon_rad = np.radians(original_lon)
32 bearing_rad = np.radians(bearing)
34 new_lat = np.arcsin(
35 np.sin(lat_rad) * np.cos(distance / R)
36 + np.cos(lat_rad) * np.sin(distance / R) * np.cos(bearing_rad)
37 )
38 new_lon = lon_rad + np.arctan2(
39 np.sin(bearing_rad) * np.sin(distance / R) * np.cos(lat_rad),
40 np.cos(distance / R) - np.sin(lat_rad) * np.sin(new_lat),
41 )
43 new_lat = np.degrees(new_lat)
44 new_lon = np.degrees(new_lon)
45 new_lon = np.where(new_lon > 360.0, new_lon - 360.0, new_lon)
47 return new_lat, new_lon
50def global_curv(central, radius, num_radial_points=16, tol=0):
51 """Calculate the CURV diagnostic for a global model."""
52 if (num_radial_points == 8) or (num_radial_points == 16):
53 bearing = np.linspace(0.0, 360.0, num_radial_points + 1)[:-1]
54 else:
55 raise ValueError(
56 f"Number of radial points should be 8 or 16. You have provided {num_radial_points}."
57 )
59 logging.info(
60 f"CURV calculated base on {num_radial_points} radial points, a {radius} km radius and {tol} tolerance."
61 )
63 surroundings = iris.cube.CubeList([])
64 for b in bearing:
65 surround = central.copy()
66 # Need to try and avoid this loop for whole domain if possible, also need to raise time and realization bits as well.
67 for lat_number, lat in enumerate(central.slices_over("latitude")):
68 for lon_number, lon in enumerate(lat.slices_over("longitude")):
69 new_lat, new_lon = _lat_lon_identifier(
70 lon.coord("latitude").points,
71 lon.coord("longitude").points,
72 radius,
73 b,
74 )
75 # Need to get interpolation better.
76 surround.data[lat_number, lon_number] = central.data[
77 np.abs(central.coord("latitude").points - new_lat[0]).argmin(),
78 np.abs(central.coord("longitude").points - new_lon[0]).argmin(),
79 ]
80 surround.add_aux_coord(
81 iris.coords.DimCoord(b, long_name="bearing", units="degrees")
82 )
83 surroundings.append(surround)
84 surroundings.merge()
85 print(surroundings)
87 curv = surroundings[0].copy()
88 curv -= central
90 curv.data[curv.data > tol] = -1.0
91 curv.data[curv.data < tol] = 1.0
93 curv.collapsed("bearing", iris.analysis.SUM)
94 curv.rename(f"CURV_calculated_from_{num_radial_points}_radial_points")
95 curv.units = "1"
96 return curv