Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mask operators #967

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
132 changes: 132 additions & 0 deletions src/CSET/operators/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,63 @@
import iris
import iris.cube
import iris.exceptions
import numpy as np

from CSET._common import iter_maybe


def apply_mask(
original_field: Union[iris.cube.Cube, iris.cube.CubeList],
masks: Union[iris.cube.Cube, iris.cube.CubeList],
) -> Union[iris.cube.Cube, iris.cube.CubeList]:
"""Apply a mask to given data as a masked array.
daflack marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
original_field: iris.cube.Cube | iris.cube.CubeList
The field(s) to be masked.
masks: iris.cube.Cube | iris.cube.CubeList
The mask being applied to the original field. Masks should
be the same length type as the original_field. The masks are applied
to each individual Cube in a CubeList in the order they are provided.

Returns
-------
A masked field.

Return type
-----------
iris.cube.Cube | iris.cube.CubeList

Notes
daflack marked this conversation as resolved.
Show resolved Hide resolved
-----
The mask is first converted to 1s and NaNs before multiplication with
the original data.

As discussed in generate_mask, you can combine multiple masks in a
recipe using other functions before applying the mask to the data.

Examples
--------
>>> land_points_only = apply_mask(temperature, land_mask)
"""
mask_list = iris.cube.CubeList()
for mask in iter_maybe(masks):
mask.data[mask.data == 0] = np.nan
mask_list.append(mask)
if len(mask_list) == 1:
daflack marked this conversation as resolved.
Show resolved Hide resolved
masked_field = original_field.copy()
daflack marked this conversation as resolved.
Show resolved Hide resolved
masked_field.data *= mask_list[0].data
masked_field.attributes["mask"] = f"mask_of_{original_field.name()}"
return masked_field
else:
mask_field_list = iris.cube.CubeList()
for data, mask in zip(original_field, mask_list, strict=True):
daflack marked this conversation as resolved.
Show resolved Hide resolved
mask_field_data = data.copy()
mask_field_data.data *= mask.data
mask_field_data.attributes["mask"] = f"mask_of_{data.name()}"
mask_field_list.append(mask_field_data)
return mask_field_list


