From a07800c8d95e861072bf7ae26ceeca5c117311b7 Mon Sep 17 00:00:00 2001 From: Stephen Moseley Date: Thu, 4 Jun 2020 15:34:53 +0100 Subject: [PATCH] Nimrod file format (#3647) --- .travis.yml | 2 +- ..._2020-Jan-31_nimrod_format_enhancement.txt | 3 + lib/iris/fileformats/nimrod.py | 119 +- lib/iris/fileformats/nimrod_load_rules.py | 961 +++++++-- lib/iris/tests/integration/test_regridding.py | 4 +- .../results/nimrod/levels_below_ground.cml | 4 +- lib/iris/tests/results/nimrod/load_2flds.cml | 31 +- lib/iris/tests/results/nimrod/mockography.cml | 4 +- .../results/nimrod/period_of_interest.cml | 2 +- .../results/nimrod/probability_fields.cml | 1876 +++++++++++++++++ .../nimrod/u1096_ng_bmr04_precip_2km.cml | 45 + .../u1096_ng_bsr05_precip_accum60_2km.cml | 43 + .../nimrod/u1096_ng_ek00_cloud3d0060_2km.cml | 109 + .../nimrod/u1096_ng_ek00_cloud_2km.cml | 293 +++ .../nimrod/u1096_ng_ek00_convection_2km.cml | 379 ++++ .../nimrod/u1096_ng_ek00_convwind_2km.cml | 279 +++ .../nimrod/u1096_ng_ek00_frzlev_2km.cml | 319 +++ .../nimrod/u1096_ng_ek00_height_2km.cml | 42 + .../nimrod/u1096_ng_ek00_precip_2km.cml | 122 ++ .../nimrod/u1096_ng_ek00_precipaccum_2km.cml | 45 + .../nimrod/u1096_ng_ek00_preciptype_2km.cml | 360 ++++ .../nimrod/u1096_ng_ek00_pressure_2km.cml | 81 + .../nimrod/u1096_ng_ek00_radiation_2km.cml | 276 +++ .../nimrod/u1096_ng_ek00_radiationuv_2km.cml | 120 ++ .../results/nimrod/u1096_ng_ek00_refl_2km.cml | 51 + .../u1096_ng_ek00_relhumidity3d0060_2km.cml | 59 + .../nimrod/u1096_ng_ek00_relhumidity_2km.cml | 49 + .../results/nimrod/u1096_ng_ek00_snow_2km.cml | 121 ++ .../nimrod/u1096_ng_ek00_soil3d0060_2km.cml | 187 ++ .../results/nimrod/u1096_ng_ek00_soil_2km.cml | 130 ++ .../nimrod/u1096_ng_ek00_temperature_2km.cml | 189 ++ .../nimrod/u1096_ng_ek00_visibility_2km.cml | 233 ++ .../results/nimrod/u1096_ng_ek00_wind_2km.cml | 281 +++ .../nimrod/u1096_ng_ek00_winduv3d0015_2km.cml | 95 + .../nimrod/u1096_ng_ek00_winduv_2km.cml | 95 + .../results/nimrod/u1096_ng_ek01_cape_2km.cml | 240 +++ ...u1096_ng_ek07_precip0540_accum180_18km.cml | 43 + .../results/nimrod/u1096_ng_umqv_fog_2km.cml | 50 + lib/iris/tests/test_nimrod.py | 68 +- .../test_tm_meridian_scaling.py | 59 - .../nimrod_load_rules/test_units.py | 151 ++ .../nimrod_load_rules/test_vertical_coord.py | 54 +- 42 files changed, 7334 insertions(+), 340 deletions(-) create mode 100644 docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-31_nimrod_format_enhancement.txt create mode 100644 lib/iris/tests/results/nimrod/probability_fields.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_bmr04_precip_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_bsr05_precip_accum60_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_cloud3d0060_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_cloud_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_convection_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_convwind_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_frzlev_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_height_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_precip_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_precipaccum_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_preciptype_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_pressure_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_radiation_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_radiationuv_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_refl_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_relhumidity3d0060_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_relhumidity_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_snow_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_soil3d0060_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_soil_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_temperature_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_visibility_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_wind_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_winduv3d0015_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek00_winduv_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek01_cape_2km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_ek07_precip0540_accum180_18km.cml create mode 100644 lib/iris/tests/results/nimrod/u1096_ng_umqv_fog_2km.cml delete mode 100644 lib/iris/tests/unit/fileformats/nimrod_load_rules/test_tm_meridian_scaling.py create mode 100644 lib/iris/tests/unit/fileformats/nimrod_load_rules/test_units.py diff --git a/.travis.yml b/.travis.yml index a9f6bf3bfe..312914d634 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,7 @@ git: install: - > - export IRIS_TEST_DATA_REF="1696ac3a823a06b95f430670f285ee97671d2cf2"; + export IRIS_TEST_DATA_REF="fffb9b14b9cb472c5eb2ebb7fd19acb7f6414a30"; export IRIS_TEST_DATA_SUFFIX=$(echo "${IRIS_TEST_DATA_REF}" | sed "s/^v//"); # Install miniconda diff --git a/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-31_nimrod_format_enhancement.txt b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-31_nimrod_format_enhancement.txt new file mode 100644 index 0000000000..454fc3617f --- /dev/null +++ b/docs/iris/src/whatsnew/contributions_3.0.0/newfeature_2020-Jan-31_nimrod_format_enhancement.txt @@ -0,0 +1,3 @@ +* The :class:`~iris.fileformats.nimrod` provides richer meta-data translation +when loading Nimrod-format data into cubes. This covers most known operational +use-cases. diff --git a/lib/iris/fileformats/nimrod.py b/lib/iris/fileformats/nimrod.py index a2dede8ba7..2f4e32ed95 100644 --- a/lib/iris/fileformats/nimrod.py +++ b/lib/iris/fileformats/nimrod.py @@ -16,7 +16,7 @@ import iris.fileformats.nimrod_load_rules -# general header (int16) elements +# general header (int16) elements 1-31 (Fortran bytes 1-62) general_header_int16s = ( "vt_year", "vt_month", @@ -47,12 +47,12 @@ "num_model_levels", "proj_biaxial_ellipsoid", "ensemble_member", - "spare1", - "spare2", + "model_origin_id", + "averagingtype", ) -# general header (float32) elements +# general header (float32) elements 32-59 (Fortran bytes 63-174) general_header_float32s = ( "vertical_coord", "reference_vertical_coord", @@ -70,15 +70,17 @@ "true_origin_easting", "true_origin_northing", "tm_meridian_scaling", + "threshold_value_alt", + "threshold_value", ) -# data specific header (float32) elements +# data specific header (float32) elements 60-104 (Fortran bytes 175-354) data_header_float32s = ( "tl_y", "tl_x", "tr_y", - "ty_x", + "tr_x", "br_y", "br_x", "bl_y", @@ -87,46 +89,71 @@ "sat_space_count", "ducting_index", "elevation_angle", + "neighbourhood_radius", + "threshold_vicinity_radius", + "recursive_filter_alpha", + "threshold_fuzziness", + "threshold_duration_fuzziness", ) -# data specific header (int16) elements +# data specific header (char) elements 105-107 (bytes 355-410) +# units, source and title + + +# data specific header (int16) elements 108-159 (Fortran bytes 411-512) data_header_int16s = ( - "radar_num", - "radars_bitmask", - "more_radars_bitmask", - "clutter_map_num", - "calibration_type", - "bright_band_height", - "bright_band_intensity", - "bright_band_test1", - "bright_band_test2", - "infill_flag", - "stop_elevation", - "int16_vertical_coord", - "int16_reference_vertical_coord", - "int16_y_origin", - "int16_row_step", - "int16_x_origin", - "int16_column_step", - "int16_float32_mdi", - "int16_data_scaling", - "int16_data_offset", - "int16_x_offset", - "int16_y_offset", - "int16_true_origin_latitude", - "int16_true_origin_longitude", - "int16_tl_y", - "int16_tl_x", - "int16_tr_y", - "int16_ty_x", - "int16_br_y", - "int16_br_x", - "int16_bl_y", - "int16_bl_x", - "sensor_id", - "meteosat_id", - "alphas_available", + "threshold_type", + "probability_method", + "recursive_filter_iterations", + "member_count", + "probability_period_of_event", + "data_header_int16_05", + "soil_type", + "radiation_code", + "data_header_int16_08", + "data_header_int16_09", + "data_header_int16_10", + "data_header_int16_11", + "data_header_int16_12", + "data_header_int16_13", + "data_header_int16_14", + "data_header_int16_15", + "data_header_int16_16", + "data_header_int16_17", + "data_header_int16_18", + "data_header_int16_19", + "data_header_int16_20", + "data_header_int16_21", + "data_header_int16_22", + "data_header_int16_23", + "data_header_int16_24", + "data_header_int16_25", + "data_header_int16_26", + "data_header_int16_27", + "data_header_int16_28", + "data_header_int16_29", + "data_header_int16_30", + "data_header_int16_31", + "data_header_int16_32", + "data_header_int16_33", + "data_header_int16_34", + "data_header_int16_35", + "data_header_int16_36", + "data_header_int16_37", + "data_header_int16_38", + "data_header_int16_39", + "data_header_int16_40", + "data_header_int16_41", + "data_header_int16_42", + "data_header_int16_43", + "data_header_int16_44", + "data_header_int16_45", + "data_header_int16_46", + "data_header_int16_47", + "data_header_int16_48", + "data_header_int16_49", + "period_seconds", ) @@ -144,6 +171,11 @@ class NimrodField: Capable of converting itself into a :class:`~iris.cube.Cube` + References: + Met Office (2003): Met Office Rain Radar Data from the NIMROD System. + NCAS British Atmospheric Data Centre, date of citation. + http://catalogue.ceda.ac.uk/uuid/82adec1f896af6169112d09cc1174499 + """ def __init__(self, from_file=None): @@ -247,9 +279,6 @@ def _read_data(self, infile): "Expected data leading_length of %d" % num_data_bytes ) - # TODO: Deal appropriately with MDI. Can't just create masked arrays - # as cube merge converts masked arrays with no masks to ndarrays, - # thus mergable cube can split one mergable cube into two. self.data = np.fromfile(infile, dtype=numpy_dtype, count=num_data) if sys.byteorder == "little": diff --git a/lib/iris/fileformats/nimrod_load_rules.py b/lib/iris/fileformats/nimrod_load_rules.py index a78d846e3d..deb4ac862c 100644 --- a/lib/iris/fileformats/nimrod_load_rules.py +++ b/lib/iris/fileformats/nimrod_load_rules.py @@ -6,77 +6,215 @@ """Rules for converting NIMROD fields into cubes.""" import warnings +import re +import string import cf_units import cftime import numpy as np import iris +import iris.coord_systems from iris.coords import DimCoord -from iris.exceptions import TranslationError +from iris.exceptions import TranslationError, CoordinateNotFoundError __all__ = ["run"] -# Meridian scaling for British National grid. -MERIDIAN_SCALING_BNG = 0.9996012717 - NIMROD_DEFAULT = -32767.0 TIME_UNIT = cf_units.Unit( - "hours since 1970-01-01 00:00:00", calendar=cf_units.CALENDAR_GREGORIAN + "seconds since 1970-01-01 00:00:00", calendar=cf_units.CALENDAR_GREGORIAN ) -FIELD_CODES = {73: "orography"} -VERTICAL_CODES = {0: "height", 1: "altitude", 12: "levels_below_ground"} - - class TranslationWarning(Warning): pass -def name(cube, field): - """Set the cube's name from the field.""" +def is_missing(field, value): + """Returns True if value matches an "is-missing" number.""" + return any( + np.isclose(value, [field.int_mdi, field.float32_mdi, NIMROD_DEFAULT]) + ) + + +def name(cube, field, handle_metadata_errors): + """Set the cube's name from the field. + Modifies the Nimrod object title based on other meta-data in the + Nimrod field and known use cases. + Adds "mean_of" or "standard_deviation_of_" to the cube name if appropriate. + """ + title_from_field_code = { + 12: "air_pressure", + 27: "snow fraction", + 28: "snow probability", + 58: "temperature", + 61: "amount_of_precipitation", + 63: "rate_of_precipitation", + 29: "fog fraction", + 101: "snow_melting_level_above_sea_level", + 102: "rain_melted_level_above_sea_level", + 155: "Visibility", + 156: "Worst visibility in grid point", + 161: "minimum_cloud_base", + 172: "cloud_area_fraction_in_atmosphere", + 218: "snowfall", + 421: "precipitation type", + 501: "vector_wind_shear", + 508: "low_level_jet_u_component", + 509: "low_level_jet_curvature", + 514: "low_level_jet_v_component", + 804: "wind speed", + 806: "wind direction", + 817: "wind_speed_of_gust", + 821: "Probabilistic Gust Risk Analysis from Observations", + } + if handle_metadata_errors: + cube_title = title_from_field_code.get(field.field_code, field.title) + else: + cube_title = field.title + if field.ensemble_member == -98: + if not re.match("(?i)^.*(mean).*", cube_title): + cube_title = "mean_of_" + cube_title + if field.ensemble_member == -99: + if not re.match("(?i)^.*(spread).*", cube_title): + cube_title = "standard_deviation_of_" + cube_title + + cube.rename(remove_unprintable_chars(cube_title)) + - cube.rename(field.title.strip()) +def remove_unprintable_chars(input_str): + """ + Removes unprintable characters from a string and returns the result. + """ + return "".join( + c if c in string.printable else " " for c in input_str + ).strip() def units(cube, field): """ Set the cube's units from the field. + Takes into account nimrod unit strings of the form unit*?? where the data + needs to converted by dividing by ??. Also converts units we know Iris + can't handle into appropriate units Iris can handle. This is mostly when + there is an inappropriate capital letter in the unit in the Nimrod file. + Some units still can't be handled by Iris so in these cases empty strings + are added as the cube's unit. The most notable unit Iris can't handle is + oktas for cloud cover. + Unhandled units are stored in an "invalid_units" attribute instead. """ - units = field.units.strip() + unit_exception_dictionary = { + "Knts": "knots", + "knts": "knots", + "J/Kg": "J/kg", + "logical": "1", + "Code": "1", + "mask": "1", + "mb": "hPa", + "unitless": "1", + "Fraction": "1", + "index": "1", + "Beaufort": "", + "mmh2o": "kg/m2", + "n/a": "1", + } + + field_units = remove_unprintable_chars(field.units) + if field_units == "m/2-25k": + # Handle strange visibility units + cube.data = (cube.data.astype(np.float32) + 25000.0) * 2 + field_units = "m" + if "*" in field_units: + # Split into unit string and integer + unit_list = field_units.split("*") + if "^" in unit_list[1]: + # Split out magnitude + unit_sublist = unit_list[1].split("^") + cube.data = cube.data.astype(np.float32) / float( + unit_sublist[0] + ) ** float(unit_sublist[1]) + else: + cube.data = cube.data.astype(np.float32) / float(unit_list[1]) + field_units = unit_list[0] + if "ug/m3E1" in field_units: + # Split into unit string and integer + unit_list = field_units.split("E") + cube.data = cube.data.astype(np.float32) / 10.0 + field_units = unit_list[0] + if field_units == "%": + # Convert any percentages into fraction + field_units = "1" + cube.data = cube.data.astype(np.float32) / 100.0 + if field_units == "oktas": + field_units = "1" + cube.data = cube.data.astype(np.float32) / 8.0 + if field_units == "dBZ": + # cf_units doesn't recognise decibels (dBZ), but does know BZ + field_units = "BZ" + cube.data = cube.data.astype(np.float32) / 10.0 + if field_units == "g/Kg": + field_units = "kg/kg" + cube.data = cube.data.astype(np.float32) / 1000.0 + if not field_units: + if field.field_code == 8: + # Relative Humidity data are unitless, but not "unknown" + field_units = "1" + if field.field_code in [505, 515]: + # CAPE units are not always set correctly. Assume J/kg + field_units = "J/kg" + if field_units in unit_exception_dictionary.keys(): + field_units = unit_exception_dictionary[field_units] + if len(field_units) > 0 and field_units[0] == "/": + # Deal with the case where the units are of the form '/unit' eg + # '/second' in the Nimrod file. This converts to the form unit^-1 + field_units = field_units[1:] + "^-1" try: - cube.units = units + cube.units = field_units except ValueError: # Just add it as an attribute. warnings.warn( - "Unhandled units '{0}' recorded in cube attributes.".format(units) + "Unhandled units '{0}' recorded in cube attributes.".format( + field_units + ) ) - cube.attributes["invalid_units"] = units + cube.attributes["invalid_units"] = field_units def time(cube, field): - """Add a time coord to the cube.""" - valid_date = cftime.datetime( - field.vt_year, - field.vt_month, - field.vt_day, - field.vt_hour, - field.vt_minute, - field.vt_second, - ) - point = TIME_UNIT.date2num(valid_date) - - bounds = None - if field.period_minutes != field.int_mdi and field.period_minutes != 0: - # Create a bound array to handle the Period of Interest if set. - bounds = (point - (field.period_minutes / 60.0), point) + """Add a time coord to the cube based on validity time and time-window.""" + if field.vt_year <= 0: + # Some ancillary files, eg land sea mask do not + # have a validity time. + return + else: + valid_date = cftime.datetime( + field.vt_year, + field.vt_month, + field.vt_day, + field.vt_hour, + field.vt_minute, + field.vt_second, + ) + point = np.around(TIME_UNIT.date2num(valid_date)).astype(np.int64) + + period_seconds = None + if field.period_minutes == 32767: + period_seconds = field.period_seconds + elif ( + not is_missing(field, field.period_minutes) + and field.period_minutes != 0 + ): + period_seconds = field.period_minutes * 60 + if period_seconds: + bounds = np.array([point - period_seconds, point], dtype=np.int64) + else: + bounds = None time_coord = DimCoord( points=point, bounds=bounds, standard_name="time", units=TIME_UNIT @@ -87,7 +225,7 @@ def time(cube, field): def reference_time(cube, field): """Add a 'reference time' to the cube, if present in the field.""" - if field.dt_year != field.int_mdi: + if not is_missing(field, field.dt_year) and field.dt_year > 0: data_date = cftime.datetime( field.dt_year, field.dt_month, @@ -97,7 +235,7 @@ def reference_time(cube, field): ) ref_time_coord = DimCoord( - TIME_UNIT.date2num(data_date), + np.array(TIME_UNIT.date2num(data_date), dtype=np.int64), standard_name="forecast_reference_time", units=TIME_UNIT, ) @@ -105,178 +243,362 @@ def reference_time(cube, field): cube.add_aux_coord(ref_time_coord) -def experiment(cube, field): - """Add an 'experiment number' to the cube, if present in the field.""" - if field.experiment_num != field.int_mdi: - cube.add_aux_coord( - DimCoord(field.experiment_num, long_name="experiment_number") - ) - - -def proj_biaxial_ellipsoid(cube, field): +def forecast_period(cube): """ - Ellipsoid definition is currently ignored. + Add a forecast_period coord based on existing time and + forecast_reference_time coords. + + Must be run after time() and reference_time() """ - pass + try: + time_coord = cube.coord("time") + frt_coord = cube.coord("forecast_reference_time") + except CoordinateNotFoundError: + return + time_delta = time_coord.cell(0).point - frt_coord.cell(0).point + + points = np.array(time_delta.total_seconds(), dtype=np.int32) + forecast_period_unit = cf_units.Unit("second") + if cube.coord("time").has_bounds(): + time_window = time_coord.cell(0).bound + time_window = time_window[1] - time_window[0] + bounds = np.array( + [points - time_window.total_seconds(), points], dtype=np.int32 + ) + else: + bounds = None + cube.add_aux_coord( + iris.coords.AuxCoord( + points, + standard_name="forecast_period", + bounds=bounds, + units=forecast_period_unit, + ) + ) -def tm_meridian_scaling(cube, field): +def mask_cube(cube, field): """ - Deal with the scale factor on the central meridian for transverse mercator - projections if present in the field. - - Currently only caters for British National Grid. + Updates cube.data to be a masked array if appropriate. """ - if field.tm_meridian_scaling not in [field.float32_mdi, NIMROD_DEFAULT]: - if abs(field.tm_meridian_scaling - MERIDIAN_SCALING_BNG) < 1e-6: - pass # This is the expected value for British National Grid - else: - warnings.warn( - "tm_meridian_scaling not yet handled: {}" - "".format(field.tm_meridian_scaling), - TranslationWarning, - ) + dtype = cube.dtype + masked_points = None + if field.datum_type == 1: + # field.data are integers + masked_points = field.data == field.int_mdi + elif field.datum_type == 0: + # field.data are floats + masked_points = np.isclose(field.data, field.float32_mdi) + if np.any(masked_points): + cube.data = np.ma.masked_array( + cube.data, mask=masked_points, dtype=dtype + ) -def british_national_grid_x(cube, field): - """Add a British National Grid X coord to the cube.""" - x_coord = DimCoord( - np.arange(field.num_cols) * field.column_step + field.x_origin, - standard_name="projection_x_coordinate", - units="m", - coord_system=iris.coord_systems.OSGB(), - ) - cube.add_dim_coord(x_coord, 1) +def experiment(cube, field): + """Add an 'experiment number' to the cube, if present in the field.""" + if not is_missing(field, field.experiment_num): + cube.add_aux_coord( + DimCoord(field.experiment_num, long_name="experiment_number") + ) -def british_national_grid_y(cube, field): +def proj_biaxial_ellipsoid(field, handle_metadata_errors): """ - Add a British National Grid Y coord to the cube. + Returns the correct dictionary of arguments needed to define an + iris.coord_systems.GeogCS. + + Based firstly on the value given by ellipsoid, then by grid if ellipsoid is + missing, select the right pre-defined ellipsoid dictionary (Airy_1830 or + international_1924). - Currently only handles origin in the top left corner. + References: + Airy 1830: https://georepository.com/ellipsoid_7001/Airy-1830.html + International 1924: https://georepository.com/ellipsoid_7022/International-1924.html """ - if field.origin_corner == 0: # top left - y_coord = DimCoord( - np.arange(field.num_rows)[::-1] * -field.row_step + field.y_origin, - standard_name="projection_y_coordinate", - units="m", - coord_system=iris.coord_systems.OSGB(), - ) - cube.add_dim_coord(y_coord, 0) + airy_1830 = { + "semi_major_axis": 6377563.396, + "semi_minor_axis": 6356256.910, + } + international_1924 = { + "semi_major_axis": 6378388.000, + "semi_minor_axis": 6356911.946, + } + if field.proj_biaxial_ellipsoid == 0: + ellipsoid = airy_1830 + elif field.proj_biaxial_ellipsoid == 1: + ellipsoid = international_1924 + elif ( + is_missing(field, field.proj_biaxial_ellipsoid) + and handle_metadata_errors + ): + if field.horizontal_grid_type == 0: + ellipsoid = airy_1830 + elif ( + field.horizontal_grid_type == 1 or field.horizontal_grid_type == 4 + ): + ellipsoid = international_1924 + else: + raise TranslationError( + """Unsupported grid type, only NG, EuroPP + and lat/long are possible""" + ) else: raise TranslationError( - "Corner {0} not yet implemented".format(field.origin_corner) + "Ellipsoid not supported, proj_biaxial_ellipsoid:{}, " + "horizontal_grid_type:{}".format( + field.proj_biaxial_ellipsoid, field.horizontal_grid_type + ) + ) + return ellipsoid + + +def set_british_national_grid_defaults(field, handle_metadata_errors): + """Check for missing coord-system meta-data and set default values for + the Ordnance Survey GB Transverse Mercator projection. Some Radarnet + files are missing these.""" + + if handle_metadata_errors: + if is_missing(field, field.true_origin_latitude): + field.true_origin_latitude = 49.0 + if is_missing(field, field.true_origin_longitude): + field.true_origin_longitude = -2.0 + if is_missing(field, field.true_origin_easting) or np.isclose( + # Some old files misquote the value in km instead of m + field.true_origin_easting, + 400.0, + ): + field.true_origin_easting = 400000.0 + if is_missing(field, field.true_origin_northing) or np.isclose( + # Some old files misquote the value in km instead of m + field.true_origin_northing, + -100.0, + ): + field.true_origin_northing = -100000.0 + if is_missing(field, field.tm_meridian_scaling): + field.tm_meridian_scaling = 0.9996012717 + + ng_central_meridian_sf_dp = 0.9996012717 + if abs(field.tm_meridian_scaling - ng_central_meridian_sf_dp) < 1.0e-04: + # Update the National Grid scaling factor to double + # precision accuracy to improve the accuracy of + # reprojection calculations that use it. + field.tm_meridian_scaling = ng_central_meridian_sf_dp + + +def coord_system(field, handle_metadata_errors): + """Define the coordinate system for the field. + Handles Transverse Mercator, Universal Transverse Mercator and Plate Carree. + + Transverse Mercator projections will default to the British National Grid if any + parameters are missing. + """ + ellipsoid = proj_biaxial_ellipsoid(field, handle_metadata_errors) + + if field.horizontal_grid_type == 0: + # Check for missing grid meta-data and insert OSGB definitions. + # Some Radarnet files are missing these. + set_british_national_grid_defaults(field, handle_metadata_errors) + if field.horizontal_grid_type == 0 or field.horizontal_grid_type == 4: + crs_args = ( + field.true_origin_latitude, + field.true_origin_longitude, + field.true_origin_easting, + field.true_origin_northing, + field.tm_meridian_scaling, + ) + if any([is_missing(field, v) for v in crs_args]): + warnings.warn( + f"Coordinate Reference System is not completely defined. " + f"Plotting and reprojection may be impaired." + ) + coord_sys = iris.coord_systems.TransverseMercator( + *crs_args, iris.coord_systems.GeogCS(**ellipsoid), ) + elif field.horizontal_grid_type == 1: + coord_sys = iris.coord_systems.GeogCS(**ellipsoid) + else: + coord_sys = None + return coord_sys -def horizontal_grid(cube, field): - """Add X and Y coords to the cube. +def horizontal_grid(cube, field, handle_metadata_errors): + """Add X and Y coordinates to the cube. + Handles Transverse Mercator, Universal Transverse Mercator and Plate Carree. - Currently only handles British National Grid. + coordinate reference system is supplied by coord_system(field) + Must be run AFTER origin_corner() """ - # "NG" (British National Grid) - if field.horizontal_grid_type == 0: - british_national_grid_x(cube, field) - british_national_grid_y(cube, field) + crs = coord_system(field, handle_metadata_errors) + if field.horizontal_grid_type == 0 or field.horizontal_grid_type == 4: + units_name = "m" + x_coord_name = "projection_x_coordinate" + y_coord_name = "projection_y_coordinate" + elif field.horizontal_grid_type == 1: + units_name = "degrees" + x_coord_name = "longitude" + y_coord_name = "latitude" else: raise TranslationError( - "Grid type %d not yet implemented" % field.horizontal_grid_type + "Horizontal grid type {} not " + "implemented".format(field.horizontal_grid_type) ) + points = np.linspace( + field.x_origin, + field.x_origin + field.num_cols * field.column_step, + field.num_cols, + endpoint=False, + dtype=np.float32, + ) + x_coord = DimCoord( + points, standard_name=x_coord_name, units=units_name, coord_system=crs, + ) + cube.add_dim_coord(x_coord, 1) + points = np.linspace( + field.y_origin - (field.num_rows - 1) * field.row_step, + field.y_origin, + field.num_rows, + endpoint=True, + dtype=np.float32, + ) + y_coord = DimCoord( + points, standard_name=y_coord_name, units=units_name, coord_system=crs, + ) + cube.add_dim_coord(y_coord, 0) -def orography_vertical_coord(cube, field): - """Special handling of vertical coords for orography fields: Do nothing.""" - # We can find values in the vertical coord, such as 9999, - # for orography fields. Don't make a vertical coord from these. - pass - - -def height_vertical_coord(cube, field): - """Add a height coord to the cube, if present in the field.""" - if ( - field.reference_vertical_coord_type == field.int_mdi - or field.reference_vertical_coord == field.float32_mdi +def vertical_coord(cube, field): + """Add a vertical coord to the cube, with bounds, if appropriate. + Handles special numbers for "at-sea-level" (8888) and "at-ground-level" (9999).""" + # vertical_codes contains conversions from the Nimrod Documentation for the + # header entry 20 for the vertical coordinate type + # Unhandled vertical_codes values (no use case identified): + # 3: ['sigma', 'model level'], + # 4: ['eta', 'model level'], + # 5: ['radar beam number', 'unknown'], + # 7: ['potential temperature', 'unknown'], + # 8: ['equivalent potential temperature', 'unknown'], + # 9: ['wet bulb potential temperature', 'unknown'], + # 10: ['potential vorticity', 'unknown'], + # 11: ['cloud boundary', 'unknown'], + vertical_codes = { + 0: { + "standard_name": "height", + "units": "m", + "attributes": {"positive": "up"}, + }, + 1: { + "standard_name": "altitude", + "units": "m", + "attributes": {"positive": "up"}, + }, + 2: { + "standard_name": "air_pressure", + "units": "hPa", + "attributes": {"positive": "down"}, + }, + 6: {"standard_name": "air_temperature", "units": "K"}, + 12: { + "long_name": "depth_below_ground", + "units": "m", + "attributes": {"positive": "down"}, + }, + } + if all( + [ + is_missing(field, x) + for x in [ + field.vertical_coord, + field.vertical_coord_type, + field.reference_vertical_coord, + field.reference_vertical_coord_type, + ] + ] ): - height_coord = DimCoord( - field.vertical_coord, - standard_name="height", - units="m", - attributes={"positive": "up"}, - ) - cube.add_aux_coord(height_coord) - else: - raise TranslationError("Bounded vertical not yet implemented") + return - -def altitude_vertical_coord(cube, field): - """Add an altitude coord to the cube, if present in the field.""" if ( - field.reference_vertical_coord_type == field.int_mdi - or field.reference_vertical_coord == field.float32_mdi + not is_missing(field, field.reference_vertical_coord_type) + and field.reference_vertical_coord_type != field.vertical_coord_type + and not is_missing(field, field.reference_vertical_coord) ): - alti_coord = DimCoord( - field.vertical_coord, - standard_name="altitude", - units="m", - attributes={"positive": "up"}, + msg = ( + "Unmatched vertical coord types " + f"{field.vertical_coord_type} != {field.reference_vertical_coord_type}. " + f"Assuming {field.vertical_coord_type}" ) - cube.add_aux_coord(alti_coord) - else: - raise TranslationError("Bounded vertical not yet implemented") - - -def levels_below_ground_vertical_coord(cube, field): - """Add a levels_below_ground coord to the cube, if present in the field.""" + warnings.warn(msg) + + coord_point = field.vertical_coord + if coord_point == 8888.0: + if "sea_level" not in cube.name(): + cube.rename(f"{cube.name()}_at_mean_sea_level") + coord_point = 0.0 + if np.isclose(field.reference_vertical_coord, 8888.0) or is_missing( + field, field.reference_vertical_coord + ): + # This describes a surface field. No changes needed. + return + + coord_args = vertical_codes.get(field.vertical_coord_type, None) + if np.isclose(coord_point, 9999.0): + if np.isclose(field.reference_vertical_coord, 9999.0) or is_missing( + field, field.reference_vertical_coord + ): + # This describes a surface field. No changes needed. + return + # A bounded vertical coord starting from the surface + coord_point = 0.0 + coord_args = vertical_codes.get( + field.reference_vertical_coord_type, None + ) + coord_point = np.array(coord_point, dtype=np.float32) if ( - field.reference_vertical_coord_type == field.int_mdi - or field.reference_vertical_coord == field.float32_mdi + field.reference_vertical_coord >= 0.0 + and field.reference_vertical_coord != coord_point ): - lev_coord = DimCoord( - field.vertical_coord, - long_name="levels_below_ground", - units="1", - attributes={"positive": "down"}, + bounds = np.array( + [coord_point, field.reference_vertical_coord], dtype=np.float32 ) - cube.add_aux_coord(lev_coord) else: - raise TranslationError("Bounded vertical not yet implemented") - - -def vertical_coord(cube, field): - """Add a vertical coord to the cube.""" - v_type = field.vertical_coord_type + bounds = None - if v_type not in [field.int_mdi, NIMROD_DEFAULT]: - if FIELD_CODES.get(field.field_code, None) == "orography": - orography_vertical_coord(cube, field) - else: - vertical_code_name = VERTICAL_CODES.get(v_type, None) - if vertical_code_name == "height": - height_vertical_coord(cube, field) - elif vertical_code_name == "altitude": - altitude_vertical_coord(cube, field) - elif vertical_code_name == "levels_below_ground": - levels_below_ground_vertical_coord(cube, field) - else: - warnings.warn( - "Vertical coord {!r} not yet handled" "".format(v_type), - TranslationWarning, - ) + if coord_args: + new_coord = iris.coords.AuxCoord( + coord_point, bounds=bounds, **coord_args + ) + # Add coordinate to cube + cube.add_aux_coord(new_coord) + return + + warnings.warn( + "Vertical coord {!r} not yet handled" + "".format(field.vertical_coord_type), + TranslationWarning, + ) def ensemble_member(cube, field): """Add an 'ensemble member' coord to the cube, if present in the field.""" - ensemble_member = getattr(field, "ensemble_member") - if ensemble_member != field.int_mdi: - cube.add_aux_coord(DimCoord(ensemble_member, "realization")) + ensemble_member_value = field.ensemble_member + + if ensemble_member_value in [-98, -99]: + # ignore these special values handled in name() + return + if not is_missing(field, ensemble_member_value): + cube.add_aux_coord( + DimCoord( + np.array(ensemble_member_value, dtype=np.int32), "realization" + ) + ) def origin_corner(cube, field): - """Ensure the data matches the order of the coords we've made.""" + """Ensure the data matches the order of the coordinates we've made.""" if field.origin_corner == 0: # top left cube.data = cube.data[::-1, :].copy() else: @@ -289,12 +611,15 @@ def origin_corner(cube, field): def attributes(cube, field): """Add attributes to the cube.""" - def add_attr(name): + def add_attr(item): """Add an attribute to the cube.""" - if hasattr(field, name): - value = getattr(field, name) - if value not in [field.int_mdi, field.float32_mdi]: - cube.attributes[name] = value + if hasattr(field, item): + value = getattr(field, item) + if is_missing(field, value): + return + if "radius" in item: + value = f"{value} km" + cube.attributes[item] = value add_attr("nimrod_version") add_attr("field_code") @@ -303,25 +628,278 @@ def add_attr(name): add_attr("sat_space_count") add_attr("ducting_index") add_attr("elevation_angle") - add_attr("radar_num") - add_attr("radars_bitmask") - add_attr("more_radars_bitmask") - add_attr("clutter_map_num") - add_attr("calibration_type") - add_attr("bright_band_height") - add_attr("bright_band_intensity") - add_attr("bright_band_test1") - add_attr("bright_band_test2") - add_attr("infill_flag") - add_attr("stop_elevation") - add_attr("sensor_id") - add_attr("meteosat_id") - add_attr("alphas_available") - - cube.attributes["source"] = field.source.strip() - - -def run(field): + cube_source = field.source.strip() + + # Handle a few known meta-data errors: + if field.field_code == 501: + cube_source = "" + if field.field_code == 821: + cube_source = "Nimrod pwind routine" + if field.source.strip() == "pwind": + cube_source = "Nimrod pwind routine" + for key in [ + "neighbourhood_radius", + "recursive_filter_iterations", + "recursive_filter_alpha", + "threshold_vicinity_radius", + "probability_period_of_event", + ]: + add_attr(key) + + # Remove member number from cube_source. This commonly takes the form ek04 where ek + # indicates the model and 04 is the realization number. As the number is represented + # by a realization coord, stripping it from here allows cubes to be merged. + match = re.match( + r"^(?P\w\w)(?P\d\d)$", cube_source + ) + try: + r_coord = cube.coord("realization") + except CoordinateNotFoundError: + r_coord = None + if match is not None: + if r_coord: + if int(match["realization"]) == r_coord.points[0]: + cube_source = match["model_code"] + cube.attributes["source"] = cube_source + cube.attributes["title"] = "Unknown" + cube.attributes["institution"] = "Met Office" + + +def known_threshold_coord(field): + """ + Supplies known threshold coord meta-data for known use cases. + threshold_value_alt exists because some meta-data are mis-assigned in the Nimrod data. + """ + coord_keys = {} + if ( + field.field_code == 161 + and field.threshold_value >= 0.0 + and "pc" not in field.title + ): + coord_keys = {"var_name": "threshold"} + if field.threshold_value_alt > 8.0: + coord_keys["standard_name"] = "height" + coord_keys["units"] = "metres" + else: + coord_keys["standard_name"] = "cloud_area_fraction" + coord_keys["units"] = "oktas" + elif field.field_code == 29 and field.threshold_value >= 0.0: + if is_missing(field, field.threshold_type): + coord_keys = { + "standard_name": "visibility_in_air", + "var_name": "threshold", + "units": "metres", + } + else: + coord_keys = { + "long_name": "fog_fraction", + "var_name": "threshold", + "units": "1", + } + elif ( + field.field_code == 422 + and field.threshold_value >= 0.0 + and is_missing(field, field.threshold_type) + ): + coord_keys = {"long_name": "radius_of_max", "units": "km"} + elif field.field_code == 821: + coord_keys = { + "standard_name": "wind_speed_of_gust", + "var_name": "threshold", + "units": "m/s", + } + return coord_keys + + +def probability_coord(cube, field, handle_metadata_errors): + """ + Adds a coord relating to probability meta-data from the header to the + cube if appropriate. + Must be run after the name method. + """ + probtype_lookup = { + 1: { + "var_name": "threshold", + "attributes": {"relative_to_threshold": "above"}, + }, + 2: { + "var_name": "threshold", + "attributes": {"relative_to_threshold": "below"}, + }, + 3: {"long_name": "percentile", "units": "1"}, + 4: { + "var_name": "threshold", + "attributes": {"relative_to_threshold": "equal"}, + }, + } + probmethod_lookup = { + 1: "AOT (Any One Time)", + 2: "ST (Some Time)", + 4: "AT (All Time)", + 8: "AOL (Any One Location)", + 16: "SW (Some Where)", + } + # The units for the threshold coord are not defined in the Nimrod meta-data. + # These represent the known use-cases. + units_from_field_code = { + 817: "m s^-1", + 804: "knots", + 422: "min^-1", + 421: "1", + 218: "cm", + 172: "oktas", + 155: "m", + 101: "m", + 63: "mm hr^-1", + 61: "mm", + 58: "Celsius", + 12: "mb", + 8: "1", + } + is_probability_field = False + coord_keys = probtype_lookup.get(field.threshold_type, {}) + if coord_keys.get("var_name") == "threshold": + is_probability_field = True + if handle_metadata_errors: + coord_keys.update(known_threshold_coord(field)) + if not coord_keys.get("units"): + coord_keys["units"] = units_from_field_code.get( + field.field_code, "unknown" + ) + coord_val = None + # coord_val could come from the threshold_value or threshold_value_alt: + if field.threshold_value_alt > -32766.0: + coord_val = field.threshold_value_alt + elif field.threshold_value > -32766.0: + coord_val = field.threshold_value + + # coord_val could also be encoded in the cube name if we have a percentile + # (this overrides the threshold_value which may be unrelated in the case of + # the 50th %ile of 3okta cloud cover) + if ( + coord_keys.get("long_name") == "percentile" + and cube.name().find("pc") > 0 + and handle_metadata_errors + ): + try: + coord_val = [ + int(x.strip("pc")) + for x in cube.name().split(" ") + if x.find("pc") > 0 + ][0] + except IndexError: + pass + + # If we found a coord_val, build the coord (with bounds) and add to cube) + if coord_val is not None: + if not is_missing(field, field.threshold_fuzziness): + bounds = [ + coord_val * field.threshold_fuzziness, + coord_val * (2.0 - field.threshold_fuzziness), + ] + bounds = np.array(bounds, dtype=np.float32) + # TODO: Enable filtering of zero-length bounds once Iris doesn't strip bounds + # in merge_cube + # if np.isclose(bounds[0], bounds[1]): + # bounds = None + else: + bounds = None + if coord_keys.get("units", None) == "oktas": + coord_keys["units"] = "1" + coord_val /= 8.0 + if bounds is not None: + bounds /= 8.0 + if coord_keys["units"] == "unknown": + coord_name = coord_keys.get( + "standard_name", + coord_keys.get("long_name", coord_keys.get("var_name", None)), + ) + warnings.warn( + f"No default units for {coord_name} coord of {cube.name()}. " + "Meta-data may be incomplete." + ) + new_coord = iris.coords.AuxCoord( + np.array(coord_val, dtype=np.float32), bounds=bounds, **coord_keys + ) + cube.add_aux_coord(new_coord) + if field.threshold_type == 3: + pass + else: + if is_probability_field: + # Some probability fields have inappropriate units (those of the threshold) + if "%" in cube.name(): + # If the cube name has % in it, convert to fraction + cube.rename(cube.name().replace("%", "fraction")) + cube.data = cube.data.astype(np.float32) / 100.0 + cube.units = "1" + cube.rename(f"probability_of_{cube.name()}") + + if field.probability_method > 0: + probability_attributes = [] + num = field.probability_method + for key in sorted(probmethod_lookup.keys(), reverse=True): + if num >= key: + probability_attributes.append(probmethod_lookup[key]) + num = num - key + cube.attributes["Probability methods"] = probability_attributes + return + + +def soil_type_coord(cube, field): + """Add soil type as a coord if appropriate""" + soil_type_codes = { + 1: "broadleaf_tree", + 2: "needleleaf_tree", + 3: "c3_grass", + 4: "c4_grass", + 5: "crop", + 6: "shrub", + 7: "urban", + 8: "water", + 9: "soil", + 10: "ice", + 601: "urban_canyon", + 602: "urban_roof", + } + soil_name = soil_type_codes.get(field.soil_type, None) + if soil_name: + cube.add_aux_coord( + iris.coords.AuxCoord( + soil_name, standard_name="soil_type", units=None + ) + ) + + +def time_averaging(cube, field): + """Decode the averagingtype code - similar to the PP LBPROC code.""" + time_averaging_codes = { + 8192: "maximum in period", + 4096: "minimum in period", + 2048: "unknown(2048)", + 1024: "unknown(1024)", + 512: "time lagged", + 256: "extrapolation", + 128: "accumulation or average", + 64: "from UM 150m", + 32: "scaled to UM resolution", + 16: "averaged over multiple surface types", + 8: "only observations used", + 4: "smoothed", + 2: "cold bias applied", + 1: "warm bias applied", + } + + num = field.averagingtype + averaging_attributes = [] + for key in sorted(time_averaging_codes.keys(), reverse=True): + if num >= key: + averaging_attributes.append(time_averaging_codes[key]) + num = num - key + if averaging_attributes: + cube.attributes["processing"] = averaging_attributes + + +def run(field, handle_metadata_errors=True): """ Convert a NIMROD field to an Iris cube. @@ -329,6 +907,9 @@ def run(field): * field - a :class:`~iris.fileformats.nimrod.NimrodField` + * handle_metadata_errors - Set to False to omit handling of known meta-data deficiencies + in Nimrod-format data + Returns: * A new :class:`~iris.cube.Cube`, created from the NimrodField. @@ -336,27 +917,29 @@ def run(field): """ cube = iris.cube.Cube(field.data) - name(cube, field) + name(cube, field, handle_metadata_errors) + mask_cube(cube, field) units(cube, field) # time time(cube, field) reference_time(cube, field) + forecast_period(cube) experiment(cube, field) # horizontal grid - proj_biaxial_ellipsoid(cube, field) - tm_meridian_scaling(cube, field) - horizontal_grid(cube, field) + origin_corner(cube, field) + horizontal_grid(cube, field, handle_metadata_errors) # vertical vertical_coord(cube, field) # add other stuff, if present + soil_type_coord(cube, field) + probability_coord(cube, field, handle_metadata_errors) ensemble_member(cube, field) + time_averaging(cube, field) attributes(cube, field) - origin_corner(cube, field) - return cube diff --git a/lib/iris/tests/integration/test_regridding.py b/lib/iris/tests/integration/test_regridding.py index f16b7f4ab5..4375e3b45a 100644 --- a/lib/iris/tests/integration/test_regridding.py +++ b/lib/iris/tests/integration/test_regridding.py @@ -59,11 +59,11 @@ def _regrid(self, method): def test_linear(self): res = self._regrid("linear") - self.assertArrayShapeStats(res, (73, 96), -16100.351951, 5603.850769) + self.assertArrayShapeStats(res, (73, 96), 17799.296120, 11207.701323) def test_nearest(self): res = self._regrid("nearest") - self.assertArrayShapeStats(res, (73, 96), -16095.965585, 5612.657155) + self.assertArrayShapeStats(res, (73, 96), 17808.068828, 11225.314310) @tests.skip_data diff --git a/lib/iris/tests/results/nimrod/levels_below_ground.cml b/lib/iris/tests/results/nimrod/levels_below_ground.cml index 22d2168578..c7a5bb1713 100644 --- a/lib/iris/tests/results/nimrod/levels_below_ground.cml +++ b/lib/iris/tests/results/nimrod/levels_below_ground.cml @@ -3,11 +3,11 @@ - + - + diff --git a/lib/iris/tests/results/nimrod/load_2flds.cml b/lib/iris/tests/results/nimrod/load_2flds.cml index d57761e235..b068657d40 100644 --- a/lib/iris/tests/results/nimrod/load_2flds.cml +++ b/lib/iris/tests/results/nimrod/load_2flds.cml @@ -1,41 +1,38 @@ - + - + + - - + + - - - - - + - - + + - - + + - + - + diff --git a/lib/iris/tests/results/nimrod/mockography.cml b/lib/iris/tests/results/nimrod/mockography.cml index 6e63d9fa87..585ef2b952 100644 --- a/lib/iris/tests/results/nimrod/mockography.cml +++ b/lib/iris/tests/results/nimrod/mockography.cml @@ -1,9 +1,11 @@ - + + + diff --git a/lib/iris/tests/results/nimrod/period_of_interest.cml b/lib/iris/tests/results/nimrod/period_of_interest.cml index ef0f33649e..258e5bcbbc 100644 --- a/lib/iris/tests/results/nimrod/period_of_interest.cml +++ b/lib/iris/tests/results/nimrod/period_of_interest.cml @@ -3,7 +3,7 @@ - + diff --git a/lib/iris/tests/results/nimrod/probability_fields.cml b/lib/iris/tests/results/nimrod/probability_fields.cml new file mode 100644 index 0000000000..7add3e75a4 --- /dev/null +++ b/lib/iris/tests/results/nimrod/probability_fields.cml @@ -0,0 +1,1876 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_bmr04_precip_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_bmr04_precip_2km.cml new file mode 100644 index 0000000000..31518dd321 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_bmr04_precip_2km.cml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_bsr05_precip_accum60_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_bsr05_precip_accum60_2km.cml new file mode 100644 index 0000000000..80cb1834c0 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_bsr05_precip_accum60_2km.cml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_cloud3d0060_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_cloud3d0060_2km.cml new file mode 100644 index 0000000000..68ec95555c --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_cloud3d0060_2km.cml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_cloud_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_cloud_2km.cml new file mode 100644 index 0000000000..c6bc6f0419 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_cloud_2km.cml @@ -0,0 +1,293 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_convection_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_convection_2km.cml new file mode 100644 index 0000000000..e6c99f9e50 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_convection_2km.cml @@ -0,0 +1,379 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_convwind_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_convwind_2km.cml new file mode 100644 index 0000000000..2f52a93277 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_convwind_2km.cml @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_frzlev_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_frzlev_2km.cml new file mode 100644 index 0000000000..b2b47715a2 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_frzlev_2km.cml @@ -0,0 +1,319 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_height_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_height_2km.cml new file mode 100644 index 0000000000..4fb1371250 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_height_2km.cml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_precip_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_precip_2km.cml new file mode 100644 index 0000000000..59776b5b74 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_precip_2km.cml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_precipaccum_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_precipaccum_2km.cml new file mode 100644 index 0000000000..0fa98e3bb6 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_precipaccum_2km.cml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_preciptype_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_preciptype_2km.cml new file mode 100644 index 0000000000..3fdf646e70 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_preciptype_2km.cml @@ -0,0 +1,360 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_pressure_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_pressure_2km.cml new file mode 100644 index 0000000000..edb0862676 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_pressure_2km.cml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_radiation_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_radiation_2km.cml new file mode 100644 index 0000000000..38f076f232 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_radiation_2km.cml @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_radiationuv_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_radiationuv_2km.cml new file mode 100644 index 0000000000..35bed38591 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_radiationuv_2km.cml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_refl_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_refl_2km.cml new file mode 100644 index 0000000000..4411ff9dd5 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_refl_2km.cml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_relhumidity3d0060_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_relhumidity3d0060_2km.cml new file mode 100644 index 0000000000..8759dac5c7 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_relhumidity3d0060_2km.cml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_relhumidity_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_relhumidity_2km.cml new file mode 100644 index 0000000000..9b7e7582d0 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_relhumidity_2km.cml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_snow_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_snow_2km.cml new file mode 100644 index 0000000000..ce549ab3cd --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_snow_2km.cml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_soil3d0060_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_soil3d0060_2km.cml new file mode 100644 index 0000000000..9385bfc9ae --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_soil3d0060_2km.cml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_soil_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_soil_2km.cml new file mode 100644 index 0000000000..a76971a1ed --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_soil_2km.cml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_temperature_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_temperature_2km.cml new file mode 100644 index 0000000000..09677ff57a --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_temperature_2km.cml @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_visibility_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_visibility_2km.cml new file mode 100644 index 0000000000..8a0f50700c --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_visibility_2km.cml @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_wind_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_wind_2km.cml new file mode 100644 index 0000000000..df2054e8af --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_wind_2km.cml @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_winduv3d0015_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_winduv3d0015_2km.cml new file mode 100644 index 0000000000..331ff59c74 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_winduv3d0015_2km.cml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek00_winduv_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek00_winduv_2km.cml new file mode 100644 index 0000000000..aa14346e2f --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek00_winduv_2km.cml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek01_cape_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek01_cape_2km.cml new file mode 100644 index 0000000000..1756ac0205 --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek01_cape_2km.cml @@ -0,0 +1,240 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_ek07_precip0540_accum180_18km.cml b/lib/iris/tests/results/nimrod/u1096_ng_ek07_precip0540_accum180_18km.cml new file mode 100644 index 0000000000..f4710dd36d --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_ek07_precip0540_accum180_18km.cml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/results/nimrod/u1096_ng_umqv_fog_2km.cml b/lib/iris/tests/results/nimrod/u1096_ng_umqv_fog_2km.cml new file mode 100644 index 0000000000..57756ccc1d --- /dev/null +++ b/lib/iris/tests/results/nimrod/u1096_ng_umqv_fog_2km.cml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/test_nimrod.py b/lib/iris/tests/test_nimrod.py index 14295f4079..bcb9255dca 100644 --- a/lib/iris/tests/test_nimrod.py +++ b/lib/iris/tests/test_nimrod.py @@ -12,6 +12,7 @@ import iris import iris.fileformats.nimrod_load_rules as nimrod_load_rules +from iris.exceptions import TranslationError def mock_nimrod_field(): @@ -37,6 +38,63 @@ def test_multi_field_load(self): ) self.assertCML(cube, ("nimrod", "load_2flds.cml")) + @tests.skip_data + def test_huge_field_load(self): + # load a wide range of cubes with all meta-data variations + for datafile in { + "u1096_ng_ek07_precip0540_accum180_18km", + "u1096_ng_ek00_cloud3d0060_2km", + "u1096_ng_ek00_cloud_2km", + "u1096_ng_ek00_convection_2km", + "u1096_ng_ek00_convwind_2km", + "u1096_ng_ek00_frzlev_2km", + "u1096_ng_ek00_height_2km", + "u1096_ng_ek00_precip_2km", + "u1096_ng_ek00_precipaccum_2km", + "u1096_ng_ek00_preciptype_2km", + "u1096_ng_ek00_pressure_2km", + "u1096_ng_ek00_radiation_2km", + "u1096_ng_ek00_radiationuv_2km", + "u1096_ng_ek00_refl_2km", + "u1096_ng_ek00_relhumidity3d0060_2km", + "u1096_ng_ek00_relhumidity_2km", + "u1096_ng_ek00_snow_2km", + "u1096_ng_ek00_soil3d0060_2km", + "u1096_ng_ek00_soil_2km", + "u1096_ng_ek00_temperature_2km", + "u1096_ng_ek00_visibility_2km", + "u1096_ng_ek00_wind_2km", + "u1096_ng_ek00_winduv3d0015_2km", + "u1096_ng_ek00_winduv_2km", + "u1096_ng_ek01_cape_2km", + "u1096_ng_umqv_fog_2km", + "u1096_ng_bmr04_precip_2km", + "u1096_ng_bsr05_precip_accum60_2km", + "probability_fields", + }: + cube = iris.load( + tests.get_data_path(("NIMROD", "uk2km", "cutouts", datafile,)) + ) + self.assertCML(cube, ("nimrod", f"{datafile}.cml")) + + @tests.skip_data + def test_load_kwarg(self): + """Tests that the handle_metadata_errors kwarg is effective by setting it to + False with a file with known incomplete meta-data (missing ellipsoid).""" + datafile = "u1096_ng_ek00_pressure_2km" + with self.assertRaisesRegex( + TranslationError, + "Ellipsoid not supported, proj_biaxial_ellipsoid:-32767, horizontal_grid_type:0", + ): + with open( + tests.get_data_path(("NIMROD", "uk2km", "cutouts", datafile,)), + "rb", + ) as infile: + iris.fileformats.nimrod_load_rules.run( + iris.fileformats.nimrod.NimrodField(infile), + handle_metadata_errors=False, + ) + def test_orography(self): # Mock an orography field we've seen. field = mock_nimrod_field() @@ -47,16 +105,19 @@ def test_orography(self): field.proj_biaxial_ellipsoid = 0 field.tm_meridian_scaling = 0.999601 field.field_code = 73 + field.reference_vertical_coord_type = field.int_mdi # Not bounded + field.reference_vertical_coord = field.int_mdi field.vertical_coord_type = 1 + field.vertical_coord = 8888 + field.ensemble_member = field.int_mdi + field.threshold_value = field.int_mdi field.title = "(MOCK) 2km mean orography" field.units = "metres" field.source = "GLOBE DTM" - nimrod_load_rules.name(cube, field) + nimrod_load_rules.name(cube, field, handle_metadata_errors=True) nimrod_load_rules.units(cube, field) nimrod_load_rules.reference_time(cube, field) - nimrod_load_rules.proj_biaxial_ellipsoid(cube, field) - nimrod_load_rules.tm_meridian_scaling(cube, field) nimrod_load_rules.vertical_coord(cube, field) nimrod_load_rules.attributes(cube, field) @@ -69,6 +130,7 @@ def test_levels_below_ground(self): field.field_code = -1 # Not orography field.reference_vertical_coord_type = field.int_mdi # Not bounded + field.reference_vertical_coord = field.int_mdi field.vertical_coord_type = 12 field.vertical_coord = 42 nimrod_load_rules.vertical_coord(cube, field) diff --git a/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_tm_meridian_scaling.py b/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_tm_meridian_scaling.py deleted file mode 100644 index 39f08edc7d..0000000000 --- a/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_tm_meridian_scaling.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright Iris contributors -# -# This file is part of Iris and is released under the LGPL license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -""" -Unit tests for the `iris.fileformats.nimrod_load_rules.tm_meridian_scaling` -function. - -""" - -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests - -from unittest import mock - -from iris.fileformats.nimrod_load_rules import ( - tm_meridian_scaling, - NIMROD_DEFAULT, - MERIDIAN_SCALING_BNG, -) -from iris.fileformats.nimrod import NimrodField - - -class Test(tests.IrisTest): - def setUp(self): - self.field = mock.Mock( - tm_meridian_scaling=NIMROD_DEFAULT, - spec=NimrodField, - float32_mdi=-123, - ) - self.cube = mock.Mock() - - def _call_tm_meridian_scaling(self, scaling_value): - self.field.tm_meridian_scaling = scaling_value - tm_meridian_scaling(self.cube, self.field) - - def test_unhandled(self): - with mock.patch("warnings.warn") as warn: - self._call_tm_meridian_scaling(1) - self.assertEqual(warn.call_count, 1) - - @tests.no_warnings - def test_british_national_grid(self): - # A value is not returned in this rule currently. - self.assertEqual( - None, self._call_tm_meridian_scaling(MERIDIAN_SCALING_BNG) - ) - - def test_null(self): - with mock.patch("warnings.warn") as warn: - self._call_tm_meridian_scaling(NIMROD_DEFAULT) - self._call_tm_meridian_scaling(self.field.float32_mdi) - self.assertEqual(warn.call_count, 0) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_units.py b/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_units.py new file mode 100644 index 0000000000..cf6dd8cdb3 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_units.py @@ -0,0 +1,151 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Unit tests for the `iris.fileformats.nimrod_load_rules.units` function. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from unittest import mock +import numpy as np + +from iris.cube import Cube +from iris.fileformats.nimrod_load_rules import ( + units, + NIMROD_DEFAULT, +) +from iris.fileformats.nimrod import NimrodField + + +class Test(tests.IrisTest): + NIMROD_LOCATION = "iris.fileformats.nimrod_load_rules" + + def setUp(self): + self.field = mock.Mock( + units="", + int_mdi=-32767, + float32_mdi=NIMROD_DEFAULT, + spec=NimrodField, + ) + self.cube = Cube(np.ones((3, 3), dtype=np.float32)) + + def _call_units( + self, data=None, units_str=None, + ): + if data is not None: + self.cube.data = data + if units_str: + self.field.units = units_str + units(self.cube, self.field) + + def test_null(self): + with mock.patch("warnings.warn") as warn: + self._call_units(units_str="m") + self.assertEqual(warn.call_count, 0) + self.assertEqual(self.cube.units, "m") + self.assertArrayAlmostEqual( + self.cube.data, np.ones_like(self.cube.data) + ) + + def test_times32(self): + with mock.patch("warnings.warn") as warn: + self._call_units( + data=np.ones_like(self.cube.data) * 32, units_str="mm/hr*32" + ) + self.assertEqual(warn.call_count, 0) + self.assertEqual(self.cube.units, "mm/hr") + self.assertArrayAlmostEqual( + self.cube.data, np.ones_like(self.cube.data) + ) + self.assertEqual(self.cube.data.dtype, np.float32) + + def test_visibility_units(self): + with mock.patch("warnings.warn") as warn: + self._call_units( + data=((np.ones_like(self.cube.data) / 2) - 25000), + units_str="m/2-25k", + ) + self.assertEqual(warn.call_count, 0) + self.assertEqual(self.cube.units, "m") + self.assertArrayAlmostEqual( + self.cube.data, np.ones_like(self.cube.data) + ) + self.assertEqual(self.cube.data.dtype, np.float32) + + def test_power_in_units(self): + with mock.patch("warnings.warn") as warn: + self._call_units( + data=np.ones_like(self.cube.data) * 1000, units_str="mm*10^3" + ) + self.assertEqual(warn.call_count, 0) + self.assertEqual(self.cube.units, "mm") + self.assertArrayAlmostEqual( + self.cube.data, np.ones_like(self.cube.data) + ) + self.assertEqual(self.cube.data.dtype, np.float32) + + def test_ug_per_m3_units(self): + with mock.patch("warnings.warn") as warn: + self._call_units( + data=((np.ones_like(self.cube.data) * 10)), + units_str="ug/m3E1", + ) + self.assertEqual(warn.call_count, 0) + self.assertEqual(self.cube.units, "ug/m3") + self.assertArrayAlmostEqual( + self.cube.data, np.ones_like(self.cube.data) + ) + self.assertEqual(self.cube.data.dtype, np.float32) + + def test_g_per_kg(self): + with mock.patch("warnings.warn") as warn: + self._call_units( + data=((np.ones_like(self.cube.data) * 1000)), units_str="g/Kg", + ) + self.assertEqual(warn.call_count, 0) + self.assertEqual(self.cube.units, "kg/kg") + self.assertArrayAlmostEqual( + self.cube.data, np.ones_like(self.cube.data) + ) + self.assertEqual(self.cube.data.dtype, np.float32) + + def test_unit_expection_dictionary(self): + with mock.patch("warnings.warn") as warn: + self._call_units(units_str="mb",) + self.assertEqual(warn.call_count, 0) + self.assertEqual(self.cube.units, "hPa") + self.assertArrayAlmostEqual( + self.cube.data, np.ones_like(self.cube.data) + ) + self.assertEqual(self.cube.data.dtype, np.float32) + + def test_per_second(self): + with mock.patch("warnings.warn") as warn: + self._call_units(units_str="/s",) + self.assertEqual(warn.call_count, 0) + self.assertEqual(self.cube.units, "s^-1") + self.assertArrayAlmostEqual( + self.cube.data, np.ones_like(self.cube.data) + ) + self.assertEqual(self.cube.data.dtype, np.float32) + + def test_unhandled_unit(self): + with mock.patch("warnings.warn") as warn: + self._call_units(units_str="kittens",) + self.assertEqual(warn.call_count, 1) + self.assertEqual(self.cube.units, "") + self.assertArrayAlmostEqual( + self.cube.data, np.ones_like(self.cube.data) + ) + self.assertEqual(self.cube.data.dtype, np.float32) + self.assertEqual(self.cube.attributes["invalid_units"], "kittens") + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_vertical_coord.py b/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_vertical_coord.py index cefea984bb..226eb3fa61 100644 --- a/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_vertical_coord.py +++ b/lib/iris/tests/unit/fileformats/nimrod_load_rules/test_vertical_coord.py @@ -28,41 +28,55 @@ class Test(tests.IrisTest): def setUp(self): self.field = mock.Mock( + vertical_coord=NIMROD_DEFAULT, vertical_coord_type=NIMROD_DEFAULT, - int_mdi=mock.sentinel.int_mdi, - field_code=mock.sentinel.field_code, + reference_vertical_coord=NIMROD_DEFAULT, + reference_vertical_coord_type=NIMROD_DEFAULT, + int_mdi=-32767, + float32_mdi=NIMROD_DEFAULT, spec=NimrodField, ) self.cube = mock.Mock() - def _call_vertical_coord(self, vertical_coord_type): - self.field.vertical_coord_type = vertical_coord_type + def _call_vertical_coord( + self, + vertical_coord_val=None, + vertical_coord_type=None, + reference_vertical_coord=None, + reference_vertical_coord_type=None, + ): + if vertical_coord_val: + self.field.vertical_coord = vertical_coord_val + if vertical_coord_type: + self.field.vertical_coord_type = vertical_coord_type + if reference_vertical_coord: + self.field.reference_vertical_coord = reference_vertical_coord + if reference_vertical_coord_type: + self.field.reference_vertical_coord_type = ( + reference_vertical_coord_type + ) vertical_coord(self.cube, self.field) def test_unhandled(self): with mock.patch("warnings.warn") as warn: - self._call_vertical_coord(-1) + self._call_vertical_coord( + vertical_coord_val=1.0, vertical_coord_type=-1 + ) warn.assert_called_once_with( "Vertical coord -1 not yet handled", TranslationWarning ) - def test_orography(self): - name = "orography_vertical_coord" - with mock.patch(self.NIMROD_LOCATION + "." + name) as orog: - self.field.field_code = 73 - self._call_vertical_coord(None) - orog.assert_called_once_with(self.cube, self.field) - - def test_height(self): - name = "height_vertical_coord" - with mock.patch(self.NIMROD_LOCATION + "." + name) as height: - self._call_vertical_coord(0) - height.assert_called_once_with(self.cube, self.field) - def test_null(self): with mock.patch("warnings.warn") as warn: - self._call_vertical_coord(NIMROD_DEFAULT) - self._call_vertical_coord(self.field.int_mdi) + self._call_vertical_coord(vertical_coord_type=NIMROD_DEFAULT) + self._call_vertical_coord(vertical_coord_type=self.field.int_mdi) + self.assertEqual(warn.call_count, 0) + + def test_ground_level(self): + with mock.patch("warnings.warn") as warn: + self._call_vertical_coord( + vertical_coord_val=9999.0, vertical_coord_type=0 + ) self.assertEqual(warn.call_count, 0)