Skip to content

Conversation

@alexproschitsky
Copy link

@alexproschitsky alexproschitsky commented Sep 5, 2025

Addresses #GitHubissuenum

Adding MIT Lincoln Laboratory code to produce Icing Severity Multivariate Regression.

Data for acceptance tests have been provided via PR metoppv/improver_test_data#106

Please advise of any necessary changes.
Alex Proschitsky

Testing:

  • Ran tests and they passed OK
  • Added new tests for the new feature(s)

CLA

  • If a new developer, signed up to CLA

Alexander Proschitsky and others added 4 commits September 5, 2025 16:19
A previous merge created extraneous lines for integrate-time-bounds test file which are not even part of the icing multivariate regression. Put the correct lines in place  to match the master branch,
Copy link
Member

@MoseleyS MoseleyS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @alexproschitsky

I've made some suggestions that I think will improve what is already good code.

Comment on lines +64 to +71
IF RH is greater than 70% and T is between 250.0K and 273.15K THEN:
AISI=100*TANH(0.06*RH-4.0)[TANH(0.1(T-247.0)]

Categorical icing severity levels are defined as
AISI < 58 : "No Icing"
58 <= AISI < 85 : "Light Icing"
85 <= AISI < 92 : "Moderate Icing"
92 <= AISI : "Severe Icing"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a published paper that we can refer to here?

Comment on lines +105 to +112
output_cubes.append(self._extract_input(cubes, input_name))
if not output_cubes[-1].units in units:
expected_unit_string = " or ".join(map(str, units))
received_unit_string = str(output_cubes[-1].units)
raise ValueError(
f"The {output_cubes[-1].name()} units are incorrect, expected "
f"units as {expected_unit_string} but received {received_unit_string})."
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you attempt to convert the cube to the units you want, it will either work, and you can use whatever you've got (Fahrenheit becomes Kelvin) or an error message is raised (Metres cannot become Kelvin).

Also, do this before appending the result to get nicer (well, I think it's nicer) code.

Suggested change
output_cubes.append(self._extract_input(cubes, input_name))
if not output_cubes[-1].units in units:
expected_unit_string = " or ".join(map(str, units))
received_unit_string = str(output_cubes[-1].units)
raise ValueError(
f"The {output_cubes[-1].name()} units are incorrect, expected "
f"units as {expected_unit_string} but received {received_unit_string})."
)
cube = self._extract_input(cubes, input_name)
cube.convert_units(units)
output_cubes.append(cube)

f"units as {expected_unit_string} but received {received_unit_string})."
)

t,rh = output_cubes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't safe, as dictionaries do not guarantee ordering. As you only have two inputs, probably easiest and clearest to unroll this loop. Also t and rh are very brief and open to misinterpretation. I recommend spelling out temperature and relative_humidity for clarity.

Comment on lines +116 to +119
if t.coord("forecast_reference_time") != rh.coord("forecast_reference_time"):
raise ValueError(
f"{t.name()} and {rh.name()} do not have the same forecast reference time"
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this test required?

What I'm thinking is that "forecast_reference_time" is not the only way of defining this. Some IMPROVER cubes have "blend_time" instead. While I admit that I would not recommend applying this function to blended data, I think it would be more important to check that both have the same scalar coords, rather than specifying forecast reference time and time specifically. How about height or pressure? Should these be present and should they match? Would it affect the algorithm if they did not?

"""
try:
cube = cubes.extract_cube(iris.Constraint(cube_name))
except iris.exceptions.ConstraintMismatchError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary to trap this exception? Is the error message from iris not sufficient? If the iris message is adequate, this whole method can be removed and CubeList.extract_cube can be called directly.

Comment on lines +161 to +165
# Regression equation if RH is greater than 70% and T is between 250.0K and 273.15K
aisi = 100*np.tanh(0.06*rh-4.0)*(np.tanh(0.1*(t-247.0)))
aisi[np.where(rh<70)] = 0
aisi[np.where(t<250.0)] = 0
aisi[np.where(t>273.15)] = 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend putting the actual calculations into a separate class-method. Then the numbers can be tested in isolation. Also, I think the equation isn't Ruff-standard. Try installing and running the pre-commit hooks described in README.md.

)
from improver.utilities.cube_checker import spatial_coords_match

class IcingSeverityMultivariateRegression_USAF2024(PostProcessingPlugin):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend against including the source and year in the class name. Github tracks the authors already.

Suggested change
class IcingSeverityMultivariateRegression_USAF2024(PostProcessingPlugin):
class IcingSeverityMultivariateRegression(PostProcessingPlugin):

@@ -0,0 +1,71 @@
#!/usr/bin/env python
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend removing usaf2024 from this file name too.

assert result.xml().splitlines(keepends=True) == expected_cube.xml().splitlines(
keepends=True
)
assert np.allclose(result.data, expected_cube.data)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What tolerance is sensible here? We usually specify something to avoid false-positive errors when underlying packages change the behaviour slightly.

Suggested change
assert np.allclose(result.data, expected_cube.data)
assert np.allclose(result.data, expected_cube.data, rtol=1e-4)

@alexproschitsky
Copy link
Author

@MoseleyS Thank you for taking your time to review. I will address these suggestions.

Alex.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants