Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions src/CSET/cset_workflow/meta/diagnostics/rose-meta.conf
Original file line number Diff line number Diff line change
Expand Up @@ -1081,15 +1081,13 @@ ns=Diagnostics/Derived/Cyclones
sort-key=sec-c8
title=Cyclones


###################################
# Extra-tropical cyclones
[Diagnostics/ETCs]
ns=Diagnostics/Derived/Cyclones/ETCs
sort-key=sec-c8a
title=Extra-tropical cyclones


###################################
# Tropical Cyclones
[Diagnostics/TCs]
Expand Down Expand Up @@ -1190,3 +1188,35 @@ help= Include condition and threshold required, it will loop over all conditions
type=python_boolean
compulsory=true
sort-key=tpb4

#######################################################################
# Observations
[Diagnostics/Observations]
ns=Diagnostics/Observations
sort-key=sec-d11
title=Observations

###################################
# Cardington Observations
[Diagnostics/Cardington]
ns=Diagnostics/Observations/Cardington
sort-key=sec-d12
title=Cardington Observations

# Air temperature timeseries
[template variables=CARDINGTON_AIR_TEMPERATURE_SINGLE_POINT_TIME_SERIES]
ns=Diagnostics/Observations/Cardington
title=Cardington air temperature timeseries
description=Create Cardington diagnostic variable timeseries
type=python_boolean
compulsory=true
sort-key=0card1

# Relative humidity timeseries
[template variables=CARDINGTON_RELATIVE_HUMIDITY_SINGLE_POINT_TIME_SERIES]
ns=Diagnostics/Observations/Cardington
title=Cardington relative humidity timeseries
description=Create Cardington diagnostic variable timeseries
type=python_boolean
compulsory=true
sort-key=0card2
2 changes: 2 additions & 0 deletions src/CSET/cset_workflow/rose-suite.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ ANALYSIS_LENGTH=""
AOA_DIAG=False
!!AOA_PLEV=[]
BASIC_QQ_PLOT=False
CARDINGTON_AIR_TEMPERATURE_SINGLE_POINT_TIME_SERIES=True
CARDINGTON_RELATIVE_HUMIDITY_SINGLE_POINT_TIME_SERIES=True
COLORBAR_FILE=""
!!CONDA_METPLUS_VENV_LOCATION=""
CONDA_PATH=""
Expand Down
26 changes: 26 additions & 0 deletions src/CSET/loaders/timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,32 @@ def load(conf: Config):
aggregation=False,
)

# Cardington air temperature time series.
if conf.CARDINGTON_AIR_TEMPERATURE_SINGLE_POINT_TIME_SERIES:
base_model = models[0]
for model in models[1:]:
yield RawRecipe(
recipe="cardington_air_temperature_single_point_time_series.yaml",
variables={
"MODEL_NAME": model["name"],
},
model_ids=[base_model["id"], model["id"]],
aggregation=False,
)

# Cardington relative humidity time series.
if conf.CARDINGTON_RELATIVE_HUMIDITY_SINGLE_POINT_TIME_SERIES:
base_model = models[0]
for model in models[1:]:
yield RawRecipe(
recipe="cardington_relative_humidity_single_point_time_series.yaml",
variables={
"MODEL_NAME": model["name"],
},
model_ids=[base_model["id"], model["id"]],
aggregation=False,
)

# Create a list of case aggregation types.
AGGREGATION_TYPES = ["lead_time", "hour_of_day", "validity_time", "all"]

Expand Down
157 changes: 85 additions & 72 deletions src/CSET/operators/regrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import warnings

import iris
import iris.coord_systems
import iris.cube
import numpy as np

