-
Notifications
You must be signed in to change notification settings - Fork 99
Introduction of Icing Multivariate Regression #2187
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| #!/usr/bin/env python | ||
| # -*- coding: utf-8 -*- | ||
| # ----------------------------------------------------------------------------- | ||
| # (C) British Crown copyright. The Met Office. | ||
| # All rights reserved. | ||
| # | ||
| # Redistribution and use in source and binary forms, with or without | ||
| # modification, are permitted provided that the following conditions are met: | ||
| # | ||
| # * Redistributions of source code must retain the above copyright notice, this | ||
| # list of conditions and the following disclaimer. | ||
| # | ||
| # * Redistributions in binary form must reproduce the above copyright notice, | ||
| # this list of conditions and the following disclaimer in the documentation | ||
| # and/or other materials provided with the distribution. | ||
| # | ||
| # * Neither the name of the copyright holder nor the names of its | ||
| # contributors may be used to endorse or promote products derived from | ||
| # this software without specific prior written permission. | ||
| # | ||
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
| # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | ||
| # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | ||
| # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | ||
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | ||
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||
| # POSSIBILITY OF SUCH DAMAGE. | ||
| """Script to create aircraft icing severidy indicies from multi-parameter datasets.""" | ||
|
|
||
| from improver import cli | ||
|
|
||
|
|
||
| @cli.clizefy | ||
| @cli.with_output | ||
| def process( | ||
| *cubes: cli.inputcube, model_id_attr: str = None, | ||
| ): | ||
| """ | ||
| From the supplied following cubes: | ||
| Air Temperature (t in K), | ||
| Relative Humidity (rh in %), | ||
| calculate a Aircraft Icing Severity Index using regression equation. | ||
|
|
||
| The cubes for T and RH must match spatial and temporal coordinates. | ||
|
|
||
| Does not collapse a realization coordinate. | ||
|
|
||
| Args: | ||
| cubes (list of iris.cube.Cube): | ||
| Cubes to be processed. | ||
| model_id_attr (str): | ||
| Name of the attribute used to identify the source model for | ||
| blending. | ||
|
|
||
| Returns: | ||
| iris.cube.Cube: | ||
| Cube of Aircraft Icing Severity Index | ||
| """ | ||
| from iris.cube import CubeList | ||
|
|
||
| from improver.icing import IcingSeverityMultivariateRegression_USAF2024 | ||
|
|
||
| result = IcingSeverityMultivariateRegression_USAF2024()( | ||
| CubeList(cubes), model_id_attr=model_id_attr | ||
| ) | ||
|
|
||
| return result | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,179 @@ | ||||||||||||||||||||||||
| # -*- coding: utf-8 -*- | ||||||||||||||||||||||||
| # ----------------------------------------------------------------------------- | ||||||||||||||||||||||||
| # (C) British Crown copyright. The Met Office. | ||||||||||||||||||||||||
| # All rights reserved. | ||||||||||||||||||||||||
| # | ||||||||||||||||||||||||
| # Redistribution and use in source and binary forms, with or without | ||||||||||||||||||||||||
| # modification, are permitted provided that the following conditions are met: | ||||||||||||||||||||||||
| # | ||||||||||||||||||||||||
| # * Redistributions of source code must retain the above copyright notice, this | ||||||||||||||||||||||||
| # list of conditions and the following disclaimer. | ||||||||||||||||||||||||
| # | ||||||||||||||||||||||||
| # * Redistributions in binary form must reproduce the above copyright notice, | ||||||||||||||||||||||||
| # this list of conditions and the following disclaimer in the documentation | ||||||||||||||||||||||||
| # and/or other materials provided with the distribution. | ||||||||||||||||||||||||
| # | ||||||||||||||||||||||||
| # * Neither the name of the copyright holder nor the names of its | ||||||||||||||||||||||||
| # contributors may be used to endorse or promote products derived from | ||||||||||||||||||||||||
| # this software without specific prior written permission. | ||||||||||||||||||||||||
| # | ||||||||||||||||||||||||
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||||||||||||||||||||||||
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||||||||||||||||||||||||
| # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | ||||||||||||||||||||||||
| # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE | ||||||||||||||||||||||||
| # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | ||||||||||||||||||||||||
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | ||||||||||||||||||||||||
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | ||||||||||||||||||||||||
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | ||||||||||||||||||||||||
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | ||||||||||||||||||||||||
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | ||||||||||||||||||||||||
| # POSSIBILITY OF SUCH DAMAGE. | ||||||||||||||||||||||||
| """Module containing aviation icing classes.""" | ||||||||||||||||||||||||
| from datetime import timedelta | ||||||||||||||||||||||||
| from typing import Tuple | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import iris | ||||||||||||||||||||||||
| import numpy as np | ||||||||||||||||||||||||
| from iris.cube import Cube, CubeList | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| from improver import PostProcessingPlugin | ||||||||||||||||||||||||
| from improver.metadata.constants import FLOAT_DTYPE | ||||||||||||||||||||||||
| from improver.metadata.utilities import ( | ||||||||||||||||||||||||
| create_new_diagnostic_cube, | ||||||||||||||||||||||||
| generate_mandatory_attributes, | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
| from improver.utilities.cube_checker import spatial_coords_match | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| class IcingSeverityMultivariateRegression_USAF2024(PostProcessingPlugin): | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| The algorithm outputs the unitless aircraft icing severity index. | ||||||||||||||||||||||||
| This index can be converted directly to categorical icing severity level | ||||||||||||||||||||||||
| using the category definitions below. Alternatively, the probability of | ||||||||||||||||||||||||
| reaching or exceeding these categorical icing severity levels can be | ||||||||||||||||||||||||
| calculated in a downstream thresholding operation. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Inputs: | ||||||||||||||||||||||||
| Temperature (T) in units of K | ||||||||||||||||||||||||
| Relative Humidity (RH) in units of % | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Outputs: | ||||||||||||||||||||||||
| Aircraft icing severity index (AISI) unitless | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Description of the algorithm: | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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" | ||||||||||||||||||||||||
|
Comment on lines
+64
to
+71
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a published paper that we can refer to here? |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| @staticmethod | ||||||||||||||||||||||||
| def _extract_input(cubes: CubeList, cube_name: str) -> Cube: | ||||||||||||||||||||||||
| """Extract the relevant cube based on the cube name. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||
| cubes: Cubes from which to extract required input. | ||||||||||||||||||||||||
| cube_name: Name of cube to extract. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||
| The extracted cube. | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||
| cube = cubes.extract_cube(iris.Constraint(cube_name)) | ||||||||||||||||||||||||
| except iris.exceptions.ConstraintMismatchError: | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||
| raise ValueError(f"No cube named {cube_name} found in {cubes}") | ||||||||||||||||||||||||
| return cube | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def _get_inputs(self, cubes: CubeList) -> Tuple[Cube, Cube]: | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| Separates T and RH cubes and checks that the following match: | ||||||||||||||||||||||||
| forecast_reference_time, spatial coords, and time. | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| output_cubes = iris.cube.CubeList() | ||||||||||||||||||||||||
| input_names = { | ||||||||||||||||||||||||
| "air_temperature": ["K"], | ||||||||||||||||||||||||
| "relative_humidity": ["%"], | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| for input_name, units in input_names.items(): | ||||||||||||||||||||||||
| 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})." | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
Comment on lines
+105
to
+112
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| t,rh = output_cubes | ||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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" | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
Comment on lines
+116
to
+119
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if not spatial_coords_match([t, rh]): | ||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||
| f"{t.name()} and {rh.name()} do not have the same spatial " | ||||||||||||||||||||||||
| f"coordinates" | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if t.coord("time") != rh.coord("time"): | ||||||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||||||
| f"{t.name()} and {rh.name()} do not have the same valid time" | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return t, rh | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| def process(self, cubes: CubeList, model_id_attr: str = None) -> Cube: | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| From the supplied Air Temperature and Relative Humidity cubes, calculate the Aircraft | ||||||||||||||||||||||||
| Icing Severity Index. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Args: | ||||||||||||||||||||||||
| cubes: | ||||||||||||||||||||||||
| Cubes of Air Temperature and Relative Humidity. | ||||||||||||||||||||||||
| model_id_attr: | ||||||||||||||||||||||||
| The name of the dataset attribute to be used to identify the source | ||||||||||||||||||||||||
| model when blending data from different models. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||
| Cube of Aircraft Icing Severity Index | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Raises: | ||||||||||||||||||||||||
| ValueError: | ||||||||||||||||||||||||
| If one of the cubes is not found, doesn't match the others, or has incorrect units | ||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||
| t, rh = self._get_inputs(cubes) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # Regression equations require math on cubes with incompatible units, so strip data | ||||||||||||||||||||||||
| template = t.copy() | ||||||||||||||||||||||||
| t = t.data | ||||||||||||||||||||||||
| rh = rh.data | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| # 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 | ||||||||||||||||||||||||
|
Comment on lines
+161
to
+165
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| cube = create_new_diagnostic_cube( | ||||||||||||||||||||||||
| name=( | ||||||||||||||||||||||||
| "aircraft_icing_severity_index" | ||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||
| units="1", | ||||||||||||||||||||||||
| template_cube=template, | ||||||||||||||||||||||||
| data=aisi.astype(FLOAT_DTYPE), | ||||||||||||||||||||||||
| mandatory_attributes=generate_mandatory_attributes( | ||||||||||||||||||||||||
| cubes, model_id_attr=model_id_attr | ||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return cube | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # (C) Crown Copyright, Met Office. All rights reserved. | ||
| # | ||
| # This file is part of 'IMPROVER' and is released under the BSD 3-Clause license. | ||
| # See LICENSE in the root of the repository for full licensing details. | ||
| """Tests for the lightning_usaf""" | ||
|
|
||
| import pytest | ||
|
|
||
| from . import acceptance as acc | ||
|
|
||
| #pytestmark = [pytest.mark.acc, acc.skip_if_kgo_missing] | ||
| CLI = acc.cli_name_with_dashes(__file__) | ||
| run_cli = acc.run_cli(CLI) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("with_model_attr", (True, False)) | ||
| def test_basic(tmp_path, with_model_attr): | ||
| """Test basic invocation""" | ||
| kgo_dir = acc.kgo_root() / "icing-severity-multivariate-regression-usaf2024" | ||
| kgo_path = kgo_dir / "kgo.nc" | ||
| output_path = tmp_path / "output.nc" | ||
|
|
||
| args = [ | ||
| kgo_dir / "rh.nc", | ||
| kgo_dir / "t.nc", | ||
| "--output", | ||
| f"{output_path}", | ||
| ] | ||
|
|
||
|
|
||
|
|
||
| run_cli(args) | ||
| acc.compare(output_path, kgo_path) |
There was a problem hiding this comment.
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.