From 97cb740bf788927eae957e1d44eaff02e782c6f9 Mon Sep 17 00:00:00 2001 From: Simon Osborne Date: Tue, 12 May 2026 14:58:33 +0100 Subject: [PATCH 01/12] Modified to multi variable constraint function --- src/CSET/operators/constraints.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/CSET/operators/constraints.py b/src/CSET/operators/constraints.py index 905c660e4..7a38d5336 100644 --- a/src/CSET/operators/constraints.py +++ b/src/CSET/operators/constraints.py @@ -63,13 +63,28 @@ def generate_var_constraint(varname: str, **kwargs) -> iris.Constraint: Returns ------- - varname_constraint: iris.Constraint + An Iris constraint for either: + - a single UM STASH code + - a single variable name + - a list of variable names (Cardington multi-input case) """ - if re.match(r"m[0-9]{2}s[0-9]{2}i[0-9]{3}$", varname): - varname_constraint = iris.AttributeConstraint(STASH=varname) - else: - varname_constraint = iris.Constraint(name=varname) - return varname_constraint + _STASH_RE = re.compile(r"m\d{2}s\d{2}i\d{3}$") + # ---- CASE 1: list of variable names (e.g. Cardington multi-variable) ---- + if isinstance(varname, (list, tuple)): + return iris.Constraint( + cube_func=lambda cube: ( + cube.var_name in varname + or cube.standard_name in varname + or cube.name() in varname + ) + ) + + # ---- CASE 2: single UM STASH code ---- + if _STASH_RE.match(varname): + return iris.AttributeConstraint(STASH=varname) + + # ---- CASE 3: single variable name ---- + return iris.Constraint(name=varname) def generate_level_constraint( From 5b5a53df57b4a1c411f677d3b583b94617c1c89e Mon Sep 17 00:00:00 2001 From: Simon Osborne Date: Wed, 27 May 2026 14:50:50 +0100 Subject: [PATCH 02/12] Update src/CSET/operators/constraints.py Co-authored-by: James Frost --- src/CSET/operators/constraints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CSET/operators/constraints.py b/src/CSET/operators/constraints.py index 7a38d5336..d4a0fe338 100644 --- a/src/CSET/operators/constraints.py +++ b/src/CSET/operators/constraints.py @@ -73,7 +73,7 @@ def generate_var_constraint(varname: str, **kwargs) -> iris.Constraint: if isinstance(varname, (list, tuple)): return iris.Constraint( cube_func=lambda cube: ( - cube.var_name in varname + cube.long_name in varname or cube.standard_name in varname or cube.name() in varname ) From 01093ea78b9302256f4174e420dace9c37477ef2 Mon Sep 17 00:00:00 2001 From: Simon Osborne Date: Wed, 27 May 2026 15:14:00 +0100 Subject: [PATCH 03/12] Update src/CSET/operators/constraints.py Co-authored-by: James Frost --- src/CSET/operators/constraints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CSET/operators/constraints.py b/src/CSET/operators/constraints.py index d4a0fe338..d3714881f 100644 --- a/src/CSET/operators/constraints.py +++ b/src/CSET/operators/constraints.py @@ -75,7 +75,7 @@ def generate_var_constraint(varname: str, **kwargs) -> iris.Constraint: cube_func=lambda cube: ( cube.long_name in varname or cube.standard_name in varname - or cube.name() in varname + or cube.var_name in varname ) ) From 49878825b1d664e85c83fa6fd6d8127aa99f2401 Mon Sep 17 00:00:00 2001 From: Simon Osborne Date: Wed, 27 May 2026 15:26:05 +0100 Subject: [PATCH 04/12] Add constraints test for multiple variables names --- tests/operators/test_constraints.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/operators/test_constraints.py b/tests/operators/test_constraints.py index e4883e5d6..398f8bbdc 100644 --- a/tests/operators/test_constraints.py +++ b/tests/operators/test_constraints.py @@ -16,6 +16,8 @@ from datetime import datetime +import iris +import numpy as np import pytest from CSET.operators import constraints @@ -277,3 +279,25 @@ def test_generate_attribute_constraint_with_value(): ) expected_attr_constraint = "AttributeConstraint({'test': '2'})" assert expected_attr_constraint in repr(attr_constraint) + + +def test_generate_var_constraint_multiple_names(): + """Test constraint works for multiple variable names.""" + # Create two cubes with different names + cube1 = iris.cube.Cube(np.arange(5), long_name="air_temperature") + cube2 = iris.cube.Cube(np.arange(5), long_name="wind_speed") + # Third cube that should NOT match + cube3 = iris.cube.Cube(np.arange(5), long_name="surface_pressure") + # Generate constraint with multiple names + constraint = constraints.generate_var_constraint(["air_temperature", "wind_speed"]) + # Apply constraint + cubes = iris.cube.CubeList([cube1, cube2, cube3]) + result = cubes.extract(constraint) + # Check correct cubes are selected + names = [c.name() for c in result] + assert "air_temperature" in names + assert "wind_speed" in names + assert "surface_pressure" not in names + + # Should only return 2 cubes + assert len(result) == 2 From e61a4924f11e8c31fac451fce49a2b86d97380ad Mon Sep 17 00:00:00 2001 From: Simon Osborne Date: Wed, 27 May 2026 16:03:14 +0100 Subject: [PATCH 05/12] Correct simple ruff formatting error --- tests/operators/test_constraints.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/operators/test_constraints.py b/tests/operators/test_constraints.py index 398f8bbdc..d478f082a 100644 --- a/tests/operators/test_constraints.py +++ b/tests/operators/test_constraints.py @@ -284,12 +284,13 @@ def test_generate_attribute_constraint_with_value(): def test_generate_var_constraint_multiple_names(): """Test constraint works for multiple variable names.""" # Create two cubes with different names - cube1 = iris.cube.Cube(np.arange(5), long_name="air_temperature") - cube2 = iris.cube.Cube(np.arange(5), long_name="wind_speed") + cube1 = iris.cube.Cube(np.arange(5), long_name="temperature_long") + cube1.var_name = "temperature" + cube2 = iris.cube.Cube(np.arange(5), standard_name="wind_speed") # Third cube that should NOT match cube3 = iris.cube.Cube(np.arange(5), long_name="surface_pressure") # Generate constraint with multiple names - constraint = constraints.generate_var_constraint(["air_temperature", "wind_speed"]) + constraint = constraints.generate_var_constraint(["temperature", "wind_speed"]) # Apply constraint cubes = iris.cube.CubeList([cube1, cube2, cube3]) result = cubes.extract(constraint) From 91e1d3b7745f26f09ed33844a4c55d626f43b98c Mon Sep 17 00:00:00 2001 From: Simon Osborne Date: Wed, 27 May 2026 16:15:22 +0100 Subject: [PATCH 06/12] Modify test constraints for multiple names --- tests/operators/test_constraints.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/operators/test_constraints.py b/tests/operators/test_constraints.py index c064be546..e7be02380 100644 --- a/tests/operators/test_constraints.py +++ b/tests/operators/test_constraints.py @@ -285,25 +285,29 @@ def test_generate_var_constraint_multiple_names(): """Test constraint works for multiple variable names.""" # Create two cubes with different names cube1 = iris.cube.Cube(np.arange(5), long_name="temperature_long") - cube1.var_name = "temperature" + cube1.var_name = "var_temperature" cube2 = iris.cube.Cube(np.arange(5), standard_name="wind_speed") # Third cube that should NOT match cube3 = iris.cube.Cube(np.arange(5), long_name="surface_pressure") # Generate constraint with multiple names - constraint = constraints.generate_var_constraint(["temperature", "wind_speed"]) + constraint = constraints.generate_var_constraint(["var_temperature", "wind_speed"]) # Apply constraint cubes = iris.cube.CubeList([cube1, cube2, cube3]) result = cubes.extract(constraint) # Check correct cubes are selected - names = [c.name() for c in result] - assert "air_temperature" in names - assert "wind_speed" in names - assert "surface_pressure" not in names + result_names = [c.name() for c in result] + assert cube1 in result + assert cube2 in result + assert cube3 not in result + + assert "temperature_long" in result_names + assert "wind_speed" in result_names + assert "surface_pressure" not in result_names # Should only return 2 cubes assert len(result) == 2 - + def test_generate_remove_single_level_constraint(): """Tests constraint to remove default model_level_number of zero.""" remove_level_constraint = constraints.generate_remove_single_level_constraint( @@ -320,4 +324,3 @@ def test_generate_remove_single_level_constraint_non_default(): ) expected_constraint = "Constraint(coord_values={'model_level_number': . at" assert expected_constraint in repr(remove_level_constraint) - From 2b556c8002be6bc893f588ed3e4ff3b93913e1ee Mon Sep 17 00:00:00 2001 From: Simon Osborne Date: Fri, 3 Jul 2026 16:14:32 +0100 Subject: [PATCH 07/12] Update src/CSET/operators/constraints.py Co-authored-by: ukmo-huw-lewis <48992503+ukmo-huw-lewis@users.noreply.github.com> --- src/CSET/operators/constraints.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CSET/operators/constraints.py b/src/CSET/operators/constraints.py index 9fa4b2bb5..d29303dc4 100644 --- a/src/CSET/operators/constraints.py +++ b/src/CSET/operators/constraints.py @@ -63,10 +63,10 @@ def generate_var_constraint(varname: str, **kwargs) -> iris.Constraint: Returns ------- - An Iris constraint for either: - - a single UM STASH code - - a single variable name - - a list of variable names (Cardington multi-input case) +varname_constraint: iris.Constraint + If a UM STASHcode is requested, varname constraint is by STASHcode + If a single variable name is requested, constraint by varname + If multiple variable names are requested, constrain by list of variables. """ _STASH_RE = re.compile(r"m\d{2}s\d{2}i\d{3}$") # ---- CASE 1: list of variable names (e.g. Cardington multi-variable) ---- From b15ce96eda469e1f4a9a99fcff1f0c3c04c9884a Mon Sep 17 00:00:00 2001 From: Simon Osborne Date: Fri, 3 Jul 2026 16:15:01 +0100 Subject: [PATCH 08/12] Update src/CSET/operators/constraints.py Co-authored-by: ukmo-huw-lewis <48992503+ukmo-huw-lewis@users.noreply.github.com> --- src/CSET/operators/constraints.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/CSET/operators/constraints.py b/src/CSET/operators/constraints.py index d29303dc4..1b959a4b8 100644 --- a/src/CSET/operators/constraints.py +++ b/src/CSET/operators/constraints.py @@ -68,22 +68,22 @@ def generate_var_constraint(varname: str, **kwargs) -> iris.Constraint: If a single variable name is requested, constraint by varname If multiple variable names are requested, constrain by list of variables. """ - _STASH_RE = re.compile(r"m\d{2}s\d{2}i\d{3}$") - # ---- CASE 1: list of variable names (e.g. Cardington multi-variable) ---- - if isinstance(varname, (list, tuple)): - return iris.Constraint( +# Case 1: UM STASHcode input +if _STASH_RE.match(varname): + return iris.AttributeConstraint(STASH=varname) + +# Case 2: Multiple varnames +elif isinstance(varname, (list, tuple)): +return iris.Constraint( cube_func=lambda cube: ( cube.long_name in varname or cube.standard_name in varname or cube.var_name in varname ) ) - - # ---- CASE 2: single UM STASH code ---- - if _STASH_RE.match(varname): - return iris.AttributeConstraint(STASH=varname) - - # ---- CASE 3: single variable name ---- + +# Case 3: Single varname +else: return iris.Constraint(name=varname) From d17ed0abccd0fe2c8b415c84f9cf4d72d6548608 Mon Sep 17 00:00:00 2001 From: Simon Osborne Date: Fri, 3 Jul 2026 16:44:31 +0100 Subject: [PATCH 09/12] Fix indentation and formatting in constraints.py --- src/CSET/operators/constraints.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/CSET/operators/constraints.py b/src/CSET/operators/constraints.py index 1b959a4b8..8b1d100c3 100644 --- a/src/CSET/operators/constraints.py +++ b/src/CSET/operators/constraints.py @@ -63,28 +63,28 @@ def generate_var_constraint(varname: str, **kwargs) -> iris.Constraint: Returns ------- -varname_constraint: iris.Constraint - If a UM STASHcode is requested, varname constraint is by STASHcode - If a single variable name is requested, constraint by varname - If multiple variable names are requested, constrain by list of variables. + varname_constraint: iris.Constraint + If a UM STASHcode is requested, varname constraint is by STASHcode + If a single variable name is requested, constraint by varname + If multiple variable names are requested, constrain by list of variables. """ -# Case 1: UM STASHcode input -if _STASH_RE.match(varname): + # Case 1: UM STASHcode input + if _STASH_RE.match(varname): return iris.AttributeConstraint(STASH=varname) - -# Case 2: Multiple varnames -elif isinstance(varname, (list, tuple)): -return iris.Constraint( + + # Case 2: Multiple varnames + elif isinstance(varname, (list, tuple)): + return iris.Constraint( cube_func=lambda cube: ( cube.long_name in varname or cube.standard_name in varname or cube.var_name in varname ) ) - -# Case 3: Single varname -else: - return iris.Constraint(name=varname) + + # Case 3: Single varname + else: + return iris.Constraint(name=varname) def generate_level_constraint( From 363f84bfec271d4fa41527c8d50b18f62b8cc70b Mon Sep 17 00:00:00 2001 From: Simon Osborne Date: Fri, 3 Jul 2026 16:48:48 +0100 Subject: [PATCH 10/12] Add regex pattern for STASH code validation --- src/CSET/operators/constraints.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/CSET/operators/constraints.py b/src/CSET/operators/constraints.py index 8b1d100c3..7739ba082 100644 --- a/src/CSET/operators/constraints.py +++ b/src/CSET/operators/constraints.py @@ -26,6 +26,9 @@ import CSET.operators._utils as operator_utils from CSET._common import iter_maybe +# STASH code pattern: mXXsXXiXXX where X is a digit +_STASH_RE = re.compile(r"^m\d{2}s\d{2}i\d{3}$") + def generate_stash_constraint(stash: str, **kwargs) -> iris.AttributeConstraint: """Generate constraint from STASH code. From 26add4e091702ba33824b673c8bddc8f7b16c398 Mon Sep 17 00:00:00 2001 From: Simon Osborne Date: Wed, 8 Jul 2026 16:46:52 +0100 Subject: [PATCH 11/12] Refactor variable name handling in constraints.py Refactor variable name handling in constraints.py to improve readability and maintainability. --- src/CSET/operators/constraints.py | 36 ++++++++++++++++++------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/CSET/operators/constraints.py b/src/CSET/operators/constraints.py index 7739ba082..caeb8a1c7 100644 --- a/src/CSET/operators/constraints.py +++ b/src/CSET/operators/constraints.py @@ -71,23 +71,29 @@ def generate_var_constraint(varname: str, **kwargs) -> iris.Constraint: If a single variable name is requested, constraint by varname If multiple variable names are requested, constrain by list of variables. """ - # Case 1: UM STASHcode input - if _STASH_RE.match(varname): - return iris.AttributeConstraint(STASH=varname) - - # Case 2: Multiple varnames - elif isinstance(varname, (list, tuple)): - return iris.Constraint( - cube_func=lambda cube: ( - cube.long_name in varname - or cube.standard_name in varname - or cube.var_name in varname - ) +# Case 1: UM STASHcode input +if _STASH_RE.match(varname): + return iris.AttributeConstraint(STASH=varname) + +# Ensure access to variable vector components for computed fields +if "wind_speed_at_10m" in iter_maybe(varname): + varname = [varname] + varname.extend(["eastward_wind_at_10m", "northward_wind_at_10m"]) + +# Case 2: Multiple varnames +if isinstance(varname, (list, tuple)): + varname_constraint = iris.Constraint( + cube_func=lambda cube: ( + cube.long_name in varname + or cube.standard_name in varname + or cube.var_name in varname ) + ) - # Case 3: Single varname - else: - return iris.Constraint(name=varname) +else: + varname_constraint = iris.Constraint(name=varname) + +return varname_constraint def generate_level_constraint( From 60828938d9b7cf8b7fd464635ca7e5bce289abda Mon Sep 17 00:00:00 2001 From: Simon Osborne Date: Wed, 8 Jul 2026 17:03:26 +0100 Subject: [PATCH 12/12] Reconfigure varname constraint to add in 10m wind speeds --- src/CSET/operators/constraints.py | 48 +++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/CSET/operators/constraints.py b/src/CSET/operators/constraints.py index caeb8a1c7..950c7d66a 100644 --- a/src/CSET/operators/constraints.py +++ b/src/CSET/operators/constraints.py @@ -55,45 +55,45 @@ def generate_stash_constraint(stash: str, **kwargs) -> iris.AttributeConstraint: def generate_var_constraint(varname: str, **kwargs) -> iris.Constraint: """Generate constraint from variable name or STASH code. - Operator that takes a CF compliant variable name string, and generates an + Operator that takes a CF compliant variable name string or list of names, and generates an iris constraint to be passed into the read or filter operator. Can also be passed a STASH code to generate a STASH constraint. Arguments --------- - varname: str - CF compliant name of variable, or a UM STASH code such as "m01s03i236". + varname: str | list[str] + CF compliant name(s) of variable, or a UM STASH code such as "m01s03i236". Returns ------- varname_constraint: iris.Constraint - If a UM STASHcode is requested, varname constraint is by STASHcode + If a single UM STASHcode is requested, varname constraint is by STASHcode If a single variable name is requested, constraint by varname If multiple variable names are requested, constrain by list of variables. """ -# Case 1: UM STASHcode input -if _STASH_RE.match(varname): - return iris.AttributeConstraint(STASH=varname) - -# Ensure access to variable vector components for computed fields -if "wind_speed_at_10m" in iter_maybe(varname): - varname = [varname] - varname.extend(["eastward_wind_at_10m", "northward_wind_at_10m"]) - -# Case 2: Multiple varnames -if isinstance(varname, (list, tuple)): - varname_constraint = iris.Constraint( - cube_func=lambda cube: ( - cube.long_name in varname - or cube.standard_name in varname - or cube.var_name in varname + # Case 1: UM STASHcode input + if isinstance(varname, str) and _STASH_RE.match(varname): + return iris.AttributeConstraint(STASH=varname) + + # Ensure access to variable vector components for computed fields + if "wind_speed_at_10m" in iter_maybe(varname): + varname = [varname] + varname.extend(["eastward_wind_at_10m", "northward_wind_at_10m"]) + + # Case 2: Multiple varnames + if isinstance(varname, (list, tuple)): + varname_constraint = iris.Constraint( + cube_func=lambda cube: ( + cube.long_name in varname + or cube.standard_name in varname + or cube.var_name in varname + ) ) - ) -else: - varname_constraint = iris.Constraint(name=varname) + else: + varname_constraint = iris.Constraint(name=varname) -return varname_constraint + return varname_constraint def generate_level_constraint(