Expand Down Expand Up @@ -199,7 +200,7 @@ def regrid_onto_xyspacing(


def regrid_to_single_point(
cube: iris.cube.Cube,
cubes: iris.cube.Cube | iris.cube.CubeList,
lat_pt: float,
lon_pt: float,
latlon_in_type: str = "rotated",
Expand All @@ -215,9 +216,9 @@ def regrid_to_single_point(

Parameters
----------
cube: Cube
An iris cube of the data to regrid. As a minimum, it needs to be 2D with
latitude, longitude coordinates.
cubes: Cube | CubeList
An iris cube or CubeList of the data to regrid. As a minimum, it needs
to be 2D with latitude, longitude coordinates.
lon_pt: float
Selected value of longitude: this should be in the range -180 degrees to
180 degrees.
Expand All @@ -236,9 +237,9 @@ def regrid_to_single_point(

Returns
-------
cube_rgd: Cube
An iris cube of the data at the specified point (this may have time
and/or height dimensions).
regridded_cubes: Cube | CubeList
An iris cube or CubeList of the data at the specified point (this may
have time and/or height dimensions).

Raises
------
Expand All @@ -259,75 +260,87 @@ def regrid_to_single_point(
is potentially unreliable.

"""
# Get x and y coordinate names.
y_coord, x_coord = get_cube_yxcoordname(cube)

# List of supported grids - check if it is compatible
# NOTE: The "RotatedGeogCS" option below seems to be required for rotated grids --
# this may need to be added in other places in these Operators.
supported_grids = (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS)
if not isinstance(cube.coord(x_coord).coord_system, supported_grids):
raise NotImplementedError(
f"Does not currently support {cube.coord(x_coord).coord_system} regrid method"
# To store regridded cubes.
regridded_cubes = iris.cube.CubeList()

# Iterate over all cubes and regrid.
for cube in iter_maybe(cubes):
# Get x and y coordinate names.
y_coord, x_coord = get_cube_yxcoordname(cube)

# List of supported grids - check if it is compatible
# NOTE: The "RotatedGeogCS" option below seems to be required for rotated grids --
# this may need to be added in other places in these Operators.
supported_grids = (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS)
if not isinstance(cube.coord(x_coord).coord_system, supported_grids):
raise NotImplementedError(
f"Does not currently support {cube.coord(x_coord).coord_system} regrid method"
)
if not isinstance(cube.coord(y_coord).coord_system, supported_grids):
raise NotImplementedError(
f"Does not currently support {cube.coord(y_coord).coord_system} regrid method"
)

# Transform input coordinates onto rotated grid if requested
if latlon_in_type == "realworld":
lon_tr, lat_tr = transform_lat_long_points(lon_pt, lat_pt, cube)
elif latlon_in_type == "rotated":
lon_tr, lat_tr = lon_pt, lat_pt

# Get axis
lat, lon = cube.coord(y_coord), cube.coord(x_coord)

# Get bounds
lat_min, lon_min = lat.points.min(), lon.points.min()
lat_max, lon_max = lat.points.max(), lon.points.max()

# Get boundaries of frame to avoid selecting gridpoint close to domain edge
lat_min_bound, lon_min_bound = (
lat.points[boundary_margin - 1],
lon.points[boundary_margin - 1],
)
if not isinstance(cube.coord(y_coord).coord_system, supported_grids):
raise NotImplementedError(
f"Does not currently support {cube.coord(y_coord).coord_system} regrid method"
lat_max_bound, lon_max_bound = (
lat.points[-boundary_margin],
lon.points[-boundary_margin],
)

# Transform input coordinates onto rotated grid if requested
if latlon_in_type == "realworld":
lon_tr, lat_tr = transform_lat_long_points(lon_pt, lat_pt, cube)
elif latlon_in_type == "rotated":
lon_tr, lat_tr = lon_pt, lat_pt

# Get axis
lat, lon = cube.coord(y_coord), cube.coord(x_coord)

# Get bounds
lat_min, lon_min = lat.points.min(), lon.points.min()
lat_max, lon_max = lat.points.max(), lon.points.max()

# Get boundaries of frame to avoid selecting gridpoint close to domain edge
lat_min_bound, lon_min_bound = (
lat.points[boundary_margin - 1],
lon.points[boundary_margin - 1],
)
lat_max_bound, lon_max_bound = (
lat.points[-boundary_margin],
lon.points[-boundary_margin],
)

# Check to see if selected point is outside the domain
if (lat_tr < lat_min) or (lat_tr > lat_max):
raise ValueError("Selected point is outside the domain.")
else:
if (lon_tr < lon_min) or (lon_tr > lon_max):
if (lon_tr + 360.0 >= lon_min) and (lon_tr + 360.0 <= lon_max):
lon_tr += 360.0
elif (lon_tr - 360.0 >= lon_min) and (lon_tr - 360.0 <= lon_max):
lon_tr -= 360.0
else:
raise ValueError("Selected point is outside the domain.")

# Check to see if selected point is near the domain boundaries
if (
(lat_tr < lat_min_bound)
or (lat_tr > lat_max_bound)
or (lon_tr < lon_min_bound)
or (lon_tr > lon_max_bound)
):
warnings.warn(
f"Selected point is within {boundary_margin} gridlengths of the domain edge, data may be unreliable.",
category=BoundaryWarning,
stacklevel=2,
)
# Check to see if selected point is outside the domain
if (lat_tr < lat_min) or (lat_tr > lat_max):
raise ValueError("Selected point is outside the domain.")
else:
if (lon_tr < lon_min) or (lon_tr > lon_max):
if (lon_tr + 360.0 >= lon_min) and (lon_tr + 360.0 <= lon_max):
lon_tr += 360.0
elif (lon_tr - 360.0 >= lon_min) and (lon_tr - 360.0 <= lon_max):
lon_tr -= 360.0
else:
raise ValueError("Selected point is outside the domain.")

# Check to see if selected point is near the domain boundaries
if (
(lat_tr < lat_min_bound)
or (lat_tr > lat_max_bound)
or (lon_tr < lon_min_bound)
or (lon_tr > lon_max_bound)
):
warnings.warn(
f"Selected point is within {boundary_margin} gridlengths of the domain edge, data may be unreliable.",
category=BoundaryWarning,
stacklevel=2,
)

regrid_method = getattr(iris.analysis, method, None)
if not callable(regrid_method):
raise NotImplementedError(f"Does not currently support {method} regrid method")
cube_rgd = cube.interpolate(((lat, lat_tr), (lon, lon_tr)), regrid_method())
return cube_rgd
regrid_method = getattr(iris.analysis, method, None)
if not callable(regrid_method):
raise NotImplementedError(
f"Does not currently support {method} regrid method"
)
cube_rgd = cube.interpolate(((lat, lat_tr), (lon, lon_tr)), regrid_method())
regridded_cubes.append(cube_rgd)
# Preserve returning a cube if only a cube has been supplied to regrid.
if len(regridded_cubes) == 1:
return regridded_cubes[0]
else:
return regridded_cubes


def transform_lat_long_points(lon, lat, cube):
Expand Down
Loading