def filter_cubes(
Expand Down Expand Up @@ -93,3 +150,78 @@ def filter_multiple_cubes(
"The constraints don't produce a single cube per constraint."
) from err
return filtered_cubes


def generate_mask(
mask_field: Union[iris.cube.Cube, iris.cube.CubeList],
condition: str,
value: float,
) -> Union[iris.cube.Cube, iris.cube.CubeList]:
"""Generate a mask to remove data not meeting conditions.
daflack marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
mask_field: iris.cube.Cube | iris.cube.CubeList
The field to be used for creating the mask.
condition: str
The type of condition applied, six available options:
'==','!=','<','<=','>', and '>='.
value: float
The value on the other side of the condition.
daflack marked this conversation as resolved.
Show resolved Hide resolved

Returns
-------
Masks meeting the conditions applied.

Return type
-----------
iris.cube.Cube | iris.cube.CubeList

Raises
------
ValueError: Unexpected value for condition. Expected ==, !=, >, >=, <, <=
Raised when condition is not supported.

Notes
-----
The mask is created in the opposite sense to numpy.ma.masked_arrays. This
method was chosen to allow easy combination of masks together outside of
this function using misc.addition or misc.multiplication depending on
applicability. The combinations can be of any fields such as orography >
500 m, and humidity == 100 %.

The conversion to a masked array occurs in the apply_mask routine, which
should happen after all relevant masks have been combined.

The same condition and value will be used when masking multiple cubes.

Examples
--------
>>> land_mask = generate_mask(land_sea_mask,'==',1)
"""
mask_list = iris.cube.CubeList()
for cube in iter_maybe(mask_field):
masks = cube.copy()
masks.data = np.zeros(masks.data.shape)
if condition == "==":
masks.data[cube.data == value] = 1
elif condition == "!=":
masks.data[cube.data != value] = 1
elif condition == ">":
masks.data[cube.data > value] = 1
elif condition == ">=":
masks.data[cube.data >= value] = 1
elif condition == "<":
masks.data[cube.data < value] = 1
elif condition == "<=":
masks.data[cube.data <= value] = 1
else:
daflack marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError("""Unexpected value for condition. Expected ==, !=,
daflack marked this conversation as resolved.
Show resolved Hide resolved
>, >=, <, <=""")
daflack marked this conversation as resolved.
Show resolved Hide resolved
masks.attributes["mask"] = f"mask_for_{cube.name()}_{condition}_{value}"

mask_list.append(masks)
if len(mask_list) == 1:
daflack marked this conversation as resolved.
Show resolved Hide resolved
return mask_list[0]
else:
return mask_list
48 changes: 48 additions & 0 deletions src/CSET/recipes/example_combined_mask_addition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
category: Quick Look
title: Example adding masks for 'OR' or "between" type filters
description: |
Generates and applies two masks, and adds them together for more complex stratified analysis.

steps:
- operator: read.read_cubes

- operator: filters.apply_mask
original_field:
operator: filters.filter_cubes
constraint:
operator: constraints.combine_constraints
variable_constraint:
operator: constraints.generate_var_constraint
varname: surface_air_temperature
cell_method_constraint:
operator: constraints.generate_cell_methods_constraint
cell_methods: []
masks:
operator: misc.addition
addend_1:
operator: filters.generate_mask
mask_field:
operator: filters.filter_cubes
constraint:
operator: constraints.generate_var_constraint
varname: surface_altitude
condition: '<'
value: 500
addend_2:
operator: filters.generate_mask
mask_field:
operator: filters.filter_cubes
constraint:
operator: constraints.generate_var_constraint
varname: surface_altitude
condition: '>'
value: 200

- operator: collapse.collapse
coordinate: [grid_latitude, grid_longitude]
method: MEAN

- operator: write.write_cube_to_nc
overwrite: True

- operator: plot.plot_line_series
48 changes: 48 additions & 0 deletions src/CSET/recipes/example_combined_mask_multiplication.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
category: Quick Look
title: Example adding masks for 'AND' type filters
description: |
Generates and applies two masks, and multiplies them together for more complex stratified analysis.

steps:
- operator: read.read_cubes

- operator: filters.apply_mask
original_field:
operator: filters.filter_cubes
constraint:
operator: constraints.combine_constraints
variable_constraint:
operator: constraints.generate_var_constraint
varname: surface_air_temperature
cell_method_constraint:
operator: constraints.generate_cell_methods_constraint
cell_methods: []
masks:
operator: misc.multiplication
multiplicand:
operator: filters.generate_mask
mask_field:
operator: filters.filter_cubes
constraint:
operator: constraints.generate_var_constraint
varname: surface_altitude
condition: '>'
value: 500
multiplier:
operator: filters.generate_mask
mask_field:
operator: filters.filter_cubes
constraint:
operator: constraints.generate_var_constraint
varname: surface_air_temperature
condition: '<='
value: 273

- operator: collapse.collapse
coordinate: [grid_latitude, grid_longitude]
method: MEAN

- operator: write.write_cube_to_nc
overwrite: True

- operator: plot.plot_line_series
36 changes: 36 additions & 0 deletions src/CSET/recipes/example_simple_mask.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
category: Quick Look
title: Example simple mask application
description: Generates and applies a simple mask to a field for stratified analysis.

steps:
- operator: read.read_cubes

- operator: filters.apply_mask
original_field:
operator: filters.filter_cubes
constraint:
operator: constraints.combine_constraints
variable_constraint:
operator: constraints.generate_var_constraint
varname: surface_air_temperature
cell_method_constraint:
operator: constraints.generate_cell_methods_constraint
cell_methods: []
masks:
operator: filters.generate_mask
mask_field:
operator: filters.filter_cubes
constraint:
operator: constraints.generate_var_constraint
varname: surface_altitude
condition: '>='
value: 500

- operator: collapse.collapse
coordinate: [grid_latitude, grid_longitude]
method: MEAN

- operator: write.write_cube_to_nc
overwrite: True

- operator: plot.plot_line_series
21 changes: 21 additions & 0 deletions src/CSET/recipes/example_spatial_plot_of_mask.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
category: Surface Spatial Plot
title: Example plot of a mask
description: |
Generates a mask and then provides a spatial map of the mask.
steps:
- operator: read.read_cubes

- operator: filters.generate_mask
mask_field:
operator: filters.filter_cubes
constraint:
operator: constraints.generate_var_constraint
varname: surface_altitude
condition: '>='
value: 500

- operator: plot.spatial_pcolormesh_plot

- operator: write.write_cube_to_nc
overwrite: True
Loading
Loading