Skip to content
Open
44 changes: 44 additions & 0 deletions src/CSET/operators/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -703,3 +703,47 @@ def differentiate(
return new_cubelist[0]
else:
return new_cubelist


def convert_visibility_to_km(cubes, **kwargs):
"""Ensure visibility is expressed in kilometres.

Arguments
---------
cubes : iris.cube.Cube | iris.cube.CubeList
A Cube or CubeList containing visibility data. Each cube must
have units that are either already in kilometres or convertible
to kilometres (e.g. metres).

Returns
-------
iris.cube.Cube | iris.cube.CubeList
The input cube(s) with visibility expressed in kilometres.
If the input is a single Cube, a Cube is returned. If the input
is an iterable of cubes, a CubeList is returned.

Notes
-----
- If a cube has units of metres ("m"), it is converted to kilometres
using ``cube.convert_units("km")``.
- If a cube is already in kilometres, it is left unchanged.
- Any units that are convertible to kilometres will be converted
using Iris unit handling.
- This operator applies the conversion in-place on each cube.

Examples
--------
>>> vis_km = convert_visibility_to_km(visibility_cube)

>>> vis_list_km = convert_visibility_to_km([cube1, cube2])
"""
if isinstance(cubes, iris.cube.Cube):
cubes = iris.cube.CubeList([cubes])
else:
cubes = iris.cube.CubeList(cubes)

for cube in cubes:
if cube.units.is_convertible("km") and str(cube.units) != "km":
cube.convert_units("km")

return cubes if len(cubes) > 1 else cubes[0]
95 changes: 61 additions & 34 deletions tests/operators/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,39 +568,66 @@ def test_extract_common_points_nocommonpoints(vertical_profile_cube):
)


def test_remove_scalar_coord():
"""Test that scalar coordinate be removed."""
# Create simple 1D cube
data = np.arange(5)
time = iris.coords.DimCoord(
np.arange(5), standard_name="time", units="hours since 1970-01-01"
def _make_cube(data, units="m"):
"""Tiny cube generator."""
data = np.asarray(data)
ny, nx = data.shape
lat = iris.coords.DimCoord(np.arange(ny), standard_name="latitude", units="degrees")
lon = iris.coords.DimCoord(
np.arange(nx), standard_name="longitude", units="degrees"
)
cube = iris.cube.Cube(data, dim_coords_and_dims=[(time, 0)])
# Add a scalar coord
realization = iris.coords.AuxCoord(1, long_name="realization")
cube.add_aux_coord(realization)
# Check it's present and scalar
assert cube.coords("realization")
assert cube.coord_dims("realization") == ()
# Run function
out = misc.remove_scalar_coords(cube, ["realization"])
# Check it’s removed
cube_out = out[0]
assert not cube_out.coords("realization")


def test_not_remove_non_scalar_coord():
"""Test that non-scalar coordinate is not removed."""
# Create 1D cube
data = np.arange(5)
time = iris.coords.DimCoord(
np.arange(5), standard_name="time", units="hours since 1970-01-01"
return iris.cube.Cube(
data,
dim_coords_and_dims=[(lat, 0), (lon, 1)],
units=units,
)
cube = iris.cube.Cube(data, dim_coords_and_dims=[(time, 0)])
# Confirm it's non-scalar
assert cube.coord_dims("time") != ()
# Run function
out = misc.remove_scalar_coords(cube, ["time"])
# Check it is still present
cube_out = out[0]
assert cube_out.coords("time")


def test_visibility_m_to_km():
"""Test that visibility is converted to kilometres from metres (common use)."""
cube = _make_cube([[1000.0]], units="m")
out = misc.convert_visibility_to_km(cube)
assert out.units == "km"
assert np.allclose(out.data, [[1.0]])


def test_visibility_already_km():
"""Test that visibility already in km is unchanged."""
cube = _make_cube([[1.0]], units="km")
out = misc.convert_visibility_to_km(cube)
assert out.units == "km"
assert np.allclose(out.data, [[1.0]])


def test_visibility_cubelist():
"""Test that cubelist is converted to km."""
cubes = [
_make_cube([[1000.0]], units="m"),
_make_cube([[2000.0]], units="m"),
]
out = misc.convert_visibility_to_km(cubes)
assert isinstance(out, iris.cube.CubeList)
assert len(out) == 2
assert np.allclose(out[0].data, [[1.0]])
assert np.allclose(out[1].data, [[2.0]])
assert all(c.units == "km" for c in out)


def test_visibility_mixed_units_cubelist():
"""Test for cubelist with mixed units."""
cubes = [
_make_cube([[1000.0]], units="m"),
_make_cube([[1.0]], units="km"),
]
out = misc.convert_visibility_to_km(cubes)
assert np.allclose(out[0].data, [[1.0]])
assert np.allclose(out[1].data, [[1.0]])
assert all(c.units == "km" for c in out)


def test_visibility_non_convertible_units():
"""Test for cubelist with non-convertible units."""
cube = _make_cube([[1.0]], units="seconds")
out = misc.convert_visibility_to_km(cube)
assert out.units == "seconds"
assert np.allclose(out.data, [[1.0]])
Loading