From bdc7175f10b16cb0f99cc8b4b44e80e3ec2cad89 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Fri, 17 Dec 2021 15:53:35 +0000 Subject: [PATCH 01/22] Synthetic FF PP NetCDF and loading benchmarks. --- .../benchmarks/generate_data/__init__.py | 86 ++++++++ .../benchmarks/generate_data/um_files.py | 207 ++++++++++++++++++ benchmarks/benchmarks/loading.py | 142 ++++++++++++ noxfile.py | 41 +++- 4 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 benchmarks/benchmarks/generate_data/__init__.py create mode 100644 benchmarks/benchmarks/generate_data/um_files.py create mode 100644 benchmarks/benchmarks/loading.py diff --git a/benchmarks/benchmarks/generate_data/__init__.py b/benchmarks/benchmarks/generate_data/__init__.py new file mode 100644 index 0000000000..b391a48217 --- /dev/null +++ b/benchmarks/benchmarks/generate_data/__init__.py @@ -0,0 +1,86 @@ +""" +Scripts for generating supporting data for benchmarking. + +Data generated using Iris should use :func:`run_function_elsewhere`, which +means that data is generated using a fixed version of Iris and a fixed +environment, rather than those that get changed when the benchmarking run +checks out a new commit. + +Downstream use of data generated 'elsewhere' requires saving; usually in a +NetCDF file. Could also use pickling but there is a potential risk if the +benchmark sequence runs over two different Python versions. + +""" +from inspect import getsource +from os import environ +from pathlib import Path +from subprocess import CalledProcessError, check_output, run +from textwrap import dedent + +#: Python executable used by :func:`run_function_elsewhere`, set via env +#: variable of same name. Must be path of Python within an environment that +#: includes Iris (including dependencies and test modules) and Mule. +try: + DATA_GEN_PYTHON = environ["DATA_GEN_PYTHON"] + _ = check_output([DATA_GEN_PYTHON, "-c", "a = True"]) +except KeyError: + error = "Env variable DATA_GEN_PYTHON not defined." + raise KeyError(error) +except (CalledProcessError, FileNotFoundError, PermissionError): + error = ( + "Env variable DATA_GEN_PYTHON not a runnable python executable path." + ) + raise ValueError(error) + +default_data_dir = (Path(__file__).parents[2] / ".data").resolve() +BENCHMARK_DATA = Path(environ.get("BENCHMARK_DATA", default_data_dir)) +if BENCHMARK_DATA == default_data_dir: + BENCHMARK_DATA.mkdir(exist_ok=True) +elif not BENCHMARK_DATA.is_dir(): + message = f"Not a directory: {BENCHMARK_DATA} ." + raise ValueError(message) + +# Manual flag to allow the rebuilding of synthetic data. +REUSE_DATA = True + + +def run_function_elsewhere(func_to_run, *args, **kwargs): + """ + Run a given function using the :const:`DATA_GEN_PYTHON` executable. + + This structure allows the function to be written natively. + + Parameters + ---------- + func_to_run : FunctionType + The function object to be run. + NOTE: the function must be completely self-contained, i.e. perform all + its own imports (within the target :const:`DATA_GEN_PYTHON` + environment). + *args : tuple, optional + Function call arguments. Must all be expressible as simple literals, + i.e. the ``repr`` must be a valid literal expression. + **kwargs: dict, optional + Function call keyword arguments. All values must be expressible as + simple literals (see ``*args``). + + Returns + ------- + str + The ``stdout`` from the run. + + """ + func_string = dedent(getsource(func_to_run)) + func_string = func_string.replace("@staticmethod\n", "") + func_call_term_strings = [repr(arg) for arg in args] + func_call_term_strings += [ + f"{name}={repr(val)}" for name, val in kwargs.items() + ] + func_call_string = ( + f"{func_to_run.__name__}(" + ",".join(func_call_term_strings) + ")" + ) + python_string = "\n".join([func_string, func_call_string]) + result = run( + [DATA_GEN_PYTHON, "-c", python_string], capture_output=True, check=True + ) + return result.stdout diff --git a/benchmarks/benchmarks/generate_data/um_files.py b/benchmarks/benchmarks/generate_data/um_files.py new file mode 100644 index 0000000000..11919f2211 --- /dev/null +++ b/benchmarks/benchmarks/generate_data/um_files.py @@ -0,0 +1,207 @@ +""" +Generate FF, PP and NetCDF files based on a minimal synthetic FF file. + +NOTE: uses the Mule package, so depends on an environment with Mule installed. +""" + + +def _create_um_files( + len_x: int, len_y: int, len_z: int, len_t: int, compress, save_paths: dict +) -> None: + """ + Generate an FF object of given shape and compression, save to FF/PP/NetCDF. + + This is run externally + (:func:`benchmarks.generate_data.run_function_elsewhere`), so all imports + are self-contained and input parameters are simple types. + """ + from copy import deepcopy + from datetime import datetime + from tempfile import NamedTemporaryFile + + from mo_pack import compress_wgdos as mo_pack_compress + from mule import ArrayDataProvider, Field3, FieldsFile + from mule.pp import fields_to_pp_file + import numpy as np + + from iris import load_cube + from iris import save as save_cube + + def packing_patch(*compress_args, **compress_kwargs) -> bytes: + """ + Force conversion from returned :class:`memoryview` to :class:`bytes`. + + Downstream uses of :func:`mo_pack.compress_wgdos` were written + for the ``Python2`` behaviour, where the returned buffer had a + different ``__len__`` value to the current :class:`memoryview`. + Unable to fix directly in Mule, so monkey patching for now. + """ + return mo_pack_compress(*compress_args, **compress_kwargs).tobytes() + + import mo_pack + + mo_pack.compress_wgdos = packing_patch + + ######## + + template = { + "fixed_length_header": {"dataset_type": 3, "grid_staggering": 3}, + "integer_constants": { + "num_p_levels": len_z, + "num_cols": len_x, + "num_rows": len_y, + }, + "real_constants": {}, + "level_dependent_constants": {"dims": (len_z + 1, None)}, + } + new_ff = FieldsFile.from_template(deepcopy(template)) + + data_array = np.arange(len_x * len_y).reshape(len_x, len_y) + array_provider = ArrayDataProvider(data_array) + + def add_field(level_: int, time_step_: int) -> None: + """ + Add a minimal field to the new :class:`~mule.FieldsFile`. + + Includes the minimum information to allow Mule saving and Iris + loading, as well as incrementation for vertical levels and time + steps to allow generation of z and t dimensions. + """ + new_field = Field3.empty() + # To correspond to the header-release 3 class used. + new_field.lbrel = 3 + # Mule uses the first element of the lookup to test for + # unpopulated fields (and skips them), so the first element should + # be set to something. The year will do. + new_field.raw[1] = datetime.now().year + + # Horizontal. + new_field.lbcode = 1 + new_field.lbnpt = len_x + new_field.lbrow = len_y + new_field.bdx = new_ff.real_constants.col_spacing + new_field.bdy = new_ff.real_constants.row_spacing + new_field.bzx = new_ff.real_constants.start_lon - 0.5 * new_field.bdx + new_field.bzy = new_ff.real_constants.start_lat - 0.5 * new_field.bdy + + # Hemisphere. + new_field.lbhem = 32 + # Processing. + new_field.lbproc = 0 + + # Vertical. + # Hybrid height values by simulating sequences similar to those in a + # theta file. + new_field.lbvc = 65 + if level_ == 0: + new_field.lblev = 9999 + else: + new_field.lblev = level_ + + level_1 = level_ + 1 + six_rec = 20 / 3 + three_rec = six_rec / 2 + + new_field.blev = level_1 ** 2 * six_rec - six_rec + new_field.brsvd1 = ( + level_1 ** 2 * six_rec + (six_rec * level_1) - three_rec + ) + + brsvd2_simulated = np.linspace(0.995, 0, len_z) + shift = min(len_z, 2) + bhrlev_simulated = np.concatenate( + [np.ones(shift), brsvd2_simulated[:-shift]] + ) + new_field.brsvd2 = brsvd2_simulated[level_] + new_field.bhrlev = bhrlev_simulated[level_] + + # Time. + new_field.lbtim = 11 + + new_field.lbyr = time_step_ + for attr_name in ["lbmon", "lbdat", "lbhr", "lbmin", "lbsec"]: + setattr(new_field, attr_name, 0) + + new_field.lbyrd = time_step_ + 1 + for attr_name in ["lbmond", "lbdatd", "lbhrd", "lbmind", "lbsecd"]: + setattr(new_field, attr_name, 0) + + # Data and packing. + new_field.lbuser1 = 1 + new_field.lbpack = int(compress) + new_field.bacc = 0 + new_field.bmdi = -1 + new_field.lbext = 0 + new_field.set_data_provider(array_provider) + + new_ff.fields.append(new_field) + + for time_step in range(len_t): + for level in range(len_z): + add_field(level, time_step + 1) + + ff_path = save_paths.get("FF", None) + pp_path = save_paths.get("PP", None) + nc_path = save_paths.get("NetCDF", None) + + if ff_path: + new_ff.to_file(ff_path) + if pp_path: + fields_to_pp_file(str(pp_path), new_ff.fields) + if nc_path: + temp_ff_path = None + # Need an Iris Cube from the FF content. + if ff_path: + # Use the existing file. + ff_cube = load_cube(ff_path) + else: + # Make a temporary file. + temp_ff_path = NamedTemporaryFile() + new_ff.to_file(temp_ff_path.name) + ff_cube = load_cube(temp_ff_path.name) + + save_cube(ff_cube, nc_path, zlib=compress) + if temp_ff_path: + temp_ff_path.close() + + +FILE_EXTENSIONS = {"FF": "", "PP": ".pp", "NetCDF": ".nc"} + + +def create_um_files( + len_x: int, + len_y: int, + len_z: int, + len_t: int, + compress: bool, + file_types: list, +) -> dict: + """ + Generate FF-based FF / PP / NetCDF files with specified shape and compression. + + Saved to a directory for all files that shape. A dictionary of the saved + paths is returned. + """ + # Self contained imports to avoid linting confusion with _create_um_files(). + from . import BENCHMARK_DATA, REUSE_DATA, run_function_elsewhere + + save_name_sections = ["UM", len_x, len_y, len_z, len_t] + save_name = "_".join(str(section) for section in save_name_sections) + save_dir = BENCHMARK_DATA / save_name + if not save_dir.is_dir(): + save_dir.mkdir(parents=True) + + save_paths = {} + files_exist = True + for file_type in file_types: + file_ext = FILE_EXTENSIONS[file_type] + save_path = (save_dir / f"{compress}").with_suffix(file_ext) + files_exist = files_exist and save_path.is_file() + save_paths[file_type] = str(save_path) + + if not REUSE_DATA or not files_exist: + _ = run_function_elsewhere( + _create_um_files, len_x, len_y, len_z, len_t, compress, save_paths + ) + + return save_paths diff --git a/benchmarks/benchmarks/loading.py b/benchmarks/benchmarks/loading.py new file mode 100644 index 0000000000..ad6e512d98 --- /dev/null +++ b/benchmarks/benchmarks/loading.py @@ -0,0 +1,142 @@ +""" +File loading benchmark tests. + +Where applicable benchmarks should be parameterised for two sizes of input data: + * minimal: enables detection of regressions in parts of the run-time that do + NOT scale with data size. + * large: large enough to exclusively detect regressions in parts of the + run-time that scale with data size. Size should be _just_ large + enough - don't want to bloat benchmark runtime. + +""" + +from iris import AttributeConstraint, Constraint, load, load_cube +from iris.cube import Cube + +from .generate_data import BENCHMARK_DATA, REUSE_DATA, run_function_elsewhere +from .generate_data.um_files import create_um_files + + +class LoadAndRealise: + params = [ + [(2, 2, 2), (1280, 960, 5)], + [False, True], + ["FF", "PP", "NetCDF"], + ] + param_names = ["xyz", "compressed", "file_format"] + + def setup_cache(self) -> dict: + file_type_args = self.params[2] + file_path_dict = {} + for xyz in self.params[0]: + file_path_dict[xyz] = {} + x, y, z = xyz + for compress in self.params[1]: + file_path_dict[xyz][compress] = create_um_files( + x, y, z, 1, compress, file_type_args + ) + return file_path_dict + + def setup( + self, + file_path_dict: dict, + xyz: tuple, + compress: bool, + file_format: str, + ) -> None: + self.file_path = file_path_dict[xyz][compress][file_format] + self.cube = self.load() + + def load(self) -> Cube: + return load_cube(self.file_path) + + def time_load(self, _, __, ___, ____) -> None: + _ = self.load() + + def time_realise(self, _, __, ___, ____) -> None: + # Cache the original data object and re-apply after realisation, which + # restores original state for the next repeat. The cache/apply steps + # add negligible time to the benchmark result. + data_original = self.cube.core_data() + _ = self.cube.data + self.cube.data = data_original + + +class STASHConstraint: + # xyz sizes mimic LoadAndRealise to maximise file re-use. + params = [[(2, 2, 2), (1280, 960, 5)], ["FF", "PP"]] + param_names = ["xyz", "file_format"] + + def setup_cache(self) -> dict: + file_type_args = self.params[1] + file_path_dict = {} + for xyz in self.params[0]: + x, y, z = xyz + file_path_dict[xyz] = create_um_files( + x, y, z, 1, False, file_type_args + ) + return file_path_dict + + def setup( + self, file_path_dict: dict, xyz: tuple, file_format: str + ) -> None: + self.file_path = file_path_dict[xyz][file_format] + + def time_stash_constraint(self, _, __, ___) -> None: + _ = load_cube(self.file_path, AttributeConstraint(STASH="m??s??i901")) + + +class TimeConstraint: + params = [[3, 20], ["FF", "PP", "NetCDF"]] + param_names = ["time_dim_len", "file_format"] + + def setup_cache(self) -> dict: + file_type_args = self.params[1] + file_path_dict = {} + for time_dim_len in self.params[0]: + file_path_dict[time_dim_len] = create_um_files( + 20, 20, 5, time_dim_len, False, file_type_args + ) + return file_path_dict + + def setup( + self, file_path_dict: dict, time_dim_len: int, file_format: str + ) -> None: + self.file_path = file_path_dict[time_dim_len][file_format] + self.time_constr = Constraint(time=lambda cell: cell.point.year < 3) + + def time_time_constraint(self, _, __, ___) -> None: + _ = load_cube(self.file_path, self.time_constr) + + +class ManyVars: + @staticmethod + def _create_file(save_path: str) -> None: + """Is run externally - everything must be self-contained.""" + import numpy as np + + from iris import save + from iris.coords import AuxCoord + from iris.cube import Cube + + data_len = 8 + data = np.arange(data_len) + cube = Cube(data, units="unknown") + extra_vars = 80 + names = ["coord_" + str(i) for i in range(extra_vars)] + for name in names: + coord = AuxCoord(data, long_name=name, units="unknown") + cube.add_aux_coord(coord, 0) + save(cube, save_path) + + def setup(self) -> None: + self.file_path = BENCHMARK_DATA / "many_var_file.nc" + if not REUSE_DATA or not self.file_path.is_file(): + # See :mod:`benchmarks.generate_data` docstring for full explanation. + _ = run_function_elsewhere( + self._create_file, + str(self.file_path), + ) + + def time_many_var_load(self) -> None: + _ = load(str(self.file_path)) diff --git a/noxfile.py b/noxfile.py index 497330de37..ff4795a7da 100755 --- a/noxfile.py +++ b/noxfile.py @@ -280,7 +280,7 @@ def linkcheck(session: nox.sessions.Session): ) -@nox.session(python=PY_VER[-1], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") @nox.parametrize( ["ci_mode"], [True, False], @@ -288,7 +288,7 @@ def linkcheck(session: nox.sessions.Session): ) def benchmarks(session: nox.sessions.Session, ci_mode: bool): """ - Perform esmf-regrid performance benchmarks (using Airspeed Velocity). + Perform Iris performance benchmarks (using Airspeed Velocity). Parameters ---------- @@ -306,6 +306,43 @@ def benchmarks(session: nox.sessions.Session, ci_mode: bool): """ session.install("asv", "nox") + + data_gen_var = "DATA_GEN_PYTHON" + if data_gen_var in os.environ: + print("Using existing data generation environment.") + else: + print("Setting up the data generation environment...") + session.run_always( + "nox", + "--session=tests", + "--install-only", + f"--python={session.python}", + ) + data_gen_python = next( + Path(".nox").rglob(f"tests*/bin/python{session.python}") + ).resolve() + session.env[data_gen_var] = data_gen_python + + print("Installing Mule into data generation environment...") + mule_dir = data_gen_python.parents[1] / "resources" / "mule" + if not mule_dir.is_dir(): + session.run_always( + "git", + "clone", + "https://github.com/metomi/mule.git", + str(mule_dir), + external=True, + ) + session.run_always( + str(data_gen_python), + "-m", + "pip", + "install", + str(mule_dir / "mule"), + external=True, + ) + + print("Running ASV...") session.cd("benchmarks") # Skip over setup questions for a new machine. session.run("asv", "machine", "--yes") From eb3d772963a2ce135f56262b49ac262c88bef650 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Wed, 5 Jan 2022 09:59:57 +0000 Subject: [PATCH 02/22] Remove legacy benchmark data directory handling. --- benchmarks/benchmarks/__init__.py | 41 ------------------------------- 1 file changed, 41 deletions(-) diff --git a/benchmarks/benchmarks/__init__.py b/benchmarks/benchmarks/__init__.py index 2e741c3da0..4a964a648d 100644 --- a/benchmarks/benchmarks/__init__.py +++ b/benchmarks/benchmarks/__init__.py @@ -5,45 +5,4 @@ # licensing details. """Common code for benchmarks.""" -import os -from pathlib import Path - -# Environment variable names -_ASVDIR_VARNAME = "ASV_DIR" # As set in nightly script "asv_nightly/asv.sh" -_DATADIR_VARNAME = "BENCHMARK_DATA" # For local runs - ARTIFICIAL_DIM_SIZE = int(10e3) # For all artificial cubes, coords etc. - -# Work out where the benchmark data dir is. -asv_dir = os.environ.get("ASV_DIR", None) -if asv_dir: - # For an overnight run, this comes from the 'ASV_DIR' setting. - benchmark_data_dir = Path(asv_dir) / "data" -else: - # For a local run, you set 'BENCHMARK_DATA'. - benchmark_data_dir = os.environ.get(_DATADIR_VARNAME, None) - if benchmark_data_dir is not None: - benchmark_data_dir = Path(benchmark_data_dir) - - -def testdata_path(*path_names): - """ - Return the path of a benchmark test data file. - - These are based from a test-data location dir, which is either - ${}/data (for overnight tests), or ${} for local testing. - - If neither of these were set, an error is raised. - - """.format( - _ASVDIR_VARNAME, _DATADIR_VARNAME - ) - if benchmark_data_dir is None: - msg = ( - "Benchmark data dir is not defined : " - 'Either "${}" or "${}" must be set.' - ) - raise (ValueError(msg.format(_ASVDIR_VARNAME, _DATADIR_VARNAME))) - path = benchmark_data_dir.joinpath(*path_names) - path = str(path) # Because Iris doesn't understand Path objects yet. - return path From 94c855b533407108beb1ec48c9da32c01934bd73 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Wed, 5 Jan 2022 10:20:53 +0000 Subject: [PATCH 03/22] GitHub benchmark action fixed PY_VER. --- .github/workflows/benchmark.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index b489eba036..041b84f17c 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,6 +17,7 @@ jobs: IRIS_TEST_DATA_VERSION: "2.5" # Lets us manually bump the cache to rebuild TEST_DATA_CACHE_BUILD: "2" + PY_VER: 3.8 steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it @@ -62,7 +63,7 @@ jobs: unzip -q iris-test-data.zip mkdir --parents ${GITHUB_WORKSPACE}/${IRIS_TEST_DATA_LOC_PATH} mv iris-test-data-${IRIS_TEST_DATA_VERSION} ${GITHUB_WORKSPACE}/${IRIS_TEST_DATA_PATH} - + - name: Set test data var run: | echo "OVERRIDE_TEST_DATA_REPOSITORY=${GITHUB_WORKSPACE}/${IRIS_TEST_DATA_PATH}/test_data" >> $GITHUB_ENV From 1e7b6a0292d3554d74038b2fd13d7d00f3fccd54 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Wed, 5 Jan 2022 10:49:40 +0000 Subject: [PATCH 04/22] Missing licence headers. --- benchmarks/benchmarks/generate_data/__init__.py | 5 +++++ benchmarks/benchmarks/generate_data/um_files.py | 5 +++++ benchmarks/benchmarks/loading.py | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/benchmarks/benchmarks/generate_data/__init__.py b/benchmarks/benchmarks/generate_data/__init__.py index b391a48217..a635d3c103 100644 --- a/benchmarks/benchmarks/generate_data/__init__.py +++ b/benchmarks/benchmarks/generate_data/__init__.py @@ -1,3 +1,8 @@ +# 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. """ Scripts for generating supporting data for benchmarking. diff --git a/benchmarks/benchmarks/generate_data/um_files.py b/benchmarks/benchmarks/generate_data/um_files.py index 11919f2211..8ad6777808 100644 --- a/benchmarks/benchmarks/generate_data/um_files.py +++ b/benchmarks/benchmarks/generate_data/um_files.py @@ -1,3 +1,8 @@ +# 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. """ Generate FF, PP and NetCDF files based on a minimal synthetic FF file. diff --git a/benchmarks/benchmarks/loading.py b/benchmarks/benchmarks/loading.py index ad6e512d98..7cc95762c5 100644 --- a/benchmarks/benchmarks/loading.py +++ b/benchmarks/benchmarks/loading.py @@ -1,3 +1,8 @@ +# 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. """ File loading benchmark tests. From 83eef278a72680c95f461d235a74ac4715616c89 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Wed, 5 Jan 2022 11:06:40 +0000 Subject: [PATCH 05/22] Cache generated benchmark data. --- .github/workflows/benchmark.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 041b84f17c..bcfceb1646 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,6 +17,7 @@ jobs: IRIS_TEST_DATA_VERSION: "2.5" # Lets us manually bump the cache to rebuild TEST_DATA_CACHE_BUILD: "2" + GEN_DATA_CACHE_BUILD: "1" PY_VER: 3.8 steps: @@ -56,6 +57,13 @@ jobs: key: test-data-${{ env.IRIS_TEST_DATA_VERSION }}-${{ env.TEST_DATA_CACHE_BUILD }} + - name: Cache generated data directory + id: cache-generated-data + uses: actions/cache@v2 + with: + path: benchmarks/.data + key: ${{ hashFiles('benchmarks/data') }}-${{ env.GEN_DATA_CACHE_BUILD }} + - name: Fetch the test data if: steps.cache-test-data.outputs.cache-hit != 'true' run: | From 94aff73f0183ccdf71826b7e2475547266693bed Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Wed, 5 Jan 2022 11:34:29 +0000 Subject: [PATCH 06/22] ALWAYS cache benchmark generated data. --- .github/workflows/benchmark.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index bcfceb1646..9be3c11548 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,7 +17,6 @@ jobs: IRIS_TEST_DATA_VERSION: "2.5" # Lets us manually bump the cache to rebuild TEST_DATA_CACHE_BUILD: "2" - GEN_DATA_CACHE_BUILD: "1" PY_VER: 3.8 steps: @@ -54,8 +53,13 @@ jobs: with: path: | ${{ env.IRIS_TEST_DATA_PATH }} - key: - test-data-${{ env.IRIS_TEST_DATA_VERSION }}-${{ env.TEST_DATA_CACHE_BUILD }} + # Make sure GHA never gets an exact cache match by using the unique + # github.sha. This means it will always store this run as a new + # cache (any changes happen DURING the run, and I don't _think_ GHA + # caching has anything native to support this). Cache restoration + # still succeeds via the partial restore-key match. + key: ${{ runner.os }}-${{ github.sha }} + restore-keys: ${{ runner.os }} - name: Cache generated data directory id: cache-generated-data From fe82b24653c1fc5ceb0bd8e94e7605f6f0349eb0 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Wed, 5 Jan 2022 16:00:16 +0000 Subject: [PATCH 07/22] Also add StructuredFF benchmark. --- benchmarks/benchmarks/loading.py | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/benchmarks/benchmarks/loading.py b/benchmarks/benchmarks/loading.py index 7cc95762c5..41320bc6bf 100644 --- a/benchmarks/benchmarks/loading.py +++ b/benchmarks/benchmarks/loading.py @@ -17,6 +17,7 @@ from iris import AttributeConstraint, Constraint, load, load_cube from iris.cube import Cube +from iris.fileformats.um import structured_um_loading from .generate_data import BENCHMARK_DATA, REUSE_DATA, run_function_elsewhere from .generate_data.um_files import create_um_files @@ -145,3 +146,41 @@ def setup(self) -> None: def time_many_var_load(self) -> None: _ = load(str(self.file_path)) + + +class StructuredFF: + """ + Test structured loading of a large-ish fieldsfile. + + Structured load of the larger size should show benefit over standard load, + avoiding the cost of merging. + """ + + params = [[(2, 2, 2), (1280, 960, 5)], [False, True]] + param_names = ["xyz", "structured_loading"] + + def setup_cache(self) -> dict: + file_path_dict = {} + for xyz in self.params[0]: + x, y, z = xyz + file_path_dict[xyz] = create_um_files(x, y, z, 1, False, ["FF"]) + return file_path_dict + + def setup(self, file_path_dict, xyz, structured_load): + self.file_path = file_path_dict[xyz]["FF"] + self.structured_load = structured_load + + def load(self): + """Load the whole file (in fact there is only 1 cube).""" + + def _load(): + _ = load(self.file_path) + + if self.structured_load: + with structured_um_loading(): + _load() + else: + _load() + + def time_structured_load(self, _, __, ___): + self.load() From 7fe7d5cbee370c1d35c1768601da029a10e1b120 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 6 Jan 2022 10:03:22 +0000 Subject: [PATCH 08/22] Revert "ALWAYS cache benchmark generated data." This reverts commit 94aff73f0183ccdf71826b7e2475547266693bed. --- .github/workflows/benchmark.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 9be3c11548..bcfceb1646 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,6 +17,7 @@ jobs: IRIS_TEST_DATA_VERSION: "2.5" # Lets us manually bump the cache to rebuild TEST_DATA_CACHE_BUILD: "2" + GEN_DATA_CACHE_BUILD: "1" PY_VER: 3.8 steps: @@ -53,13 +54,8 @@ jobs: with: path: | ${{ env.IRIS_TEST_DATA_PATH }} - # Make sure GHA never gets an exact cache match by using the unique - # github.sha. This means it will always store this run as a new - # cache (any changes happen DURING the run, and I don't _think_ GHA - # caching has anything native to support this). Cache restoration - # still succeeds via the partial restore-key match. - key: ${{ runner.os }}-${{ github.sha }} - restore-keys: ${{ runner.os }} + key: + test-data-${{ env.IRIS_TEST_DATA_VERSION }}-${{ env.TEST_DATA_CACHE_BUILD }} - name: Cache generated data directory id: cache-generated-data From 0a806f1eecc0d6dd4239d00e2b5a86718b800712 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 6 Jan 2022 10:11:28 +0000 Subject: [PATCH 09/22] Revert "Cache generated benchmark data." This reverts commit 83eef278a72680c95f461d235a74ac4715616c89. --- .github/workflows/benchmark.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index bcfceb1646..041b84f17c 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -17,7 +17,6 @@ jobs: IRIS_TEST_DATA_VERSION: "2.5" # Lets us manually bump the cache to rebuild TEST_DATA_CACHE_BUILD: "2" - GEN_DATA_CACHE_BUILD: "1" PY_VER: 3.8 steps: @@ -57,13 +56,6 @@ jobs: key: test-data-${{ env.IRIS_TEST_DATA_VERSION }}-${{ env.TEST_DATA_CACHE_BUILD }} - - name: Cache generated data directory - id: cache-generated-data - uses: actions/cache@v2 - with: - path: benchmarks/.data - key: ${{ hashFiles('benchmarks/data') }}-${{ env.GEN_DATA_CACHE_BUILD }} - - name: Fetch the test data if: steps.cache-test-data.outputs.cache-hit != 'true' run: | From 7561195467ef6fad05e24b5e11f9e6716da3638e Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 6 Jan 2022 13:27:11 +0000 Subject: [PATCH 10/22] Improved benchmark GHA env caching (2min faster?) --- .github/workflows/benchmark.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 041b84f17c..a8247a247b 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -16,6 +16,7 @@ jobs: IRIS_TEST_DATA_PATH: benchmarks/iris-test-data IRIS_TEST_DATA_VERSION: "2.5" # Lets us manually bump the cache to rebuild + ENV_CACHE_BUILD: "0" TEST_DATA_CACHE_BUILD: "2" PY_VER: 3.8 @@ -33,19 +34,15 @@ jobs: run: | pip install nox - - name: Cache .nox and .asv/env directories + - name: Cache environment directories id: cache-env-dir uses: actions/cache@v2 with: path: | .nox benchmarks/.asv/env - # Make sure GHA never gets an exact cache match by using the unique - # github.sha. This means it will always store this run as a new - # cache (Nox may have made relevant changes during run). Cache - # restoration still succeeds via the partial restore-key match. - key: ${{ runner.os }}-${{ github.sha }} - restore-keys: ${{ runner.os }} + $CONDA/pkgs + key: ${{ runner.os }}-${{ hashFiles('requirements/') }}-${{ env.ENV_CACHE_BUILD }} - name: Cache test data directory id: cache-test-data From d503ce62c41e9d827b91121e4f6395d1fcb6b644 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Feb 2022 10:54:53 +0000 Subject: [PATCH 11/22] [pre-commit.ci] pre-commit autoupdate (#4560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * v3.2.0rc0 whats new and version string (#4557) * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/psf/black: 21.12b0 → 22.1.0](https://github.com/psf/black/compare/21.12b0...22.1.0) - [github.com/asottile/blacken-docs: v1.12.0 → v1.12.1](https://github.com/asottile/blacken-docs/compare/v1.12.0...v1.12.1) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci Co-authored-by: lbdreyer Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 +- benchmarks/benchmarks/plot.py | 2 +- .../meteorology/plot_wind_barbs.py | 4 +- .../meteorology/plot_wind_speed.py | 2 +- docs/src/whatsnew/{dev.rst => 3.2.rst} | 9 +- docs/src/whatsnew/dev.rst.template | 112 ------------------ docs/src/whatsnew/index.rst | 2 +- docs/src/whatsnew/latest.rst | 2 +- lib/iris/__init__.py | 2 +- lib/iris/analysis/__init__.py | 2 +- lib/iris/analysis/_grid_angles.py | 2 +- lib/iris/analysis/_scipy_interpolate.py | 2 +- lib/iris/analysis/calculus.py | 12 +- lib/iris/analysis/cartography.py | 6 +- lib/iris/analysis/maths.py | 2 +- lib/iris/analysis/stats.py | 4 +- lib/iris/fileformats/netcdf.py | 4 +- lib/iris/fileformats/pp.py | 4 +- lib/iris/tests/integration/test_netcdf.py | 6 +- lib/iris/tests/test_basic_maths.py | 38 +++--- .../analysis/cartography/test_rotate_winds.py | 8 +- .../regrid/test_RectilinearRegridder.py | 2 +- .../test_add_categorised_coord.py | 2 +- ...__collapse_degenerate_points_and_bounds.py | 2 +- 24 files changed, 59 insertions(+), 176 deletions(-) rename docs/src/whatsnew/{dev.rst => 3.2.rst} (98%) delete mode 100644 docs/src/whatsnew/dev.rst.template diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 97dff666cf..228970bee2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: no-commit-to-branch - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 22.1.0 hooks: - id: black pass_filenames: false @@ -50,7 +50,7 @@ repos: args: [--filter-files] - repo: https://github.com/asottile/blacken-docs - rev: v1.12.0 + rev: v1.12.1 hooks: - id: blacken-docs types: [file, rst] diff --git a/benchmarks/benchmarks/plot.py b/benchmarks/benchmarks/plot.py index 45905abd2f..24899776dc 100644 --- a/benchmarks/benchmarks/plot.py +++ b/benchmarks/benchmarks/plot.py @@ -22,7 +22,7 @@ def setup(self): # Should generate 10 distinct contours, regardless of dim size. dim_size = int(ARTIFICIAL_DIM_SIZE / 5) repeat_number = int(dim_size / 10) - repeat_range = range(int((dim_size ** 2) / repeat_number)) + repeat_range = range(int((dim_size**2) / repeat_number)) data = np.repeat(repeat_range, repeat_number) data = data.reshape((dim_size,) * 2) diff --git a/docs/gallery_code/meteorology/plot_wind_barbs.py b/docs/gallery_code/meteorology/plot_wind_barbs.py index c3c056eb4a..b09040c64e 100644 --- a/docs/gallery_code/meteorology/plot_wind_barbs.py +++ b/docs/gallery_code/meteorology/plot_wind_barbs.py @@ -30,7 +30,7 @@ def main(): # To illustrate the full range of barbs, scale the wind speed up to pretend # that a storm is passing over - magnitude = (uwind ** 2 + vwind ** 2) ** 0.5 + magnitude = (uwind**2 + vwind**2) ** 0.5 magnitude.convert_units("knot") max_speed = magnitude.collapsed( ("latitude", "longitude"), iris.analysis.MAX @@ -41,7 +41,7 @@ def main(): vwind = vwind / max_speed * max_desired # Create a cube containing the wind speed - windspeed = (uwind ** 2 + vwind ** 2) ** 0.5 + windspeed = (uwind**2 + vwind**2) ** 0.5 windspeed.rename("windspeed") windspeed.convert_units("knot") diff --git a/docs/gallery_code/meteorology/plot_wind_speed.py b/docs/gallery_code/meteorology/plot_wind_speed.py index fd03f54205..40d9d0da00 100644 --- a/docs/gallery_code/meteorology/plot_wind_speed.py +++ b/docs/gallery_code/meteorology/plot_wind_speed.py @@ -27,7 +27,7 @@ def main(): vwind = iris.load_cube(infile, "y_wind") # Create a cube containing the wind speed. - windspeed = (uwind ** 2 + vwind ** 2) ** 0.5 + windspeed = (uwind**2 + vwind**2) ** 0.5 windspeed.rename("windspeed") # Plot the wind speed as a contour plot. diff --git a/docs/src/whatsnew/dev.rst b/docs/src/whatsnew/3.2.rst similarity index 98% rename from docs/src/whatsnew/dev.rst rename to docs/src/whatsnew/3.2.rst index e2d4c2bc0b..c78e1283d6 100644 --- a/docs/src/whatsnew/dev.rst +++ b/docs/src/whatsnew/3.2.rst @@ -1,13 +1,13 @@ .. include:: ../common_links.inc -|iris_version| |build_date| [unreleased] -**************************************** +v3.2 (31 Jan 2022) [unreleased] +******************************* This document explains the changes made to Iris for this release (:doc:`View all changes `.) -.. dropdown:: :opticon:`report` |iris_version| Release Highlights +.. dropdown:: :opticon:`report` v3.2.0 Release Highlights :container: + shadow :title: text-primary text-center font-weight-bold :body: bg-light @@ -18,8 +18,7 @@ This document explains the changes made to Iris for this release * We've added experimental support for :ref:`Meshes `, which can now be loaded and - attached to a cube. Mesh support is based on the based on `CF-UGRID`_ - model. + attached to a cube. Mesh support is based on the `CF-UGRID`_ model. * We've also dropped support for ``Python 3.7``. And finally, get in touch with us on :issue:`GitHub` if you have diff --git a/docs/src/whatsnew/dev.rst.template b/docs/src/whatsnew/dev.rst.template deleted file mode 100644 index 79c578ca65..0000000000 --- a/docs/src/whatsnew/dev.rst.template +++ /dev/null @@ -1,112 +0,0 @@ -.. include:: ../common_links.inc - -|iris_version| |build_date| [unreleased] -**************************************** - -This document explains the changes made to Iris for this release -(:doc:`View all changes `.) - - -.. dropdown:: :opticon:`report` |iris_version| Release Highlights - :container: + shadow - :title: text-primary text-center font-weight-bold - :body: bg-light - :animate: fade-in - :open: - - The highlights for this major/minor release of Iris include: - - * N/A - - And finally, get in touch with us on :issue:`GitHub` if you have - any issues or feature requests for improving Iris. Enjoy! - - -NOTE: section below is a template for bugfix patches -==================================================== - (Please remove this section when creating an initial 'latest.rst') - -v3.X.X (DD MMM YYYY) -==================== - -.. dropdown:: :opticon:`alert` v3.X.X Patches - :container: + shadow - :title: text-primary text-center font-weight-bold - :body: bg-light - :animate: fade-in - - The patches in this release of Iris include: - - #. N/A - -NOTE: section above is a template for bugfix patches -==================================================== - (Please remove this section when creating an initial 'latest.rst') - - - -📢 Announcements -================ - -#. N/A - - -✨ Features -=========== - -#. N/A - - -🐛 Bugs Fixed -============= - -#. N/A - - -💣 Incompatible Changes -======================= - -#. N/A - - -🚀 Performance Enhancements -=========================== - -#. N/A - - -🔥 Deprecations -=============== - -#. N/A - - -🔗 Dependencies -=============== - -#. N/A - - -📚 Documentation -================ - -#. N/A - - -💼 Internal -=========== - -#. N/A - - -.. comment - Whatsnew author names (@github name) in alphabetical order. Note that, - core dev names are automatically included by the common_links.inc: - - - - -.. comment - Whatsnew resources in alphabetical order: - - diff --git a/docs/src/whatsnew/index.rst b/docs/src/whatsnew/index.rst index 51f03e8d8f..f425e649b9 100644 --- a/docs/src/whatsnew/index.rst +++ b/docs/src/whatsnew/index.rst @@ -10,7 +10,7 @@ Iris versions. .. toctree:: :maxdepth: 1 - dev.rst + 3.2.rst 3.1.rst 3.0.rst 2.4.rst diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 56aebe92dd..2bdbea5d85 120000 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -1 +1 @@ -dev.rst \ No newline at end of file +3.2.rst \ No newline at end of file diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 26f03c0566..aca4e77e88 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -104,7 +104,7 @@ def callback(cube, field, filename): # Iris revision. -__version__ = "3.2.dev0" +__version__ = "3.2.0rc0" # Restrict the names imported when using "from iris import *" __all__ = [ diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index 465a521065..b1a9e1d259 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -1394,7 +1394,7 @@ def _lazy_rms(array, axis, **kwargs): # all. Thus trying to use this aggregator with weights will currently # raise an error in dask due to the unexpected keyword `weights`, # rather than silently returning the wrong answer. - return da.sqrt(da.mean(array ** 2, axis=axis, **kwargs)) + return da.sqrt(da.mean(array**2, axis=axis, **kwargs)) @_build_dask_mdtol_function diff --git a/lib/iris/analysis/_grid_angles.py b/lib/iris/analysis/_grid_angles.py index 127aec7c1e..0b52f54568 100644 --- a/lib/iris/analysis/_grid_angles.py +++ b/lib/iris/analysis/_grid_angles.py @@ -120,7 +120,7 @@ def _angle(p, q, r): mid_lons = np.deg2rad(q[0]) pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) - pr_norm = np.sqrt(np.sum(pr ** 2, axis=0)) + pr_norm = np.sqrt(np.sum(pr**2, axis=0)) pr_top = pr[1] * np.cos(mid_lons) - pr[0] * np.sin(mid_lons) index = pr_norm == 0 diff --git a/lib/iris/analysis/_scipy_interpolate.py b/lib/iris/analysis/_scipy_interpolate.py index c6b33c56a4..fc64249729 100644 --- a/lib/iris/analysis/_scipy_interpolate.py +++ b/lib/iris/analysis/_scipy_interpolate.py @@ -229,7 +229,7 @@ def compute_interp_weights(self, xi, method=None): xi_shape, method, indices, norm_distances, out_of_bounds = prepared # Allocate arrays for describing the sparse matrix. - n_src_values_per_result_value = 2 ** ndim + n_src_values_per_result_value = 2**ndim n_result_values = len(indices[0]) n_non_zero = n_result_values * n_src_values_per_result_value weights = np.ones(n_non_zero, dtype=norm_distances[0].dtype) diff --git a/lib/iris/analysis/calculus.py b/lib/iris/analysis/calculus.py index 409782f256..4630f47967 100644 --- a/lib/iris/analysis/calculus.py +++ b/lib/iris/analysis/calculus.py @@ -629,14 +629,10 @@ def curl(i_cube, j_cube, k_cube=None): # (d/dtheta (i_cube * sin(lat)) - d_j_cube_dphi) # phi_cmpt = 1/r * ( d/dr (r * j_cube) - d_k_cube_dtheta) # theta_cmpt = 1/r * ( 1/cos(lat) * d_k_cube_dphi - d/dr (r * i_cube) - if ( - y_coord.name() - not in [ - "latitude", - "grid_latitude", - ] - or x_coord.name() not in ["longitude", "grid_longitude"] - ): + if y_coord.name() not in [ + "latitude", + "grid_latitude", + ] or x_coord.name() not in ["longitude", "grid_longitude"]: raise ValueError( "Expecting latitude as the y coord and " "longitude as the x coord for spherical curl." diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index 373487af53..f704468e33 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -335,7 +335,7 @@ def _quadrant_area(radian_lat_bounds, radian_lon_bounds, radius_of_earth): raise ValueError("Bounds must be [n,2] array") # fill in a new array of areas - radius_sqr = radius_of_earth ** 2 + radius_sqr = radius_of_earth**2 radian_lat_64 = radian_lat_bounds.astype(np.float64) radian_lon_64 = radian_lon_bounds.astype(np.float64) @@ -1010,8 +1010,8 @@ def _transform_distance_vectors_tolerance_mask( # Squared magnitudes should be equal to one within acceptable tolerance. # A value of atol=2e-3 is used, which corresponds to a change in magnitude # of approximately 0.1%. - sqmag_1_0 = u_one_t ** 2 + v_zero_t ** 2 - sqmag_0_1 = u_zero_t ** 2 + v_one_t ** 2 + sqmag_1_0 = u_one_t**2 + v_zero_t**2 + sqmag_0_1 = u_zero_t**2 + v_one_t**2 mask = np.logical_not( np.logical_and( np.isclose(sqmag_1_0, ones, atol=2e-3), diff --git a/lib/iris/analysis/maths.py b/lib/iris/analysis/maths.py index 107d964ed4..1cbc90cc60 100644 --- a/lib/iris/analysis/maths.py +++ b/lib/iris/analysis/maths.py @@ -540,7 +540,7 @@ def power(data, out=None): return _math_op_common( cube, power, - cube.units ** exponent, + cube.units**exponent, new_dtype=new_dtype, in_place=in_place, ) diff --git a/lib/iris/analysis/stats.py b/lib/iris/analysis/stats.py index 89dde1818b..711e3c5bfb 100644 --- a/lib/iris/analysis/stats.py +++ b/lib/iris/analysis/stats.py @@ -168,10 +168,10 @@ def _ones_like(cube): covar = (s1 * s2).collapsed( corr_coords, iris.analysis.SUM, weights=weights_1, mdtol=mdtol ) - var_1 = (s1 ** 2).collapsed( + var_1 = (s1**2).collapsed( corr_coords, iris.analysis.SUM, weights=weights_1 ) - var_2 = (s2 ** 2).collapsed( + var_2 = (s2**2).collapsed( corr_coords, iris.analysis.SUM, weights=weights_2 ) diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 100ab29daa..73a137b4af 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -2738,9 +2738,9 @@ def _create_cf_data_variable( cmin, cmax = _co_realise_lazy_arrays([cmin, cmax]) n = dtype.itemsize * 8 if masked: - scale_factor = (cmax - cmin) / (2 ** n - 2) + scale_factor = (cmax - cmin) / (2**n - 2) else: - scale_factor = (cmax - cmin) / (2 ** n - 1) + scale_factor = (cmax - cmin) / (2**n - 1) if dtype.kind == "u": add_offset = cmin elif dtype.kind == "i": diff --git a/lib/iris/fileformats/pp.py b/lib/iris/fileformats/pp.py index 9f213ec4db..9bda98bf61 100644 --- a/lib/iris/fileformats/pp.py +++ b/lib/iris/fileformats/pp.py @@ -403,7 +403,7 @@ def _calculate_str_value_from_value(self): def _calculate_value_from_str_value(self): self._value = np.sum( - [10 ** i * val for i, val in enumerate(self._strvalue)] + [10**i * val for i, val in enumerate(self._strvalue)] ) def __len__(self): @@ -418,7 +418,7 @@ def __getitem__(self, key): # if the key returns a list of values, then combine them together # to an integer if isinstance(val, list): - val = sum([10 ** i * val for i, val in enumerate(val)]) + val = sum([10**i * val for i, val in enumerate(val)]) return val diff --git a/lib/iris/tests/integration/test_netcdf.py b/lib/iris/tests/integration/test_netcdf.py index f7aaa1d05c..2a45561e17 100644 --- a/lib/iris/tests/integration/test_netcdf.py +++ b/lib/iris/tests/integration/test_netcdf.py @@ -416,7 +416,7 @@ def setUp(self): levels.units = "centimeters" levels.positive = "down" levels.axis = "Z" - levels[:] = np.linspace(0, 10 ** 5, 3) + levels[:] = np.linspace(0, 10**5, 3) volcello.id = "volcello" volcello.out_name = "volcello" @@ -507,9 +507,9 @@ def _get_scale_factor_add_offset(cube, datatype): else: masked = False if masked: - scale_factor = (cmax - cmin) / (2 ** n - 2) + scale_factor = (cmax - cmin) / (2**n - 2) else: - scale_factor = (cmax - cmin) / (2 ** n - 1) + scale_factor = (cmax - cmin) / (2**n - 1) if dt.kind == "u": add_offset = cmin elif dt.kind == "i": diff --git a/lib/iris/tests/test_basic_maths.py b/lib/iris/tests/test_basic_maths.py index e753adbae8..24f2b89442 100644 --- a/lib/iris/tests/test_basic_maths.py +++ b/lib/iris/tests/test_basic_maths.py @@ -249,7 +249,7 @@ def test_apply_ufunc(self): np.square, a, new_name="squared temperature", - new_unit=a.units ** 2, + new_unit=a.units**2, in_place=False, ) self.assertCMLApproxData(a, ("analysis", "apply_ufunc_original.cml")) @@ -259,14 +259,14 @@ def test_apply_ufunc(self): np.square, a, new_name="squared temperature", - new_unit=a.units ** 2, + new_unit=a.units**2, in_place=True, ) self.assertCMLApproxData(b, ("analysis", "apply_ufunc.cml")) self.assertCMLApproxData(a, ("analysis", "apply_ufunc.cml")) def vec_mag(u, v): - return math.sqrt(u ** 2 + v ** 2) + return math.sqrt(u**2 + v**2) c = a.copy() + 2 @@ -295,7 +295,7 @@ def test_apply_ufunc_fail(self): def test_ifunc(self): a = self.cube - my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units ** 2) + my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units**2) b = my_ifunc(a, new_name="squared temperature", in_place=False) self.assertCMLApproxData(a, ("analysis", "apply_ifunc_original.cml")) @@ -307,7 +307,7 @@ def test_ifunc(self): self.assertCMLApproxData(a, ("analysis", "apply_ifunc.cml")) def vec_mag(u, v): - return math.sqrt(u ** 2 + v ** 2) + return math.sqrt(u**2 + v**2) c = a.copy() + 2 @@ -347,7 +347,7 @@ def test_ifunc_init_fail(self): def test_ifunc_call_fail(self): a = self.cube - my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units ** 2) + my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units**2) # should now NOT fail because giving 2 arguments to an ifunc that # expects only one will now ignore the surplus argument and raise @@ -367,7 +367,7 @@ def test_ifunc_call_fail(self): my_ifunc(a) my_ifunc = iris.analysis.maths.IFunc( - lambda a: (a, a ** 2.0), lambda cube: cf_units.Unit("1") + lambda a: (a, a**2.0), lambda cube: cf_units.Unit("1") ) # should fail because data function returns a tuple @@ -553,9 +553,9 @@ def test_square_root(self): a.data = abs(a.data) a.units **= 2 - e = a ** 0.5 + e = a**0.5 - self.assertArrayAllClose(e.data, a.data ** 0.5) + self.assertArrayAllClose(e.data, a.data**0.5) self.assertCML(e, ("analysis", "sqrt.cml"), checksum=False) self.assertRaises(ValueError, iris.analysis.maths.exponentiate, a, 0.3) @@ -585,26 +585,26 @@ def test_apply_ufunc(self): np.square, a, new_name="more_thingness", - new_unit=a.units ** 2, + new_unit=a.units**2, in_place=False, ) - ans = a.data ** 2 + ans = a.data**2 self.assertArrayEqual(b.data, ans) self.assertEqual(b.name(), "more_thingness") self.assertEqual(b.units, cf_units.Unit("m^2")) def vec_mag(u, v): - return math.sqrt(u ** 2 + v ** 2) + return math.sqrt(u**2 + v**2) c = a.copy() + 2 vec_mag_ufunc = np.frompyfunc(vec_mag, 2, 1) b = iris.analysis.maths.apply_ufunc(vec_mag_ufunc, a, c) - ans = a.data ** 2 + c.data ** 2 - b2 = b ** 2 + ans = a.data**2 + c.data**2 + b2 = b**2 self.assertArrayAlmostEqual(b2.data, ans) @@ -617,17 +617,17 @@ def test_ifunc(self): a = self.cube a.units = cf_units.Unit("meters") - my_ifunc = iris.analysis.maths.IFunc(np.square, lambda x: x.units ** 2) + my_ifunc = iris.analysis.maths.IFunc(np.square, lambda x: x.units**2) b = my_ifunc(a, new_name="more_thingness", in_place=False) - ans = a.data ** 2 + ans = a.data**2 self.assertArrayEqual(b.data, ans) self.assertEqual(b.name(), "more_thingness") self.assertEqual(b.units, cf_units.Unit("m^2")) def vec_mag(u, v): - return math.sqrt(u ** 2 + v ** 2) + return math.sqrt(u**2 + v**2) c = a.copy() + 2 @@ -637,12 +637,12 @@ def vec_mag(u, v): ) b = my_ifunc(a, c) - ans = (a.data ** 2 + c.data ** 2) ** 0.5 + ans = (a.data**2 + c.data**2) ** 0.5 self.assertArrayAlmostEqual(b.data, ans) def vec_mag_data_func(u_data, v_data): - return np.sqrt(u_data ** 2 + v_data ** 2) + return np.sqrt(u_data**2 + v_data**2) vec_mag_ifunc = iris.analysis.maths.IFunc( vec_mag_data_func, lambda a, b: (a + b).units diff --git a/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py b/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py index 9e3af90603..eafaa20ec8 100644 --- a/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py +++ b/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py @@ -343,8 +343,8 @@ def test_orig_coords(self): def test_magnitude_preservation(self): u, v = self._uv_cubes_limited_extent() ut, vt = rotate_winds(u, v, iris.coord_systems.OSGB()) - orig_sq_mag = u.data ** 2 + v.data ** 2 - res_sq_mag = ut.data ** 2 + vt.data ** 2 + orig_sq_mag = u.data**2 + v.data**2 + res_sq_mag = ut.data**2 + vt.data**2 self.assertArrayAllClose(orig_sq_mag, res_sq_mag, rtol=5e-4) def test_data_values(self): @@ -437,9 +437,9 @@ def test_rotated_to_osgb(self): self.assertArrayEqual(expected_mask, vt.data.mask) # Check unmasked values have sufficiently small error in mag. - expected_mag = np.sqrt(u.data ** 2 + v.data ** 2) + expected_mag = np.sqrt(u.data**2 + v.data**2) # Use underlying data to ignore mask in calculation. - res_mag = np.sqrt(ut.data.data ** 2 + vt.data.data ** 2) + res_mag = np.sqrt(ut.data.data**2 + vt.data.data**2) # Calculate percentage error (note there are no zero magnitudes # so we can divide safely). anom = 100.0 * np.abs(res_mag - expected_mag) / expected_mag diff --git a/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py b/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py index f0dba83748..a018507fb3 100644 --- a/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py +++ b/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py @@ -33,7 +33,7 @@ def setUp(self): self.xs, self.ys = np.meshgrid(self.x.points, self.y.points) def transformation(x, y): - return x + y ** 2 + return x + y**2 # Construct a function which adds dimensions to the 2D data array # so that we can test higher dimensional functionality. diff --git a/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py b/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py index b7c59ff566..0c20f16f5a 100644 --- a/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py +++ b/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py @@ -36,7 +36,7 @@ def test_vectorise_call(self): # The reason we use numpy.vectorize is to support multi-dimensional # coordinate points. def fn(coord, v): - return v ** 2 + return v**2 with mock.patch( "numpy.vectorize", return_value=self.vectorised diff --git a/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py b/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py index 0f2a8a2d4b..c9c4821e0a 100644 --- a/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py +++ b/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py @@ -65,7 +65,7 @@ def test_3d(self): def test_multiple_odd_dims(self): # Test to ensure multiple collapsed dimensions don't interfere. # make a 5-D array where dimensions 0, 2 and 3 are degenerate. - array = np.arange(3 ** 5).reshape([3] * 5) + array = np.arange(3**5).reshape([3] * 5) array[1:] = array[0:1] array[:, :, 1:] = array[:, :, 0:1] array[:, :, :, 1:] = array[:, :, :, 0:1] From ca169af6028891ee446150eb1c4d7be44bfeea15 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 1 Feb 2022 16:46:13 +0000 Subject: [PATCH 12/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- benchmarks/benchmarks/generate_data/um_files.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/benchmarks/generate_data/um_files.py b/benchmarks/benchmarks/generate_data/um_files.py index 8ad6777808..fe8bf6a246 100644 --- a/benchmarks/benchmarks/generate_data/um_files.py +++ b/benchmarks/benchmarks/generate_data/um_files.py @@ -107,9 +107,9 @@ def add_field(level_: int, time_step_: int) -> None: six_rec = 20 / 3 three_rec = six_rec / 2 - new_field.blev = level_1 ** 2 * six_rec - six_rec + new_field.blev = level_1**2 * six_rec - six_rec new_field.brsvd1 = ( - level_1 ** 2 * six_rec + (six_rec * level_1) - three_rec + level_1**2 * six_rec + (six_rec * level_1) - three_rec ) brsvd2_simulated = np.linspace(0.995, 0, len_z) From 73b8d843681561996b022bc450ba688a734e8701 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Wed, 2 Feb 2022 14:20:51 +0000 Subject: [PATCH 13/22] Kick Cirrus. From fc9a1cb2d346ec0682148e2ae33524aea310a4cd Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 10 Feb 2022 16:30:54 +0000 Subject: [PATCH 14/22] Revert "Merge remote-tracking branch 'upstream/main' into all_benchmarks" This reverts commit 81c4bcf0a70eca15cb248513a8164a798592a495, reversing changes made to 7561195467ef6fad05e24b5e11f9e6716da3638e. --- .cirrus.yml | 6 + .github/workflows/refresh-lockfiles.yml | 2 +- .pre-commit-config.yaml | 6 +- .readthedocs.yml | 22 +- benchmarks/benchmarks/plot.py | 2 +- docs/gallery_code/README.rst | 2 +- .../meteorology/plot_wind_barbs.py | 4 +- .../meteorology/plot_wind_speed.py | 2 +- docs/src/_templates/imagehash.html | 15 - docs/src/common_links.inc | 1 - docs/src/conf.py | 10 +- .../contributing_documentation.rst | 176 +- .../contributing_documentation_easy.rst | 102 - .../contributing_documentation_full.rst | 173 -- .../contributing_graphics_tests.rst | 2 - .../contributing_testing_index.rst | 1 - .../documenting/whats_new_contributions.rst | 21 +- docs/src/developers_guide/edit_button.png | Bin 92308 -> 0 bytes docs/src/developers_guide/edit_on_github.png | Bin 53986 -> 0 bytes docs/src/developers_guide/find_main.png | Bin 38567 -> 0 bytes docs/src/developers_guide/fork_banner.png | Bin 18871 -> 0 bytes docs/src/developers_guide/imagehash_index.rst | 20 - docs/src/developers_guide/propose_changes.png | Bin 24197 -> 0 bytes docs/src/developers_guide/pull_request.png | Bin 105614 -> 0 bytes docs/src/developers_guide/release.rst | 32 +- docs/src/further_topics/index.rst | 4 +- docs/src/further_topics/lenient_maths.rst | 16 +- docs/src/further_topics/metadata.rst | 11 +- docs/src/further_topics/ugrid/data_model.rst | 566 ----- .../ugrid/images/data_structured_grid.svg | 1 - .../ugrid/images/data_ugrid_mesh.svg | 1 - .../ugrid/images/geovistalogo.svg | 573 ----- .../ugrid/images/iris-esmf-regrid.svg | 93 - .../ugrid/images/plotting_basic.png | Bin 42338 -> 0 bytes .../ugrid/images/plotting_global.png | Bin 133531 -> 0 bytes .../ugrid/images/ugrid_edge_data.svg | 1 - .../ugrid/images/ugrid_element_centres.svg | 1 - .../ugrid/images/ugrid_node_independence.svg | 1 - .../ugrid/images/ugrid_variable_faces.svg | 1 - docs/src/further_topics/ugrid/index.rst | 54 - docs/src/further_topics/ugrid/operations.rst | 995 -------- .../further_topics/ugrid/partner_packages.rst | 100 - docs/src/index.rst | 13 +- docs/src/installing.rst | 10 +- docs/src/sphinxext/image_test_output.py | 78 - docs/src/userguide/cube_maths.rst | 6 +- docs/src/userguide/cube_statistics.rst | 14 +- .../interpolation_and_regridding.rst | 4 +- docs/src/userguide/iris_cubes.rst | 4 +- docs/src/userguide/loading_iris_cubes.rst | 91 +- docs/src/userguide/navigating_a_cube.rst | 8 +- docs/src/userguide/plotting_a_cube.rst | 11 +- docs/src/userguide/saving_iris_cubes.rst | 8 - docs/src/userguide/subsetting_a_cube.rst | 6 +- docs/src/whatsnew/1.4.rst | 3 +- docs/src/whatsnew/3.0.rst | 5 +- docs/src/whatsnew/3.1.rst | 5 +- docs/src/whatsnew/index.rst | 2 +- docs/src/whatsnew/latest.rst | 266 ++- docs/src/whatsnew/latest.rst.template | 112 + etc/cf-standard-name-table.xml | 2033 ++++++++--------- lib/iris/_merge.py | 7 +- lib/iris/_representation/cube_summary.py | 2 +- lib/iris/analysis/__init__.py | 104 +- lib/iris/analysis/_area_weighted.py | 1022 +-------- lib/iris/analysis/_grid_angles.py | 2 +- lib/iris/analysis/_scipy_interpolate.py | 2 +- lib/iris/analysis/calculus.py | 12 +- lib/iris/analysis/cartography.py | 6 +- lib/iris/analysis/maths.py | 32 +- lib/iris/analysis/stats.py | 4 +- lib/iris/common/metadata.py | 2 +- lib/iris/common/resolve.py | 40 +- lib/iris/coord_systems.py | 5 +- lib/iris/coords.py | 419 +--- lib/iris/cube.py | 103 +- lib/iris/experimental/equalise_cubes.py | 30 + lib/iris/experimental/raster.py | 24 - lib/iris/experimental/regrid.py | 1102 ++++++++- lib/iris/experimental/regrid_conservative.py | 44 - lib/iris/experimental/ugrid/load.py | 22 +- lib/iris/experimental/ugrid/mesh.py | 645 +++--- lib/iris/experimental/ugrid/metadata.py | 13 +- lib/iris/fileformats/__init__.py | 13 +- lib/iris/fileformats/_ff_cross_references.py | 6 +- .../fileformats/_nc_load_rules/actions.py | 21 +- lib/iris/fileformats/abf.py | 9 - lib/iris/fileformats/dot.py | 9 - lib/iris/fileformats/netcdf.py | 22 +- lib/iris/fileformats/pp.py | 4 +- lib/iris/fileformats/um_cf_map.py | 10 +- lib/iris/io/__init__.py | 16 +- lib/iris/plot.py | 14 +- lib/iris/tests/integration/test_netcdf.py | 6 +- lib/iris/tests/integration/test_trajectory.py | 16 +- .../0d_str.txt | 2 +- .../1d_str.txt | 2 +- .../2d_str.txt | 2 +- .../3d_str.txt | 2 +- .../4d_str.txt | 2 +- .../results/cdm/str_repr/0d_cube.__str__.txt | 2 +- .../cdm/str_repr/0d_cube.__unicode__.txt | 2 +- .../cdm/str_repr/cell_methods.__str__.txt | 2 +- .../cdm/str_repr/missing_coords_cube.str.txt | 2 +- .../results/cdm/str_repr/similar.__str__.txt | 2 +- .../unicode_attribute.__unicode__.txt | 2 +- .../coord_api/str_repr/aux_nontime_repr.txt | 12 +- .../coord_api/str_repr/aux_nontime_str.txt | 26 +- .../coord_api/str_repr/aux_time_repr.txt | 3 +- .../coord_api/str_repr/aux_time_str.txt | 7 +- .../coord_api/str_repr/dim_nontime_repr.txt | 12 +- .../coord_api/str_repr/dim_nontime_str.txt | 25 +- .../coord_api/str_repr/dim_time_repr.txt | 3 +- .../coord_api/str_repr/dim_time_str.txt | 9 +- .../tests/results/derived/no_orog.__str__.txt | 2 +- .../results/derived/removed_orog.__str__.txt | 2 +- .../results/derived/removed_sigma.__str__.txt | 2 +- .../trajectory/tri_polar_latitude_slice.cml | 45 +- .../util/as_compatible_shape_collapsed.cml | 144 ++ lib/iris/tests/test_basic_maths.py | 38 +- lib/iris/tests/test_coord_api.py | 146 +- lib/iris/tests/test_coordsystem.py | 23 - lib/iris/tests/test_file_save.py | 18 - lib/iris/tests/test_io_init.py | 49 +- lib/iris/tests/test_load.py | 37 - lib/iris/tests/test_mapping.py | 5 +- lib/iris/tests/test_pickling.py | 11 +- lib/iris/tests/test_util.py | 98 + .../test_AreaWeightedRegridder.py | 17 +- .../unit/analysis/cartography/test_project.py | 2 +- .../cartography/test_rotate_grid_vectors.py | 6 +- .../analysis/cartography/test_rotate_winds.py | 8 +- .../regrid/test_RectilinearRegridder.py | 2 +- .../unit/common/mixin/test_CFVariableMixin.py | 6 +- .../test_add_categorised_coord.py | 2 +- .../unit/coords/test_AncillaryVariable.py | 49 +- .../tests/unit/coords/test_CellMeasure.py | 33 +- lib/iris/tests/unit/coords/test_Coord.py | 60 +- .../unit/coords/test__DimensionalMetadata.py | 1058 +-------- lib/iris/tests/unit/cube/test_Cube.py | 26 +- .../ugrid/mesh/test_Connectivity.py | 99 +- .../unit/experimental/ugrid/mesh/test_Mesh.py | 269 +-- .../experimental/ugrid/mesh/test_MeshCoord.py | 130 +- .../ugrid/mesh/test_Mesh__from_coords.py | 2 +- .../metadata/test_ConnectivityMetadata.py | 46 +- .../nc_load_rules/actions/__init__.py | 3 + .../actions/test__hybrid_formulae.py | 99 +- .../actions/test__latlon_dimcoords.py | 337 --- .../fileformats/netcdf/test_Saver__ugrid.py | 4 +- ...__collapse_degenerate_points_and_bounds.py | 2 +- .../tests/unit/io/test__generate_cubes.py | 37 - lib/iris/tests/unit/io/test_save.py | 45 - .../cube_printout/test_CubePrintout.py | 6 +- .../cube_summary/test_CubeSummary.py | 6 +- lib/iris/util.py | 184 +- noxfile.py | 11 +- pyproject.toml | 2 +- requirements/ci/nox.lock/py37-linux-64.lock | 231 ++ requirements/ci/py37.yml | 48 + requirements/ci/py38.yml | 1 + setup.cfg | 3 +- tools/gen_helpers.py | 36 + tools/gen_stash_refs.py | 126 + tools/gen_translations.py | 216 ++ tools/generate_std_names.py | 4 +- tools/translator/__init__.py | 1116 +++++++++ 166 files changed, 5817 insertions(+), 8738 deletions(-) delete mode 100644 docs/src/_templates/imagehash.html delete mode 100755 docs/src/developers_guide/contributing_documentation_easy.rst delete mode 100755 docs/src/developers_guide/contributing_documentation_full.rst delete mode 100755 docs/src/developers_guide/edit_button.png delete mode 100755 docs/src/developers_guide/edit_on_github.png delete mode 100755 docs/src/developers_guide/find_main.png delete mode 100755 docs/src/developers_guide/fork_banner.png delete mode 100644 docs/src/developers_guide/imagehash_index.rst delete mode 100755 docs/src/developers_guide/propose_changes.png delete mode 100755 docs/src/developers_guide/pull_request.png delete mode 100644 docs/src/further_topics/ugrid/data_model.rst delete mode 100644 docs/src/further_topics/ugrid/images/data_structured_grid.svg delete mode 100644 docs/src/further_topics/ugrid/images/data_ugrid_mesh.svg delete mode 100644 docs/src/further_topics/ugrid/images/geovistalogo.svg delete mode 100644 docs/src/further_topics/ugrid/images/iris-esmf-regrid.svg delete mode 100644 docs/src/further_topics/ugrid/images/plotting_basic.png delete mode 100644 docs/src/further_topics/ugrid/images/plotting_global.png delete mode 100644 docs/src/further_topics/ugrid/images/ugrid_edge_data.svg delete mode 100644 docs/src/further_topics/ugrid/images/ugrid_element_centres.svg delete mode 100644 docs/src/further_topics/ugrid/images/ugrid_node_independence.svg delete mode 100644 docs/src/further_topics/ugrid/images/ugrid_variable_faces.svg delete mode 100644 docs/src/further_topics/ugrid/index.rst delete mode 100644 docs/src/further_topics/ugrid/operations.rst delete mode 100644 docs/src/further_topics/ugrid/partner_packages.rst delete mode 100644 docs/src/sphinxext/image_test_output.py mode change 120000 => 100644 docs/src/whatsnew/latest.rst create mode 100644 docs/src/whatsnew/latest.rst.template create mode 100644 lib/iris/experimental/equalise_cubes.py create mode 100644 lib/iris/tests/results/util/as_compatible_shape_collapsed.cml delete mode 100644 lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__latlon_dimcoords.py delete mode 100755 lib/iris/tests/unit/io/test__generate_cubes.py delete mode 100755 lib/iris/tests/unit/io/test_save.py create mode 100644 requirements/ci/nox.lock/py37-linux-64.lock create mode 100644 requirements/ci/py37.yml create mode 100644 tools/gen_helpers.py create mode 100644 tools/gen_stash_refs.py create mode 100644 tools/gen_translations.py create mode 100644 tools/translator/__init__.py diff --git a/.cirrus.yml b/.cirrus.yml index 92b8d788e6..b3992de64a 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -60,6 +60,7 @@ linux_task_template: &LINUX_TASK_TEMPLATE - echo "$(date +%Y).$(expr $(date +%U) / ${CACHE_PERIOD}):${CONDA_CACHE_BUILD}" - uname -r populate_script: + - export CONDA_OVERRIDE_LINUX="$(uname -r | cut -d'+' -f1)" - bash miniconda.sh -b -p ${HOME}/miniconda - conda config --set always_yes yes --set changeps1 no - conda config --set show_channel_urls True @@ -140,6 +141,8 @@ task: only_if: ${SKIP_TEST_TASK} == "" << : *CREDITS_TEMPLATE matrix: + env: + PY_VER: 3.7 env: PY_VER: 3.8 name: "${CIRRUS_OS}: py${PY_VER} tests" @@ -150,6 +153,7 @@ task: << : *IRIS_TEST_DATA_TEMPLATE << : *LINUX_TASK_TEMPLATE tests_script: + - export CONDA_OVERRIDE_LINUX="$(uname -r | cut -d'+' -f1)" - echo "[Resources]" > ${SITE_CFG} - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG} @@ -170,6 +174,7 @@ task: << : *IRIS_TEST_DATA_TEMPLATE << : *LINUX_TASK_TEMPLATE tests_script: + - export CONDA_OVERRIDE_LINUX="$(uname -r | cut -d'+' -f1)" - echo "[Resources]" > ${SITE_CFG} - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG} @@ -192,6 +197,7 @@ task: name: "${CIRRUS_OS}: py${PY_VER} link check" << : *LINUX_TASK_TEMPLATE tests_script: + - export CONDA_OVERRIDE_LINUX="$(uname -r | cut -d'+' -f1)" - mkdir -p ${MPL_RC_DIR} - echo "backend : agg" > ${MPL_RC_FILE} - echo "image.cmap : viridis" >> ${MPL_RC_FILE} diff --git a/.github/workflows/refresh-lockfiles.yml b/.github/workflows/refresh-lockfiles.yml index f7fa10069f..32c347320c 100644 --- a/.github/workflows/refresh-lockfiles.yml +++ b/.github/workflows/refresh-lockfiles.yml @@ -71,7 +71,7 @@ jobs: strategy: matrix: - python: ['38'] + python: ['37', '38'] steps: - uses: actions/checkout@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 228970bee2..9af8d08a70 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ minimum_pre_commit_version: 1.21.0 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.0.1 hooks: # Prevent giant files from being committed. - id: check-added-large-files @@ -29,7 +29,7 @@ repos: - id: no-commit-to-branch - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 21.11b1 hooks: - id: black pass_filenames: false @@ -50,7 +50,7 @@ repos: args: [--filter-files] - repo: https://github.com/asottile/blacken-docs - rev: v1.12.1 + rev: v1.12.0 hooks: - id: blacken-docs types: [file, rst] diff --git a/.readthedocs.yml b/.readthedocs.yml index 63c4798050..b54b0f065b 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,20 +1,20 @@ version: 2 build: - os: ubuntu-20.04 - tools: - python: mambaforge-4.10 + image: latest conda: - environment: requirements/ci/readthedocs.yml + environment: requirements/ci/readthedocs.yml sphinx: - configuration: docs/src/conf.py - fail_on_warning: false + configuration: docs/src/conf.py + fail_on_warning: false python: - install: - - method: pip - path: . - extra_requirements: - - docs + install: + - method: setuptools + path: . + +formats: + - htmlzip + - pdf diff --git a/benchmarks/benchmarks/plot.py b/benchmarks/benchmarks/plot.py index 24899776dc..45905abd2f 100644 --- a/benchmarks/benchmarks/plot.py +++ b/benchmarks/benchmarks/plot.py @@ -22,7 +22,7 @@ def setup(self): # Should generate 10 distinct contours, regardless of dim size. dim_size = int(ARTIFICIAL_DIM_SIZE / 5) repeat_number = int(dim_size / 10) - repeat_range = range(int((dim_size**2) / repeat_number)) + repeat_range = range(int((dim_size ** 2) / repeat_number)) data = np.repeat(repeat_range, repeat_number) data = data.reshape((dim_size,) * 2) diff --git a/docs/gallery_code/README.rst b/docs/gallery_code/README.rst index 720fd1e6f6..02263dc5e5 100644 --- a/docs/gallery_code/README.rst +++ b/docs/gallery_code/README.rst @@ -23,4 +23,4 @@ you may start the jupyter notebook via:: If you wish to contribute to the gallery see the :ref:`contributing.documentation.gallery` section of the -:ref:`contributing.documentation_full`. +:ref:`contributing.documentation`. diff --git a/docs/gallery_code/meteorology/plot_wind_barbs.py b/docs/gallery_code/meteorology/plot_wind_barbs.py index b09040c64e..c3c056eb4a 100644 --- a/docs/gallery_code/meteorology/plot_wind_barbs.py +++ b/docs/gallery_code/meteorology/plot_wind_barbs.py @@ -30,7 +30,7 @@ def main(): # To illustrate the full range of barbs, scale the wind speed up to pretend # that a storm is passing over - magnitude = (uwind**2 + vwind**2) ** 0.5 + magnitude = (uwind ** 2 + vwind ** 2) ** 0.5 magnitude.convert_units("knot") max_speed = magnitude.collapsed( ("latitude", "longitude"), iris.analysis.MAX @@ -41,7 +41,7 @@ def main(): vwind = vwind / max_speed * max_desired # Create a cube containing the wind speed - windspeed = (uwind**2 + vwind**2) ** 0.5 + windspeed = (uwind ** 2 + vwind ** 2) ** 0.5 windspeed.rename("windspeed") windspeed.convert_units("knot") diff --git a/docs/gallery_code/meteorology/plot_wind_speed.py b/docs/gallery_code/meteorology/plot_wind_speed.py index 40d9d0da00..fd03f54205 100644 --- a/docs/gallery_code/meteorology/plot_wind_speed.py +++ b/docs/gallery_code/meteorology/plot_wind_speed.py @@ -27,7 +27,7 @@ def main(): vwind = iris.load_cube(infile, "y_wind") # Create a cube containing the wind speed. - windspeed = (uwind**2 + vwind**2) ** 0.5 + windspeed = (uwind ** 2 + vwind ** 2) ** 0.5 windspeed.rename("windspeed") # Plot the wind speed as a contour plot. diff --git a/docs/src/_templates/imagehash.html b/docs/src/_templates/imagehash.html deleted file mode 100644 index 8b0dac0cce..0000000000 --- a/docs/src/_templates/imagehash.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "!layout.html" %} - -{% block body %} - -

Test: {{ test }}

- - -{% for hash, file in hashfiles %} -
-

{{hash}}

- -
-{% endfor %} - -{% endblock %} \ No newline at end of file diff --git a/docs/src/common_links.inc b/docs/src/common_links.inc index 67fc493e3e..eb1ea60b7a 100644 --- a/docs/src/common_links.inc +++ b/docs/src/common_links.inc @@ -37,7 +37,6 @@ .. _test-iris-imagehash: https://github.com/SciTools/test-iris-imagehash .. _using git: https://docs.github.com/en/github/using-git .. _requirements/ci/: https://github.com/SciTools/iris/tree/main/requirements/ci -.. _CF-UGRID: https://ugrid-conventions.github.io/ugrid-conventions/ .. comment diff --git a/docs/src/conf.py b/docs/src/conf.py index 19f22e808f..e13f12a13a 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -158,7 +158,6 @@ def _dotv(version): "sphinx_gallery.gen_gallery", "matplotlib.sphinxext.mathmpl", "matplotlib.sphinxext.plot_directive", - "image_test_output", ] if skip_api == "1": @@ -200,9 +199,7 @@ def _dotv(version): # -- copybutton extension ----------------------------------------------------- # See https://sphinx-copybutton.readthedocs.io/en/latest/ -copybutton_prompt_text = r">>> |\.\.\. " -copybutton_prompt_is_regexp = True -copybutton_line_continuation_character = "\\" +copybutton_prompt_text = ">>> " # sphinx.ext.todo configuration ----------------------------------------------- # See https://www.sphinx-doc.org/en/master/usage/extensions/todo.html @@ -211,7 +208,6 @@ def _dotv(version): # api generation configuration autodoc_member_order = "groupwise" autodoc_default_flags = ["show-inheritance"] -autodoc_typehints = "none" autosummary_generate = True autosummary_imported_members = True autopackage_name = ["iris"] @@ -331,10 +327,8 @@ def _dotv(version): "gallery_dirs": ["generated/gallery"], # filename pattern for the files in the gallery "filename_pattern": "/plot_", - # filename pattern to ignore in the gallery + # filename patternt to ignore in the gallery "ignore_pattern": r"__init__\.py", - # force gallery building, unless overridden (see src/Makefile) - "plot_gallery": "'True'", } # ----------------------------------------------------------------------------- diff --git a/docs/src/developers_guide/contributing_documentation.rst b/docs/src/developers_guide/contributing_documentation.rst index e289b1548d..efd31d4f20 100644 --- a/docs/src/developers_guide/contributing_documentation.rst +++ b/docs/src/developers_guide/contributing_documentation.rst @@ -1,21 +1,173 @@ -How to Contribute to the Documentation --------------------------------------- +.. _contributing.documentation: + +Contributing to the Documentation +--------------------------------- Documentation is important and we encourage any improvements that can be made. If you believe the documentation is not clear please contribute a change to improve the documentation for all users. -If you're confident diving right in, please head for -:ref:`contributing.documentation_full`. +Any change to the Iris project whether it is a bugfix, new feature or +documentation update must use the :ref:`development-workflow`. + + +Requirements +~~~~~~~~~~~~ + +The documentation uses specific packages that need to be present. Please see +:ref:`installing_iris` for instructions. + + +.. _contributing.documentation.building: + +Building +~~~~~~~~ + +This documentation was built using the latest Python version that Iris +supports. For more information see :ref:`installing_iris`. + +The build can be run from the documentation directory ``docs/src``. + +The build output for the html is found in the ``_build/html`` sub directory. +When updating the documentation ensure the html build has *no errors* or +*warnings* otherwise it may fail the automated `cirrus-ci`_ build. + +Once the build is complete, if it is rerun it will only rebuild the impacted +build artefacts so should take less time. + +There is an option to perform a build but skip the +:ref:`contributing.documentation.gallery` creation completely. This can be +achieved via:: + + make html-noplot + +Another option is to skip the :ref:`iris` documentation creation. This can be +useful as it reduces the time to build the documentation, however you may have +some build warnings as there maybe references to the API documentation. +This can be achieved via:: + + make html-noapi + +You can combine both the above and skip the +:ref:`contributing.documentation.gallery` and :ref:`iris` documentation +completely. This can be achieved via:: + + make html-quick + +If you wish to run a full clean build you can run:: + + make clean + make html + +This is useful for a final test before committing your changes. + +.. note:: In order to preserve a clean build for the html, all **warnings** + have been promoted to be **errors** to ensure they are addressed. + This **only** applies when ``make html`` is run. + +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris + +.. _contributing.documentation.testing: + +Testing +~~~~~~~ + +There are a ways to test various aspects of the documentation. The +``make`` commands shown below can be run in the ``docs`` or +``docs/src`` directory. + +Each :ref:`contributing.documentation.gallery` entry has a corresponding test. +To run the tests:: + + make gallerytest + +Many documentation pages includes python code itself that can be run to ensure +it is still valid or to demonstrate examples. To ensure these tests pass +run:: + + make doctest + +See :data:`iris.cube.Cube.data` for an example of using the `doctest`_ +approach. + +.. _doctest: http://www.sphinx-doc.org/en/stable/ext/doctest.html + +The hyperlinks in the documentation can be checked automatically. +If there is a link that is known to work it can be excluded from the checks by +adding it to the ``linkcheck_ignore`` array that is defined in the +`conf.py`_. The hyperlink check can be run via:: + + make linkcheck + +If this fails check the output for the text **broken** and then correct +or ignore the url. + +.. comment + Finally, the spelling in the documentation can be checked automatically via the + command:: + + make spelling + + The spelling check may pull up many technical abbreviations and acronyms. This + can be managed by using an **allow** list in the form of a file. This file, + or list of files is set in the `conf.py`_ using the string list + ``spelling_word_list_filename``. + + +.. note:: In addition to the automated `cirrus-ci`_ build of all the + documentation build options above, the + https://readthedocs.org/ service is also used. The configuration + of this held in a file in the root of the + `github Iris project `_ named + ``.readthedocs.yml``. + + +.. _conf.py: https://github.com/SciTools/iris/blob/main/docs/src/conf.py + + +.. _contributing.documentation.api: + +Generating API Documentation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to auto generate the API documentation based upon the docstrings a +custom set of python scripts are used, these are located in the directory +``docs/src/sphinxext``. Once the ``make html`` command has been run, +the output of these scripts can be found in +``docs/src/generated/api``. + +If there is a particularly troublesome module that breaks the ``make html`` you +can exclude the module from the API documentation. Add the entry to the +``exclude_modules`` tuple list in the +``docs/src/sphinxext/generate_package_rst.py`` file. + + +.. _contributing.documentation.gallery: + +Gallery +~~~~~~~ + +The Iris :ref:`sphx_glr_generated_gallery` uses a sphinx extension named +`sphinx-gallery `_ +that auto generates reStructuredText (rst) files based upon a gallery source +directory that abides directory and filename convention. + +The code for the gallery entries are in ``docs/gallery_code``. +Each sub directory in this directory is a sub section of the gallery. The +respective ``README.rst`` in each folder is included in the gallery output. + +For each gallery entry there must be a corresponding test script located in +``docs/gallery_tests``. -If you're not then we've got a step-by-step guide here to walk you through it: -:ref:`contributing.documentation_easy` +To add an entry to the gallery simple place your python code into the +appropriate sub directory and name it with a prefix of ``plot_``. If your +gallery entry does not fit into any existing sub directories then create a new +directory and place it in there. -.. toctree:: - :maxdepth: 1 - :hidden: +The reStructuredText (rst) output of the gallery is located in +``docs/src/generated/gallery``. - contributing_documentation_easy - contributing_documentation_full - \ No newline at end of file +For more information on the directory structure and options please see the +`sphinx-gallery getting started +`_ documentation. diff --git a/docs/src/developers_guide/contributing_documentation_easy.rst b/docs/src/developers_guide/contributing_documentation_easy.rst deleted file mode 100755 index f54de628bf..0000000000 --- a/docs/src/developers_guide/contributing_documentation_easy.rst +++ /dev/null @@ -1,102 +0,0 @@ - -.. _contributing.documentation_easy: - -Contributing to the Documentation (the easy way) ------------------------------------------------- - -Documentation is important and we encourage any improvements that can be made. -If you believe the documentation is not clear please contribute a change to -improve the documentation for all users. - -The guide below is designed to be accessible to those with little-to-no -knowledge of programming and GitHub. If you find that something doesn't work as -described or could use more explanation then please let us know (or contribute -the improvement yourself)! - -First Time Only Steps -^^^^^^^^^^^^^^^^^^^^^ - -1. Create a `GitHub `_ account. - -2. Complete the Scitools Contributor License Agreement (`link to Google Form - `_). - This is a one-off requirement for anyone who wishes to contribute to a - Scitools repository - including the documentation. - -Steps to Complete Each Time You Propose Changes -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -1. Navigate to the documentation page that you want to edit (on this site). - -2. Click the ``Edit on GitHub`` button at the **top right** of the page. - -.. image:: edit_on_github.png - -3. In the resulting GitHub page select **main** from the ``Switch - branches/tags`` drop-down menu near the **top left** of the page (to the left - of the ``iris / docs / src / ...`` links) if it isn't already. This changes - the branch to **main**. - -.. image:: find_main.png - -4. Click the pencil symbol near the **top right** (to the right of the ``Raw`` - and ``Blame`` buttons). - -.. image:: edit_button.png - -5. Make your edits! Try to strike a balance between informing the audience - enough that they understand and overwhelming them with information. - -.. note:: - - You may see the following message at the top of the edit page, informing you - that GitHub has created you your own ``fork`` (or copy) of the project as a - precursor to allowing you to edit the page. Your changes will be merged into - the main version of the documentation later. - - .. image:: fork_banner.png - -6. Scroll to the bottom of the edit page and enter some appropriate information - in the two boxes under ``Propose changes``. You can just keep the default text - if you like or enter something more specific - a short sentence explaining - what's changed is fine. Then click the ``Propose changes`` button. - -.. image:: propose_changes.png - -7. In the resulting page titled ``Pull Request``, write a brief description of - what you've changed underneath the following three lines: - -.. code:: - - ### Description - - - -Describing what you've changed and why will help the person who reviews your changes. - -.. image:: pull_request.png - -8. Click the ``Create pull request`` button. - -.. tip:: - - If you're not sure that you're making your pull request right, or have a - question, then make it anyway! You can then comment on it tagging - ``@SciTools/iris-devs`` to ask your question (then edit your pull request if - you need to). - -What Happens Next? -^^^^^^^^^^^^^^^^^^ - -Another Iris contributor will review your changes (this happens for everyone who -makes changes to Iris or its documentation). The reviewer might make comments or -ask questions (don't worry about missing these, GitHub will email you to let you -know). You can respond to these comments underneath where they appear in GitHub. - -Once you've worked everything out together, the reviewer will merge your changes -into the main version of the documentation so that they're accessible for -everyone to benefit from. - -**You've now contributed to the Iris documentation!** If you've caught the bug -and want to get more involved (or you're just interested what that would mean) -then chat to the person reviewing your code or another Iris contributor. \ No newline at end of file diff --git a/docs/src/developers_guide/contributing_documentation_full.rst b/docs/src/developers_guide/contributing_documentation_full.rst deleted file mode 100755 index 77b898c0f3..0000000000 --- a/docs/src/developers_guide/contributing_documentation_full.rst +++ /dev/null @@ -1,173 +0,0 @@ - -.. _contributing.documentation_full: - -Contributing to the Documentation ---------------------------------- - -This guide is for those comfortable with the development process, looking for -the specifics of how to apply that knowledge to Iris. You may instead find it -easier to use the :ref:`contributing.documentation_easy`. - -Any change to the Iris project whether it is a bugfix, new feature or -documentation update must use the :ref:`development-workflow`. - - -Requirements -~~~~~~~~~~~~ - -The documentation uses specific packages that need to be present. Please see -:ref:`installing_iris` for instructions. - - -.. _contributing.documentation.building: - -Building -~~~~~~~~ - -This documentation was built using the latest Python version that Iris -supports. For more information see :ref:`installing_iris`. - -The build can be run from the documentation directory ``docs/src``. - -The build output for the html is found in the ``_build/html`` sub directory. -When updating the documentation ensure the html build has *no errors* or -*warnings* otherwise it may fail the automated `cirrus-ci`_ build. - -Once the build is complete, if it is rerun it will only rebuild the impacted -build artefacts so should take less time. - -There is an option to perform a build but skip the -:ref:`contributing.documentation.gallery` creation completely. This can be -achieved via:: - - make html-noplot - -Another option is to skip the :ref:`iris` documentation creation. This can be -useful as it reduces the time to build the documentation, however you may have -some build warnings as there maybe references to the API documentation. -This can be achieved via:: - - make html-noapi - -You can combine both the above and skip the -:ref:`contributing.documentation.gallery` and :ref:`iris` documentation -completely. This can be achieved via:: - - make html-quick - -If you wish to run a full clean build you can run:: - - make clean - make html - -This is useful for a final test before committing your changes. - -.. note:: In order to preserve a clean build for the html, all **warnings** - have been promoted to be **errors** to ensure they are addressed. - This **only** applies when ``make html`` is run. - -.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris - -.. _contributing.documentation.testing: - -Testing -~~~~~~~ - -There are a ways to test various aspects of the documentation. The -``make`` commands shown below can be run in the ``docs`` or -``docs/src`` directory. - -Each :ref:`contributing.documentation.gallery` entry has a corresponding test. -To run the tests:: - - make gallerytest - -Many documentation pages includes python code itself that can be run to ensure -it is still valid or to demonstrate examples. To ensure these tests pass -run:: - - make doctest - -See :data:`iris.cube.Cube.data` for an example of using the `doctest`_ -approach. - -.. _doctest: http://www.sphinx-doc.org/en/stable/ext/doctest.html - -The hyperlinks in the documentation can be checked automatically. -If there is a link that is known to work it can be excluded from the checks by -adding it to the ``linkcheck_ignore`` array that is defined in the -`conf.py`_. The hyperlink check can be run via:: - - make linkcheck - -If this fails check the output for the text **broken** and then correct -or ignore the url. - -.. comment - Finally, the spelling in the documentation can be checked automatically via the - command:: - - make spelling - - The spelling check may pull up many technical abbreviations and acronyms. This - can be managed by using an **allow** list in the form of a file. This file, - or list of files is set in the `conf.py`_ using the string list - ``spelling_word_list_filename``. - - -.. note:: In addition to the automated `cirrus-ci`_ build of all the - documentation build options above, the - https://readthedocs.org/ service is also used. The configuration - of this held in a file in the root of the - `github Iris project `_ named - ``.readthedocs.yml``. - - -.. _conf.py: https://github.com/SciTools/iris/blob/main/docs/src/conf.py - - -.. _contributing.documentation.api: - -Generating API Documentation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In order to auto generate the API documentation based upon the docstrings a -custom set of python scripts are used, these are located in the directory -``docs/src/sphinxext``. Once the ``make html`` command has been run, -the output of these scripts can be found in -``docs/src/generated/api``. - -If there is a particularly troublesome module that breaks the ``make html`` you -can exclude the module from the API documentation. Add the entry to the -``exclude_modules`` tuple list in the -``docs/src/sphinxext/generate_package_rst.py`` file. - - -.. _contributing.documentation.gallery: - -Gallery -~~~~~~~ - -The Iris :ref:`sphx_glr_generated_gallery` uses a sphinx extension named -`sphinx-gallery `_ -that auto generates reStructuredText (rst) files based upon a gallery source -directory that abides directory and filename convention. - -The code for the gallery entries are in ``docs/gallery_code``. -Each sub directory in this directory is a sub section of the gallery. The -respective ``README.rst`` in each folder is included in the gallery output. - -For each gallery entry there must be a corresponding test script located in -``docs/gallery_tests``. - -To add an entry to the gallery simple place your python code into the -appropriate sub directory and name it with a prefix of ``plot_``. If your -gallery entry does not fit into any existing sub directories then create a new -directory and place it in there. - -The reStructuredText (rst) output of the gallery is located in -``docs/src/generated/gallery``. - -For more information on the directory structure and options please see the -`sphinx-gallery getting started -`_ documentation. diff --git a/docs/src/developers_guide/contributing_graphics_tests.rst b/docs/src/developers_guide/contributing_graphics_tests.rst index 1268aa2686..53e895f440 100644 --- a/docs/src/developers_guide/contributing_graphics_tests.rst +++ b/docs/src/developers_guide/contributing_graphics_tests.rst @@ -63,8 +63,6 @@ This consists of: developer to easily compare proposed new **acceptable** result images against the existing accepted reference images, for each failing test. -The acceptable images for each test can be viewed online. The :ref:`testing.imagehash_index` lists all the graphical tests in the test suite and -shows the known acceptable result images for comparison. Reviewing Failing Tests ======================= diff --git a/docs/src/developers_guide/contributing_testing_index.rst b/docs/src/developers_guide/contributing_testing_index.rst index c5cf1b997b..517111507b 100644 --- a/docs/src/developers_guide/contributing_testing_index.rst +++ b/docs/src/developers_guide/contributing_testing_index.rst @@ -8,6 +8,5 @@ Testing contributing_testing contributing_graphics_tests - imagehash_index contributing_running_tests contributing_ci_tests diff --git a/docs/src/developers_guide/documenting/whats_new_contributions.rst b/docs/src/developers_guide/documenting/whats_new_contributions.rst index 576fc5f6a6..ebb553024b 100644 --- a/docs/src/developers_guide/documenting/whats_new_contributions.rst +++ b/docs/src/developers_guide/documenting/whats_new_contributions.rst @@ -4,21 +4,16 @@ Contributing a "What's New" Entry ================================= -Iris uses a file named ``dev.rst`` to keep a draft of upcoming development changes -that will form the next stable release. Contributions to the :ref:`iris_whatsnew` +Iris uses a file named ``latest.rst`` to keep a draft of upcoming changes +that will form the next release. Contributions to the :ref:`iris_whatsnew` document are written by the developer most familiar with the change made. The contribution should be included as part of the Iris Pull Request that introduces the change. -The ``dev.rst`` and the past release notes are kept in the -``docs/src/whatsnew/`` directory. If you are writing the first contribution after -an Iris release: **create the new** ``dev.rst`` by copying the content from -``dev.rst.template`` in the same directory. - -.. note:: - - Ensure that the symbolic link ``latest.rst`` references the ``dev.rst`` file - within the ``docs/src/whatsnew`` directory. +The ``latest.rst`` and the past release notes are kept in +``docs/src/whatsnew/``. If you are writing the first contribution after +an Iris release: **create the new** ``latest.rst`` by copying the content from +``latest.rst.template`` in the same directory. Since the `Contribution categories`_ include Internal changes, **all** Iris Pull Requests should be accompanied by a "What's New" contribution. @@ -27,7 +22,7 @@ Pull Requests should be accompanied by a "What's New" contribution. Git Conflicts ============= -If changes to ``dev.rst`` are being suggested in several simultaneous +If changes to ``latest.rst`` are being suggested in several simultaneous Iris Pull Requests, Git will likely encounter merge conflicts. If this situation is thought likely (large PR, high repo activity etc.): @@ -48,7 +43,7 @@ situation is thought likely (large PR, high repo activity etc.): * PR reviewer: review the "What's New" PR, merge once acceptable -These measures should mean the suggested ``dev.rst`` changes are outstanding +These measures should mean the suggested ``latest.rst`` changes are outstanding for the minimum time, minimising conflicts and minimising the need to rebase or merge from trunk. diff --git a/docs/src/developers_guide/edit_button.png b/docs/src/developers_guide/edit_button.png deleted file mode 100755 index ee2e7858f051aaa63af6ac8f23a8954603ec8251..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 92308 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 diff --git a/docs/src/developers_guide/edit_on_github.png b/docs/src/developers_guide/edit_on_github.png deleted file mode 100755 index f802ebd9d63bccd37dfd5f50bfed425d2aa2e096..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53986 zcmc$_cTkf}^goIP5s{*ZNEekRAib9eB1jVj=~WSt8hQ^=dXI|qCLkasbV5(0*HEN~ zCM|SA3nh@`7hm7^e(!JQ_s5+(cjoR4!#yQq8hBB=#k1LGn6yWp;?rc72kz`9O)xNNJWtwcsv0iim7O-_2Iba`g% zPDXZ{>(Ac>b=?QMWMsd*)m4;U`B-cD z-o)uy?HUD*=seYRRi!3Qjmun7D&#iQIhSqXFSfiXr&GG8@#@)Kx(h9LF1Jdw&^cca z3DVBXf0|qPgXsNk&V8U?E=i0>CaEWV^;DjtJ4I~5drr)D4wK@`gLy(4*`M-3KuP$* z-zu4Lu+^VQ{VA1KS}y;sk-eo3qWrho&3u{u-|8ocE4Q?ohy)4{*aws0eKxlPS{58C+;|y;waeTVBiSEYV5-(bBwTP!x^l3b1v$-AdZ^4fMh4)%0 zIGPT28hhJQj*elR|NbJKjAhUY*lh_1Vt<8R{+E6JWP3~d8Arfl@0o|# znU$rW#~fn$mN@_GVHQpr`|-S?sx8+f6e;-p!^cQUfSq_y3a|^=AgnR&kB+!Vd3`tq*>sw%&JA>dAX?k3wC3_ z4#R<_H$5*s>pGC}2sbp7p5$~G?aU!k=Ho1~N=SZFZbE*&c_G_lGd;)EdeymxT57uf z%|>0t(*w!kHuwJd9V218nG-t)nul#r=ghR!O4+*oxr(PtXvde~1#@lK<6E(fwl)O; z>p(HRLIfAPW3nQAEslJ8{#BUmsNjKUwVnr7)&&#Qwy`UoQvWcVw79{R;a2y!AXh;! zXRQ9j>>a(PSE}9LF4IrNTeTh~T0+YB1epy{F3N0~H%Fuov@X+6vu8*jjfYMsPi=2%av=Nzf2!>nSMRd>LO_);ES)&V-UoTqN;!yxWgccv5^c|X?56Ic`w|D z@5;kmo59@Tf?1EfNpXpFJvyxrHHl>DdTfte>1m6j!$OE~@}0Ufc7(s<@D^iWF^x`S zplx8?=>o+L95CK`XoCxmoR>^ujBVW9yR@4Db@Yx!rRABNL(65qJMC6E z2cf4n-U=@OyLTi1XHZ9YC8k^sg8|DI0cRO#PN}%G-QupqH7>J$&R(N3H08)Dr}#B)GH1OwuDGpjgDfbLzBoUYMb1e(iSWhhUc?oV z4j0xAd2F1$fgL z#(SSquC5)jn!Iy(g0OvM(-T0p_#PjV6y@ zMsM=NjOPRNLFim|xqFgcM>cPktMa=H^rz3W2%8f%NaAxsP1S^*rih*3oWny&)V0<2 zaV~GOqhQS1eKWVVHWpg%_FziuT!G&1yUZL-!!XBQEbe?z22_ z`)9>gW1?Tj*OkV3m+RbdrT!<}^D=M{_I9oIxMA9&jmVBVmw|F{rnl?Eq4?H_qP~?h}^OVYa#Bngq_+l`0fw0 z*A=4f%qU9;Wa*C9T2=E{@^(n$!wqj43;GJsYuDj+^63@LF4ab--@qdUgm6PEeN;gr zv@%4Ly-$K+EB82#@qRzV{M|l!I6t(aa3`hFc^(LjjImxaV_O@#7_gLpCLh@uO62Nw z+Gx@jB6hxJX@2;`ERTn#z#wNbs^eHEgO|~Jvv~!Ak!A)P8g6xMRQl(bn#emXP?Z^} z(UI4=!B`j2aywIsm~gXNbRo# zlvEFaA zP4uTcti`jBl;r5$&?2ez9HB9*#p6qxr*0@bZKrIL$03{o2cB(T;86qN`B%-Py>Qml zEE5X#E{nUEWR}2ferMza%0HJ|$hgtDtApKUz@h+Mt;T?s z@5+K5CoQk^22;mL`UZJScwB8o#iQ5|FdJ3_(QaICt@^r2&gwzK%{=~mbh=u#B5Gef$`^~bl zQO!W};?Mj{do91ZR0F$`y{6i7z7E{doH$^#%NdiK64#!#T&0=t&azN|E4qy`pwhKl z+8tPUqGq!@ZfhELp$`(=OCwfjQh90(-kUPc_Bn9)pTzM$JWM&6PqPBg}~}8#DB0XlJVZb{x(u@xU>#b>Qj6gn$JdoRpdMD>r@u9leURZb?5CwZw{FW1bHo?!8_wyH*7HSj7qEz9}UxQRWu_ zgAr;6rf1k#T6U3)4?ANs%yQLuQWb+DrD~BAt0iT-VF{2KK+mt+ShLcXtb@CcW zO2Lx2d&wjhv?Z?jB8L&pc04%It2WC2(zkOC*A4T*Jji&X7G3BJwsI};i6-$h{mh#@ zWFJwXXP_T#!jq^A8^86m8E#`yafjC-U-M<%MqRr$i^|II?YGX3UDw(hGwb|)W`j{` zj}16qGNI*#{SyeT5t3-ZrQ%DmR*OBjUun>U^7o5vXbTzIi6ndD$5Z!S&gb|Si5s-y z8m)N}6j+OI=4;rD8{u*i4x(al2La8W`PvoJ%!Q(LaEoU(^~e#$-`dD&8i(T7TGF-z zk6f6F_tX}3V@D28qJhKN`(bgYECT>#Z_e8C$0oz+QgeT;jURi)(WKA4=QMqBIG@cq zj&jZqTeAk)fSQO+Gv8~ko>#PT`n3sfSj;agrXH=O0q|YP^xexH{wP>=@YEq7s$PO% z;N9owG#oFfN?~$)V%8-*kd>oMa%G8Q-$k3INi5%vSIC}lu(rbetAPZM;$yOR@TG$ zoZRPk#b;CkdHzv7$nq+dy5c#1ulCpTPzbDtlhSbT^+dX3>i&}o4?SVm#gqz-bWZsO zt?TE#TvR!iVxRK+%N!-nHy2)}hug=E9(703J8%r8cAaT~^w|S+)AzdEC~`A^P8skL za=If^_OB_3y>mzC%Fr#21@A7McH2b{{;rl)d&#CZ_M+=lqBW|As^5|~dM@QZ%^I2x zsW?*`waVG77aiX6RVH3A@H^of4wI5{6L+GeS7+5a_lkd?1<9CpWL{7)6vsTPHUlZ} z%=-_1pJF*gr+6GI55I%GyC7P0$4Gd%;jy?Y)HTL2tE*Pzwaq7#|KbaiNE|GUxwb)# zrTQaU+IqT|`5=$KY?A>^SY3YPlB&KMW%nkOo}7NXFlH(>akGF|-os>Z)L*k8(kT+7 zruorloZfr7h9z2{#mp>M1jCd3f`7pkWV3i7-GI5nJx!ZY=^RZCh4=6lZ12gnhi_7j z1kFhYDxdylVNP=tG_t0D2aK5D%{vrd3DL`nq89f`DL+4*>|pCGS(NM6ZT(};XVSo| z7IRJ;#}-nx^;D+!aW=!MmHL2|_K)NMg@c%^N7XCLxZUX68&nGQ&&4|i8>};M^tFoT z{N5#rZEQi2X)N*_=XN6^$`QE)Xfjl^Be zchJe%XyX~HE^Ny+k!-QyMr}(*22tAF7&rTw$2%(DXI{XClGUoY6=A60YDoN0r{HCn zg($OXouh}!I1Fh%$$)&0WBQz-Rl6Fm@N;26(UlH01cxS~_XZ#;>;)GOX^(Q!T+iE$+a8mZL@?dQS$$>&qDWL~DUcc-j_mXrS zs-~JV>o#GTVYqV;Iep?4gk4>QvtLKVj!kjpr&v>&>P9gb^Ra1tq0p?G(^HRcGKwy| zj|wQ+j&dtKVKVngO1&XxULP@3&bPGo=)j&XQEHxzgJ~MC>;NUw8 zYtq`piAd%@-Atef!sZ|Hf1PwOehd^EFSGEIzV@a`=irAwn)J~;T z5!<^`-zLwep^h$51}S+CPe|^&)HM5;oY_!APx|rhd8&RV+q%a6p_yl1cbv_on%P*k zdz32_yiOaI+EfBA(aW1q40R?@ur0H*Pje69r*Xfc)YwZ)=ZA!KHqCxcYSpIrIOl6#;5NUNi8Ib=p4!sY znZa6cIH)&V7bmB?t=)#ri@;WF@4cs>Q-)&tjcC8RPgK{>ge<}}(4H?ebVlry!*%sl ztB_12_jy=fBKgkmCFQ@;3G}J+U0+^6=`rlD=p5!-HnAPjYY8wSUv}|xAD)rNL~olX z2h^&DJoTG*0q)H;OU+5PiZ6-2( zJHcA!wkGa<8XMcEK1p;UdbhM5m?(gj)g7E>6^zi12zkz;z^B0?_5RxB$u2;XU`nt= z*J=1pE`LFE{8>2k5)?b_umJrtjcpmYpW$a^L-=Kh^P|sP5VK62Z zu-p2wfG44@`nq{)G!Mz;IU%SoLfV{)`PkS-{)IG+yKuV>tpT58$*Q$ZymN9Gr)yT$Cw$h)Z)$jlR<_=A;n8%X^0EHCKhAZ#z@He!pK!o^ zzQ5~C@O$jA*sErq>GQ7SWIB)*k^MwCTgn5e?bmF#G2Ruzx&ynFM{yy^O1aj*?GH?^&S2@MCISGR2d3mpZrE-c1OX(~I z-SfzXw3@IVOH~Xx6V6u!qc;PQxHQwvRw{(sJI65Rqx;D(Zh!Y4S@ZGyU@iPwUa#$l zH!3c#FQ+~jCg8qf_DR=W_ zHXA$l3R&~1I8YqT?H2Uc<0sP+ty(RSuBX|iFwU_|qE1t}n_l(U^8G*ZLHQ%*p4yy; zznRuq^wVscc@v`;t88~UDn&fGG(^g;vd(kv-IR$z)up9Jek+tW%(byBIg!EJ>Rh_H z2Lt|Ectz$ZjQ})4Vc5nlXH!8%zI@=lCXY#}bxUtp?NTnU*0wlEsdm7hGw>y7l^$i# zSUZUYeV2zGd<%>WDJ;2Za3wrb`g(#v&Ytd+*HR&C#kFs>ImnDJKhg@flX4>6Vac*O z3C)Sud6RbyZ?ThfQ_W-f&B$UuU?s}JtLeQmQJi7WDbD*xA>g2cT`NZLp{dQaiI`Q$ zt-;xcfHc)^8<1)bCeYvNY4A*68AokEDV8>LBsXYW_k{UGPpfq4USwxXZh~z%H598j&=_8$bQoX=DoJsT5*tMcYs)`1mbtY zDnb)$zKFDYKT4|>mK~6hdev{x;3!<0-HotW*r4vifJP&(-=v_AG|*vDf^fDo+e@h2 z!jfG1cuNt}4EL^m2e>Di$riuKpqE%We%wtBV5q7=^X;X66LM%nb+N;jqOLh{v-|7u zz2!>Y)pMT-T)Pd2uiU_MN6YPGSt7CveaDunF!fV2I~uH7KKQ7>15#ke0WaWabo#9D z21olepc6Ri4VW2fTG;Dwc4sURbS58;S{-FPqH;E!XCqCObD9fw!v5&<_Zy>_DJnGrMm+Rwc$dt|zb z^z9aR7c~9skOSQncX)#GuOA>Urk$8xtp$`D&Hq%}$r4{U+6dK<)0X_slOSkM=WZd; z$(JFAc~o}WKj2GvZigHxYD`s@T&zhv~^2={rz)@ony^+q^=QN=DTp3`gG9l zZ1~?>dhnlHTAO5Vkl>N8pU>sC2^2`C5Oa|r(MX_o`KGOLl2-<@tK zJMp!Fe`psAuHbrfcFHaCUpK$Wsd3(~0o>4jA+rBZxK_&;F7c9rzBTYNeVJt1MA2?o zxRh$zp<*!QLL^jk$37tTWJV(<}YJK`v3do}ipdHC9EyzFDOaz^2|!%EKPZ)GL`LcFk$s z(t7VV&N{)-KwUGiO>%7`T@Kjt+$Xj~iE$ZslU*`SS%k{cmSjzl6PBeyS>L^jybrdh zai*wGJ)NH-^B|HOo(`Ao`Q=|`9fT~}9T+q_IxkHHQ7!;2K?}p{m76U&l(o_7UVXIr zZEY(3Cbz41Lk&x$?w-Ry+h(g%8na28y5$YHSDp0go%6Dw=TFWyX|}89tD)(jB$3rL z%p=Pwf?v=8V+fGl$;8Odq1p#)aeez(ug>twDrxs+m_+^)!<9HHoBY&c0)c4EGv#9= zfk8H6H3NDWNrTShwx4IoLs&EttH_j$8@X=$dx z^A9-Ht^|D|%;~c_dqE-X(vA4x&G4NIY2zscmYDF!%c>5T%&B20Pu!>X9&1+ev@5lU zb=kny;fW0AFy;?HxU*?UwOq*sI+7dk<~Au}sd#7dDSbqQ%(5${`)_AD#eX#3{q0EuCy0Nq}Ob$Cs_4;k8(|j@vc=_KdY?X{X@0zBZiw zC!p}B%=wYi2OR@Wejfu1NB#u_@8!2&CcPn;R#Cb`f$c9{MDi7S3i>NR#6g@W*Poe? z$|4z-Me8bIcg`PCtDW@{3}gxVmpFLpK@cSgw+xs(BLSBJ{{fF@wr(vUE$br~ciaEa zMw%JIWqOf0e~bqS_<>`+Fom^q1O?jLPyfu6B&`P|JMlj(R+#Q8_rFlzY5sZqUwQ;z!VTZ!w;H^jo|T z8vmNtrTePi(C%lQ4W-~+dS73TzP_^&9nh90_^c3oiUAN?I>5feyMuTKg6)9(<_P#S z1#lYQ1c~q_KGS&=!ZCQ(ZL4_D1;$S41niV4^4U^ke|$nm_Soo|yYOD%e52D7fP6qt zIEB#Z?u(Db6jX}xFLoCQnu_v%Q;oC5HIx7!oh-$!Z*@OuN9k#K)2~1I_(b(Y#bPci zVEKO5iP0QSC|%J#?hRjl)ffm3 zK1*oOA`qQ<3?JTGkQv+}bk4dXBppW?!_5gRS(d)zNN?gu25qJsexa%_DMP<`A)Ns_ zkJ?-C=CC-9E#|=o|Ib*qTU3tGRP(dogK$J9v1QLPz(+Yir0yYHMp52q*K6XYwK53D zdOxJY^Uc^K6&&DD8z6CKQf@N{vr5%>%v`2Z(xmV2+XCf(e&s2-u`7q#RJ={lfJ?8JxkCIgoumI!tHz&Np}yQt}`HD|2e_0Afy=xxxa5h~RMPP*EL% zOLake`&Yw++8+$*^tAIKCli3(oP19dyfs!U$?leXKQo8}oTKxi=hp8C!G-6*P-UvB zi~yp$u+-+j%8TFNxgp9^TPEhLU1u3bNL>BOUQU) zx#Kqs!w}pUX_@2q@N;PDZ*4TdKvmY7pK0|<8i8q)3TZL&3mo^QrS&B(b7Zk`Ny6qU z(XdN`Sus-ItGq^|G6?wzfk_41ybr3!=&&Cy?F6jE41%^@ z^f_z;qg)QtiKbrYq`{M72-s}%_kQ>jxcE^i!oM7i;V}0fNMD(601^PPX?~pyOYq&L z*1_{>1QDBJN$9r)oZ87WhTK`bx#Qn;a&mlD#SLMnp@=OBu**-?xInlxq+}w9@$-=??i*Rq(>UbZ=*5Sw=M7Y1bSBd*^9b&c!CnE^!F4N z4qp5K=O*Wm(Y7-wAQ<2Ns2dgHS(+ACm}g2+>=G6A?}J4ERS^8&B$Y=^ZqSM1Dt-<1 zzTT4FjLe7I&OU$fHd|&3WKEH`7oX5W``PyZq%T%r;2bqLiJSejBV;UVzs|r%S~Jo@ zfs{e?C1)rv$*h~zPYYIk9t|7F1*>@m6VDd_G4)F&Q~Ypz{{XN|<^&lQY{?Im-<%KJ zP4e~i1@Eh6J*?Zu6iWqc4u#udq_Ic_r`5R3L+>`#^I38LVc*6u5RaQ4NIm%}_3jlI zANHl}d~FdCU#0o1S_G~8#f0+=@O3@>MPHmG+$#f(ahddJQ=II2;7l|`TG`9V=s~^Y7c)g+@}zav_6yYoQ8^>1E8QBxd-DKXZ%u6mh`0y@R4>o? zgHN%P$9Vkd6Zmch7~d~NJk=@7It;BJ4D`fT02PM=#%CF9&D_Qq!*2yFR5}oj8m0&8 zJaH4;3NGW@$627Rk}`8wd=l|KG1|?Gau~2w+W-PToyQtWRpWQ)w7wFR9hUekMbrcA zP_`_$s@qI>gCoD{M+$rj5YD!xqbYa{TT0$`QM-A841e!IOUND@;M0}wZLHO0DgBZf zRbEk9aC+o}fr={{*Wg#jgeE>t6jy($I1mnAqfkGvA-PYG6wh({;jqefwV6aACdlp@cGE+z76dy+b^n$r&JwzWfAzTW zs`kXM(oeRO`w4Me$wM@a6?P+@)kqW{+~mV}kgmIH#cBSH(O&D!qj82tr@nm@)qa2> zaAt#9Oo{=1ww@|!%2Dv%S+kag(E;Al)P=O(KyW@wNVS?hYGQTv}+TGK6I`p2mWUFejaY$APC?K!T^mTo=K=dzC*^)*nMLkHYTMZ(7K#qKSBL z66ADW=h4|&v9tUcPtrL8OoRyaOmV49>sH96ALX8z(u7)-3a9QbvC3W904D>M9_cH{ zQedV?LriiiP@AvZ5z@>PEsNAsxFO9J_tr*IsH5`m)t3sP_P50$aEO47!xWoa64%Qj zSAnzcrmMA(dS7oUaD}2PK2rasS_y-+BLCUTXQm4NFDK*7-O6ozP#f3DE;DRLQR44@ z$s7b#RrF8+#-aohfL#2&j%tB z!bG81o5M5-c*$jdB`_08Ex*RtwqNka=vSuac0CNXnQk`l z{P{=3Px#~4NZ-vEXU6sz!Y+Eu^KFC_-q#DIS6~q{gxtY(Js@zR@KnZE?a4>V+2CL8 z?F4uggB0O~ubld#gjyTw#M$a9Jw_`XGq6kVP9JdNMjeDYlsZj5JX$FXSca-yV%QAf z?pI>~tq;q*H{~2}->)xsjCOnV(!Mn^0r3fF7Y*BS0Eb#VnloR?PnhUyKP;7m+gKV! zxB6LWF%Uh68OyRxR#KwjUj9=lwakCOIeykaCWXa3qH16vH6-;V~+PtlFIm{>pM+nHFniEk2bC)%ct|xTH z?|$H+w+d}&@}Ej0A7bC`|6rJR!ROU$36MKK3{lDQb272HT)SUAVQVvQ?1Gw*Mbmz^ zL1OETxhs(K;?T2jP0MN3{vF;`ToH{b0Ed=7>Y!k?#$Q@5a#P` zV+CanHY#kd^PMyvz=@$k0Xu^TyiivC;RYgbr@LNm{*+{z4rGe1Vu*MI;Oujg4&nII zU>trP0sR3us9+p;=(|Rw=6fca{KDHNpui~H|8|h>hl_m`qm`Rbf61PyR9BEx3f^XJ z0yRJGwhUNMT?zm67;_d+@0g4b)hq_G$(lGJLJT)x$2GOdB5%fge< zON|DKu=|0`-CC~3aE3z!o0Rt1cw~ZViBC;V`EAdpeQ8jSG5q7r9dggh)0v*cd=Gq4 z^k|e2NoIInPUcm|yp4J_M}Ly6Lb6?Q)@&9%qi%Pt3p#w3ef2=8 zscwto@yGgfWb_chi{Mf4NsGNkOYMPPb7hix)8m_g8yTuE`d$#A+kAPyQoCNcfHckh z9KEL%%`DUyrn|XEFYf7IJuw?5a$wy`6&YFm-hp_o8bxFe*DI^*QHdSp-hDar2q`fCWW+#i|?cB%_VnL@}chGGsS-dPwdHh&AOLcxE^ z!};g~B5_6y&G(GwV<#>YqpqTUjV&nWF~#2#eWmmEbXv04WMI!-;;d_^zMtHr#c#Xh z!l#PXif_Qeil?Qv<`?~M6fVgbE*Wvk4bGSi^xj)c7`I8dqU9B5!aZYOC|P;%%A$*v z+MOwtAerhbImY(T_0PfLGV!W~aVFjmn|RqVs#=*Xh^lTp)4Pgm-1|6wKU*}gDb6?+ zl68|na$zy{BWw_%iSSygyYnh>sQuAPlZ--F)+C;be8d42CvO z|3p*CSB+dna+bXE3c_0NY755 zc`l;XCjUgw*WSN(^Qv`+uNtp|gUq)nG{%hSdXs^w;HU3YEndGRAObHsz0<+Hr@bj) z5U7Vq2ZKWsXa}cey?4T_=PjD{(rC-1k6XIno|Y@MQ9JSulL^G#8sIq=kwpw8xv}f@ zhHzN!@zyN(q$|qC*3zSg5%A@>%l8Z5OOqyytwvowU23PuoJm1_ zg{i5AZ-ZhDG8PBbY@IcqbEC_(e~ed4AK~nZU-8|y?mB-^9O4_VaP$?S>#0b#p_!!V zF#}B;b3ZrpALS@B&xbyrYBpzu5^_?L30ioNq`efF*GriisJ-+Kdtk-)gs_$QrVv7DI08v0mQ1Z=AC0GxlkkB6q%m!9K5)Pf5&C;%T|e?@MKhm3B0} zCl(Z)qpo9umMMsh^z8z;p7?1A#&S1wFc7Vyi0{feKhOzW1S%f)V2Bfzmi`ARHdKhl z2D~M#$=70@130u4*0&w5=g2~_~%5(K};0XkpeI6YL80Gz&DO7Ei3SlAQ-dIDp7 z8H0{WvAb*1a0`~2y!I$5lDz247%UE66{kz;JKnHY*gIL9;;%9()K%+@J5@KJ?Bgyb zXX!F5Y)GrGxGLTHdV(8xRyO9`y}=jknBUOxwf}6 zeb(uXNS<#b!fK~W8^3j|J{xA+>=Uf=CP@{h z#}WM2nQUzMnDCqJK;i7$m3zOM{+N^gb`~Pj$3G;4CXRo=L^lF&rE#yOSgjvrY@Td8 z7+*N1seq1g=e^C(OfCuuYG90kWVUttr5M?#j?icbGV!s#6cM1f(45m5ekUvSB#ic) z1c!>5ob@{it=toJ|J}Rzl0VGi64JCl@xxENUKv6hIaTOm?LK}S>T8=}B+<9+@|NGl zdSJXQX(ji2>^E_9dC0?FgRc3?qy&g^c?_`dRi{B(mk#=8vS?|_?wDyohVP&d90vuT zMhl%Ic>sjZkU#;9H7U@y#sfGS79t=E22KkQCxOdl;g-i8EEwGMmlQC#?Yoeh3#xWiiJC_%&~(oS5w!Q5mg3rIeA@e4Y9*CHWd)YAQ8S;aI6n zH2+KDZcA<2vzMI5FE#KyAtFf5^+$W=Z7v$Q}z6og>Q{0hC z6FS+AeF_S9rjL)HTw(pvCLNYq!#aAnWcTWzwsO-rC~lAIhJ+xw&8O_eUz+Nt_o@T( zQlegd{j$RHwD@77kYvq%viHZ64L762E{hk4Y^V8L)YWnIEft;Ir6Tu;=DYz7{9Wm> zYX?_kq2MjS+6{3RQC*!wC)cUV_^p;#MJDw!v5tcL<_;_Bmq>!e$mUyrjodFKDNM%% zGph_lvz4G&?Z8lVL_cd?rSGWdLVX2$mfyCZuL5kT8fKnvNJ!qnPE|rY*qF&szpOWN zor3zL_fm}bgKdhnM>-9Dy?R4Qr34oiMjAW9x1ZgaZkT*rcF9_!4wqjA-melHJlg>* zuYe9Aiu-#y0q5~qgp)1+eh+Xu+6CYYBmMF~2m@*7AT<{dkIDdNg#g?E#NN1Fjd*Ac zK40Y^o>&H)TZ0L%+YrFnWY_Z6&76d68}Ij_R(xEhv%3#NCQaQkVfPZci{2Z*xLGA( zT{;H4F|%q{w2w0XOlj$@AVW ztw30WP|S!MjSP7QdEmE9pT`Stla(xfU#1TZopxqNcvpt7`F9IGBO_c6;<1COuksvZf8A34I-$zen_Y1dX6xOTh}7Q9nyzoogL)3ak3d}( zzIphSW>X9M9fIfa;>Oeagw<6yFFIyv^d+*8P!CHH*M{R79>2!KCcyxSkIm+g=wZ?W ze8T_L^Gt{TS(WUcocGNF*Ym7Po6)dE$_^=r5PSt~$}{6OxZD$usarZS1>$SLgw+M| zq_0{{+XTD3+8F+atbtQVDF*F3fSqwu=M;}=C+|e9)BrX0Dg3de`${+XZ1rGy>#n#9 zDcA8TmB-~j*^D32q${(2kg%Qw+F#;0YgYr{x;T310}o27;5b_1ZpR>gEQxreX?api zx@bqTa1)ro)iT23;KBYrVSkWtD5Qw(0h}%K5SDYnhk1iUEFcq$Ed#H_2oaHt0OAzl zYtkpP!l2XfXw<-O~A2wtk ztm1iye;}|wrCa*{mPGk~X8QjeK(_!e6P-NVV;zs!Xn_ZKSBg|u9jyZ*PKNu>6a zB1!nF72q$j_~GYFCT$Ac`^kS3Kd@ zFYV)_m#F?^CQhp5mCakXZaGd>t~4Ld#RvZ#+Fb`;s7-(JC-VJwgP97>Gj$w<-EbWM zzMA##Dw0hp+3{G1g$jI2IP=FePmGMT&9eS|sd4L!WT(J2;}33s-_(5%Z6NZ_y!)c> zQfI6wA`m|fL$C2{q-56h8(>J7mdOG9O!tKg5BpX0018S@e*;01TOzaL#1JTQ)KoEn_x1p zpdfN^5OFXdw{G`0t@f@HFY;--7PHxY;89i9%Id16&qAmW{3PeU3G%7haou~@_cPlw z&=)B&QC(f#bUf$(e~s>Do=rru=OvXnQ(!X}@o#?ahtXW|t2ctJ?_TZo`#a2#Tkdjn zBJba2Judl6^7xrZE!mAr4w60li+@*IIjVjvT$;C^#@JN&pH7c|h}0erx!mQgYCTM` zt1N%^PwR(rYiIOI#JZ|6YyMcwm-Usuy_d<*k`~?lw|nia8j!3^=6@IOv9p!)Z)(*` z5tU)dOkzr5w@Ig%R6c1Yr+Edp{?08Ce%D_eA8GSXE7|f*w5s_KYZEU^P>PiKch~X1 z%`%bof=N%GgjISb3+m~Zy!(@NP2$m(Z0c$(@|AZw!dSfHdtlsO;`-Qn$|Uu+*vg7z zb3OH_e=uee@j>!5&`y-%r3P8BmHo4+n7_o2yJp-p>t&*KY3|VJJ+?>xo9O(q9|^ma zy2P9JvBmY1%WrZzcZOJ>9$-Y0&u~raTwn_eWVx zh8L8t{mqkpOPSX)4v5diEEhW!*NzRESt?||^u~`swkF5-{D4`kuXl65>RiJ{iZVPr z;s~O=DtMXRb?$Vw(3^0Vc)RJKQOZ-`(ufvNM`y4nGt=T!RF2hNpo~|@67}o8WtQUX zn6Bac3==pbP(>-iZP%F2&Zv1VJ_D+aM7D0Tnd9d;j=ua6HkC#&l!Lo1t*lA9v0`I? zY`~%lo}D*sDZ97BwDerh`igZ!eQUDN>LT3K{+etZjTKnO-iBd zlP2{y3nUX&t1o=!i)C`K@)i`TED5G4G;fBNqRF{(!`_RD(=+Moj(t6U{!-&6X?6m< z(3-c}aeCXPi7}!bbk5J`UBz8ZagUot6LOBHmUfURX>Sa!W^W%F$Iuytw1+%pUa~EW>xPX&7C#B=hx___mBPEWjpNd|3a6{|6J+^_RVC{m+cNYz(Mv29>&29T4R)q%$D~n>* zMf!g-|AoHhr-I{`u?jkOVwb0-fDR~qfW+LVIJu&|LK`P1Y+7I*qQt`G_t*5v8%$cG zn8dR9CzRaJvlKl`KqF$?WpF$AXC>fs3ZCROe|$$_<+n@=d~ceD%g8Z$VJxjk4Qf$& zt}pYTKDPI<@as8a4bR>ve2CRWWzFXl^t0R8rIj173M^8}d&_U{H)d{f=0RK$>SwYH zht}K7??I8ThGwPJ5j$^u>K(%A)Xn){KmPL#&F|1(uMX@g(@_ehnWK37NLS7_jyZY1 zwOtWt*TsyTTxj^)vw9&fNM|Z;mc91ncj;u8#TUWS_o+#MX?UCFp}6fRjuBGtowYYG z9r-+IM)F}>OUOsvGEqkWXYz-kFI6q=g~4)fy#hz_ow}YkH=yba-XKRROP(dYC`WB|cHJ1mtCqPX6?yxd{NFaI)mw;3I&+dRkec|eR_ngIT(Pkg^BmOoiowx||ZC_BTf zb(rCIFnObZ+R)ylVVmdA@o^JP_BkG-_A^XsEtHFcpLXtq3xc#>3CZuGd!EF4tAxw5 zmRl{pRofm_&b1`(`SdccPwuNF&L8DaVAL6P>p67uRa(0OlqxDLwKFQF(B6)BgdiGv zQLw7|LVk_?F!Rj)BDu4MwAC0%9do1M8EbQu!K0#_*NJ0yJZ)2sO0Erd=}(f<#-FX@ z<}!~`7vC4quK2CgK&Ipg9TvVjF$U3 zn-Pp)h%R=;puDW~;*Y`$LCzjd+-WO7*o4me5_aoUm5JivQmlIYN~D9p648Dm5#~JY zbSnvn!jBi`;+z=wJ{Ms8#+D4@M+(5re$A4oWI+mfHN9u=a&h!p-T#ZdxBiRz`Tjsr zF_4z-lCA}51eBHzsijN08w4Z-mPY9mknY$e7o;2M4(SG|Mf$$b_xtnqKJNV!zW#>0 zJ9B2{oH^$_PYks3+TWan9F^{b?n5Q}SfoN6I6?Rlr3E=KJ{6TtPFGi5WbURjMQZt8 z;PlqrmS4cMj#BzJF{(t`F&3L5SAfdb^ms>jKxcPi<&5N>M|?zIpX3huslV2ttIuSw zQOPwiBa4<&p3re+MW^|YqzrFzas`|f@8L{r%&=)6i8jKmnrB>1K9uJ@&Lf<235gYI zBb2y466m-lQ%Zk)oL$ivKIm($rB$9Q<7*A6c{Jl<>6XBeBQx;4&ygD{T`2xoEIQ5$ z76wvfO}?yJb-S9PqDf=tU@efTg_9IePII$9lO!QV^KeaD*Ekw*aQVaa-hrJIKcUh| z;^Lu4;VKjsj)1WK=<3*zTKPtxQ!HYk0kD&hulf1JrkjkS)!tP-hLdl!WHd(8=A;3RogHajB`>vjUhBw@==Zmq0|A>!1%q7QqwT|}}Yv}(V! zxicQlxH&rY^7KujykSf1W>+eW+63p?&5v`ZIer*!lMqi^%CxKqFp7FD@6!M%?E8_x zEnL2sXgiQNzrRB#J&iQsY)9WW&6dPSGDSDLJTF4*7Na?GucaWn?dRPM_e=;%wSVKCwIg5ryh_;-yE)nN>{JvA{W)Y<_{gw)&Fd-k6LVbaMwv#c@-FO!5I{&Mi%ej>oeS&Ko2gSa0A<>jT z*()nVSZ6Nz(|im62M~YAe!bD&^+*J4hu1`QVJ9X@DB^Rqa`-F-a_2Rw*v3>km*I|a zD{W%=M^wOeA#PJ7EK7Uw#MriC+`gSPaYzMY7RFp;7GA*f10LNHc*c|j>=Vf5qd5N9 zCNa&k?ZWX1cTLkGFg*NzY)a~X9J&5V+77g|y6(y!*%-E}F%FAlwf2@nqfGqO=4WZc zIG)>OD1Cu3W@T1xMZ8uQV4X2rdgSghifrQ^u+!GH8aS9Ue1>2wkqRbk4m-^mk1}Wt zeoz-D))aZT?RZ`PY=ilAtW2Wg<(qnch;dis-fT@79Ikss_4uAi{9L`!GE_)$zR6cnuqH-vMDzx^l zsRlE*MGw`#HcB+PeoKDRZOX4hH*{biO14l`(;Tc0Zh92%hp=F+eT4HOdCdxEdzQMq9c=DC}wxS0MDl{|X3xe@@fy zeUPJX(BcKAGkMkx+x&TiUpC0}3>l}=LAa0cJ;=v}cd>wKb8ml2PeWH(Paw10 zUfj@c0Z#(xkpW)$I366HK&OH)m%2Z%$2Kz(v%{nvUiP(Vp|n%QCWL&#{RgoepG|`h z-KHhz6r11VZI6Gaf>T9)w-LJzE*wgYUGS7HG<`VqwQVx<;gOuxqnE!)7rjf{A?I2AMqm$F%g` zWe0keb2b%kP%tjAO-ONc1KK`MI;ny zl;N-7$i14NN{n8Z(iD1qCOND9;0axOX1S}*iD(dlX_$^(XVrnRso9`)tI*FJgbg3f zEMvZC4DTLl9j1K&%`=EK*Of8hO2i6*Ro@8urgqzKqfk8O_+}uQUDl9YQSU73XWd<- zYoQsfxEA7e5&hxqL!8d0pHp0qKTWgUp)J{5<8!1~Wu+jY_cn!REA%%%*CPjxT5OWZ z=h=|i6W!&Tc#QjbJQJjD15l=^HuH1m?SOs&`Pe;QfYLjHNORZUv zJjNi!TIo;aSG}Urd-i)}pm2)9aGKa@KV%%w+m8>A!5vg^(+Q>NwLoSRf0XGVcxmU- z9!#dhhShE@I*l>1)O??lc2=^A-<3J|kRfza7Kagy?xNn(E=;^rjn|HMU*3Fc6XAj> z`qUpnNH18}Wl{1gM5=Fb^YL4~*-we_Pi=yLEd(>44$3gko@mVWL8l{%TyB|MTfhI7 z4q`y7t$zb~erYVQQ1u@DWTmd5Th(OP7PHb^gK(R2+a53u)_X5*H`JLrzMm}n>nYuw zU8S@s2;0U*P-pHicNjkN!ETMu`+{{^aTQ1JTK}B@aAWSh3V`tPZt=Q(H+Nbbc8%?w zw!3`O>yPr#AEjFW87JsaOpPf3QVJe+=CWNJ5_nQB&JLo#JuTv;u zXk34T6pnK-jzd!zw|HC966jvQZ53>p3Hg}b$vER)@Py8jxcW3gSe+V0g-Gs6I0j!M z_~s~yR*!&^)S`HnmA73pN%~_wr%wO`!8A=3a+p29XuPzL8HWR@K}ABM>}HVX#(dm5 zyN7zRPelMDvz?)44VJq%HfV*1 z4b%vzn_pXT*$TXCB!2~RxHut^B)K`_K5dw{l^t{jQ7O*k?R$td#s9*fQ`k$4GtV4k zh^z8Nd~QT1;n|&U;58(OLkU3P2zDhiR<{L~rh$fyYGY#~fS8V=e!r4Pth(Q`xj!^LeOMb=y8m~oGz&HYiWv_Y@b=r{8 z6!v3cZ&{R0bU_~Dz_t5M!&q4452wr-QRqCg2d@25a@O!>_MWeW?Of^ba(pq!%B#wm z2cqTTtcWL_qEQp71(ILwdymxnaNbfutQ#G}n$1q($kGDuz6ngN1e0;y?d5OMX$qgtY zD_w*w$K>KMV`vRZm}K+XLRsa@m+2Gv<{P}K5P`Bg0WTHF<_-ggDe^K5jKS)XMIE8V=az^$2xkCpZ?h+ z9Ivx|xM-I&HPH2;!njMlOwW7TG=WGK7Y#VHX|s$7Q|e$$il*C}OZM6y_gChRzl+?K zHtc(zuO&pd-TU;>pcRDPA}%%l^D6U2b*`12s@C&(Vt1+l6oQjkE`^?VHYS?N1;b

W|5KNl8Q-RubXV^-P&qb#KQTi^WUbZZPjMRNE@DK zIodS7MaIc`v#435G>Ny;d&MDDgD}heeiH`?4lj$qk*(qd} zk=oIXE*w^QACw)A@w(Rx08Ba?2Wo}iA~iyoq3e)oFlG#MZfODW_*A)0vwfRjVl?KZ zj;@P_YFleAeW{je^Q`A31n8v0MWo@+lEf&9k80^@*(kgxpEA2rvJdpk@mH17unQHk zz13JDDazyK?)Pc(6m4u6D%@(&i$I9Q{PrM2Olh-;Gd#ta7cP<_)Dp@(>gNtaA)WN% zOHsZ}K3k=~0XC2)`C%Q*}l^kd}(NFHF)sh8>y zLB!kr+n3wFv1-HQlDXcXVYhWObVZV-a9d5BZ)Au#t@jIgA*!KJ;Pc%XtFlu~S|mHi z7m0lJ{mw|^r%&h#6V7VfTJ{IoRtQc3b*wQLRpkIQ2Jmm%Uw>FRUWh~F>0)8iaOw)t zAeNPq&>lOFQFc@3&cIs41RtctE6}zVn@}VX$8eZNEL)wJtl)AmJZuqHl$9`i?g@W& z(34JXs39fmtybY;q@~WsoxVYbo zca+k7heyHOY&>9RY)?CE84!oFo|U4P&6NXjOs0fWGMQ*nQ+niDPVRg%1tSAn(ZMDp zfz&xK&%c!zu0w)j4i&(E$I-l;rm3s@gh?4$qMX}K7=E4V>4%?$(;#^w<Ql9BgItV3kh*@OFUeHs!`LyA+%4xv@mUGjba_?nHN=**7~)Lz1v! zdtYVC0>w+nuCdW%sV-3S@I6luIJx0iC#91ZaoO<~-Y?B=L0Qr5DF-3SbfO1@a1Dwsv*GhOGKeI} z&~F|41kkZve#4XVbj0IXhP#AvUyE}rvk&$FfW$+l?z#o@B5?bZjGS~e(@?UYXaP`Dec4w)D4m?gdMch(+oWhP6kG1Cy81r!+0LHI z)gJJNni{0X_hK}o_oD>1*r3Sm6(AKM4)|<33^z?b{84S;rxSS0>ZHQ&HPMhad699V z*%XN8-$+V+FSc1&%+b}dugt3|DYg9Msv*`=j6X@j570wNuas;l{O}2jjP7Z9#%c*y z#k+yz!9j)T!Ya;h!gS=Gn^YXJ@+~K9B+`>70PFoWW&$z=zsyO}ZNi(8HOD z@3%&BE^1Nu?05rR}`n>*oi6a?*uaCk>yyTTm6K} zR{LYW;h=Q9?iw-M(YI6Dpb&2SvdpypA3%*5u@k$b`;+sv2L#D}k*Z z;8HR(oDN0(#(h!$7gB-p*x%!Xs9ayo{)V>y`T-+0a>1brR0LcHJqemvmi3sM~ltcfE?4Jw+FWX<6^iuw}hkjBPTT~d-s=(d& z?*S1$XV?>J|B)i|AEn>Eh+6!jdNexr*Ks`}kwL}c;LFASk3P^7WpF^qJ&uUK*77=R zgigG_bPM34e@R1o7aLDyv_A0Hjv7Q~S3kpkVX^)fX#o^DB_Ti@6bKLgOWG3xnNe(x zwReyIn-fbQ_TBU4Zv(=AtuvgVM5f(`1oyuMyvkQ5P73isqyFn#yh`BokkHq`{}HzO zOhy~mX|4tNH}?CKPH)Q-{jS@FGB2i+S`Hl_a|+gL|GTei{4OSbWt)8!7};_+P2)f1 z#hVPQhW+6`2>zoFi6^%61%?hG!+$d)%_)N?H(9^_wR1>4U${D)l)5qg`qo9*h!MrE z=$F6vDEd(>k1s6nClH_cmq(E(fBm=HfN}EIJ|H3a|0Q<-Tm5x<|M#;$lD2{;M=Io5 z4TpO#HhBXU?dJfbN0|R6m$Y;-Cz{*ZHvL)-v#VQ+7YoLKB#e)bi(Cvx_4f8gk_%ae zBVn2Rn}6aT1@xK&wD8%SlcJB$4W5Qhh1-E1|D#tQ{wS)u!+E2_aX2E&9zh}EGhJyq zH+?VQ{y&|;Km7PC4!Q@b{Wovri7?PK2N(@d4Z3j8(=hfX_tkD}+C(g|_67h<0ZH=I zcj7+iSTt&#n3w=O=@2T!4r4nOM`J~WG8b;d;XO{If7*|~`UyC{fJ4nY@2u(*YuFYf zboH}s-N5K{d)&y~!lHnq30r&4_x2j_{c4YTnIj62J32bpo3AQ4B7Xp&UH!#&(VEx! zd5rYe^75aKX&dQY3X#askN#UW8XaIic1Do60cOIY+oE?C|K!9349H9Y<^nP_ydL9- zTyH6|!%s$n$vvgzPOpx?FS@Tp7^P}ey(5~G4>{NIJ{?ccafBoyelE2E3r^#A%GK-* z!KPXNp3!W#C~T~bwDsYS9s>Awl?bKv6Rqsg@$o^!f$(wmtI0Bb-(d*?kH%^z+w5U~2;A#TFVk3O9n1JfOFLjcQOAUwi=qvyH?Dq?%~eRVY~p%AN%qf;frOlK z0`#wqB4=f0zFfle?z#xa8KUxk~y7IE4AP58kuyaZq8|OBSa40|jZ(0i0L@fE*U}DE&)ch1{$d}Xn17AaVsYQ1K6Zu?sr?Mo%e15f2grW@G*-Taw#T<0^ zq=7E;ck&0oB2s`y8y_DJkU01RBM5`6-hQbS*grtHJSkhm#l+eG7QC1;jsgI8#r_iD zUq!`Thx>4G>X#h2BLK#LF{CCq!Dr5|v|oGMU>UZ0soXi?+H*$9!CGZf(P7WFLE5ed zU5>5kj=7ufo8QanpfE=Oe||e}S>bzoLt@wHF!J?{R3vFkfOC8Q0gy~3%MP#3$&tXg zFZ}=+haag$JeMlb{*N8>r8&Pv{ONR!-6C{SPw-to;aYFVe1lsQDZk@#J0vYF%@OMr zF=b*mut}WgErHB`mU&GZ(GvKjzW%}=FhF-J`v6n^bhqj|Ah-%CJT?F=3HmJU;JT!O?H4qBFO|N#>U>Nr~nCuw^MwAB*O210p0>8`Adc_Bxw?499Plv z6<0e&jV?s}k+LI+F`wOTPkeIdkzKtlgRoHX=_ks=1Wm(Le_iFF6mK0jTt z2tTmC6XgtMX)|fLZ!Pesn~VI_|Wn4 zJvNc*bSlEA+?NoA?(P^G@AUj9;}&tNHLU4Jj`S*Z+L2_!j)IAsr72*(vEyXF?($Z~ z(|N}e`=QCvm&^r9)G}Yo-&=vpKK&5|X^cA+TQ_ySR**gjtFOeaGuf=s8(5 z&QRNoRUVoh9p%%t^$8#8OkWV@7Y*68aFC^?7DJW7#ZQP&_|8HklvVzXznqz2F}+C< z(D?veuN?1`X-|iNXq%6kuQJ<9SAT<6N~zw=oh0hZhtx5xjNIA{yNq&YH=Vs4+nZaB z6@U1GlK+pbAx@RpO{Job0dHxT$Ze=-$CnlF9C!BAAsm?;)wE0s`hAH}^8L}+A)G$| z(5lGPLv0IrRP8V(^Fo!7MDYC?c<55bYJR!$6URhSI0Kum&g4btVy@i*^T2pc$MDAM z(5=+w0}M>fZ;s;r>wo zP*Va`GfGJE1X?sL@n=?BdSu^TmktS-<)>nVxd-1Uquo@l7cF{9j2_* zlWH)!P34L_V~dKu_K&7J4INEIbqW}uZg)_oT3qScM8orH^aDlEa24FrISWIJCR@e| zMpqY+#{*{f#=?CI>*0k@>Di{UjkbvEYh=$Jiv@+3&g|*BR0l`73t@4q@R@o9cd?8> z+3$!}xibs#{UHq3=yaZ7=CWgRv?M|moy*YFZc#<6xV;4*4eD&>`D8P1U1*KRAic_4 z6D$5azQmB&nnsnKp$b1pw+p!&POaMn10%!YOg@XyN{jd%oaw@92IW&LlSB?TLGFkRv*Jvbx!EeqFvmbWY=vc{ z<`01n|0VG0U0|f^+;;FuP=a7}=*wj!DQemX1^NC`Q`-_zb{6#Y`MATKNxb(!y1~PT zZEO1EwGt>oJ2oQ6mz45A%aVz0#~s>ha$L4;T-C9SgG+bH(j!lz$S){i0NV|=WGc&LpZx|#p3mzWo!CFMrTTXU1}32BoLr~K42DqS}mmE0F4zKU{wLto4oU|C?35>V)2yD$eC3!^%t%X&U}$KTu2IBWcRtvcxru7*NjBU zAT~voa=AIQ(5+#0BdXM)5lm0y=>;wv3*Hqd`9x^f1or*}j1bp#NR^A|U976>pGXT@ zV2A9;#f~fNG~MxtG|?XeKI{47h<&ZSkV|m#Wxh4x45S2r{$7?NHeZ?;7 zOE?+E-s=8^(fPYnf0Ytd2!pro03L?72hh|mWKMnDu^r0X-hM2SbMk|cfnm`aPWYt( zI^pQ$f6HWPcbvECN8lsEjd*rUA~(gPSk;z;l|!P~bH;rkP*fIgCtl%$miEr*4ev<9 zA9niV6G9l?)Nlz96o{Z=rVU%$44CuH8FCa1b0z~*#sap_$_Z3c;Q5%+1PrPIpP*D3 zF=$xKE`yG^W=IqUKw1l3&(k4;#L%tHou~fFGuPNpg$7=X>33CO$%U63?i*1aUfIx+ z4to}qJLJ#2?Rl@E>?N!1W#0YmjT?xgS`P968KW-X6c$?o!wiO(h9;N2f7So+)5ITs z68MLo-oSE(wMz6RgbUY0-hcORJI0nFmRH=|oY&Ff%K~l33IuQ1o^UCAQkAS&Cny^W zk8jHkaZOO|o3;x8T7m^Sali&;W9j&=EOmFMw6mKf?e0G!lzXmYw0y38nTU&)wn0di z_=0g!ZR}uQHs1L$&R~Me6nhTGo?BUYUF(k$JjdF^k67RewM-)~u+pYIHUp(y-r+%%F8^qePbQ#>dDnY&E0|D$ z2X?Yvyw2C(Mao+Og|s1W`D#=i>m~XlA>%~q@<;|nGMALvZEX^3Hn^29^e#D$&NWfSQp9IdBR#2ozt zOtYrj=cXxZ+E4w^>8^Nh#Bk)E2qX|R9GG82n8HW|Q}@l@1m5!as+a(oIBuEG$VPpZ zKH0?zeS-ZtJkEa)4`?Aup=Fbowws>@P6UO6j8W*`^H!nIq1}HbXYS0c5$WWGRruOu zv$l7Am4|~h3Kg$vKm}x|Z+p^0iU{2%2^RVk|Nj{>=0_+*!!j(md+COQx9ADos#g<= z63B39ez>u*(j>eqj`N`^@uw*{?S$wFdM)?i`QfoSQs^4q@%5V)6uREzh*@S85;?-J zp89uk{q^Y|k6szb_uEYay4gjc{SpOA`Lo$&>j_V$gb1JGYtWJz(l6 zjn+qF_eeYx@tcVX|6!sIp+vs%aid#wen_5Us%<)z6a?EY*H&#-C+1BA;?Y-N_5}PE zpMO?TTm2mV1PoegaQ|$9=EfVJ@1f?i3Il()&XTGd5}j7Lbd3OS+uKAodz)a=g{FFL zzSVeR&_tO(c>_CspLOEmRy59Z{YTjF{uMSLpuKJwMoKLaCg3TD-kd(L1`Dn z8ja|#C3YIEIE!09+OcM3hi1+;IMmyuF6Bh5vu^KLrs`2Ha$+X}85;yQ(lKk)v3Kz$ z)!o@>5M4P=t$}ED4rYOFth2CcR=o5T{v&;7A>)R7uW>DxVv|dBx6x6(;q#ZkyF>Ze z%-WU*o#U{m`s9sp82ea3`)*5Mezod$@!F7<-QvoG?B@$Gb8sK;G8$v4MNJMEuFDD} zBw|+RtHeNl%L&N-PV`pIK@Nr4VDI$hp)8`cH9Um{ewoNDV_PHWpp*Ib1|}mzsuEZa ztg|^*=4+Cgv$j9mS zj?GLPTS87x5!PiYzDMNB<3{9N-!ciMG!AMaCMIVqG~vp|C(9U6n_aAvCxA`fK^L2# zz`$bf;reC-INgKJEkO6%>Lwu4cjx$2S;JeCNLXoe?F)-+LrGtB#(L7CmYWHj#~6P6 zXe`q^$IG*PC=E9A1i#}~Q}$z+#Lkv|7jHFg9jJW@s6pHLejp|7JU&U<4>pt2QF`1% zAyBGP@-DI^P~q#V-J_#QK`8=(6M?OvFthLLoO!)vad`;-5sm&Mn1B(=t!dg3c0ZjOdDVG`LDU%^gdNUYWVDk``~j&LYRW@JybsM6sj(xR$b4 zGs35vVod+cfvBeu1!(z5uW-iTvC7VdJDDO9oHX!ChRE9ccB8vG{%N%vK??9|tqVTK27yb+g)QiJSCP z_n>$$(r1r_V*HmZZDJ9=cA@+7 zE#6;w{k6Q`ks_K%d0@9jArU(QG4BG?7Dj9IhqOM7Ksjr=Y}&Ud_<7EcEBp-im<0rb*-pN_R-!n)yhiK3MDUS@TsQGo!8^W(&*W+K|z0*pAU!tV- z@BO~DeEzvWwRBQiz>}V8IXF*O&iFQK9>ubiT6uTCMf`!BBocxI{HTxx3(DLyB`}#C z^-P8WQ~5(20YI*cWW%zHGUwTpE5FpqmWpab%rFPOHteR8oAxv~zdkrwQSL-o|j$|f%Vb{(CC0XHMJaBYAL zl-ED&)$LhXiU2z9bLW_zw3LRaQrcWDec{z z?T6GYcG{IaPH66w=qtW|pzM3uRTtZt5-+T@t*^;W$O}`9yvW5(E+qFa??(8b17Ll3 z!y<>3&bUAR1U|_8*paig_Ci*&>yn!5Md3wsD#qP-lyiSB0pUN+wai1v2&rtpUKK%dRgl_UOXL!)G%05u`_k`zv8`n*q<#9!2 z@&E`Fd}smU$3a?J1AsSO`M5B*u-ud<;~}Z1HcksXAVLZn7+rIq@ORTuV`khtLs(sKFLymq_@MBoBR{Is^|f8iAxxC`e_ldfY;g&hJsmd zpjZNd7>)xOu#R^uV9q63BM{HxAp##)CV!$38O=jsRR(aHG=cwJpVU0sPRM#XTE zZ$|_#B>lqcycuQHD46|%Ti+#!}tI3lZkmFPc)5G_7 z)FK^@ct)F0nZ}9a35!p0HFKs_TUGz1wd4#?%rViJ&r@Xcs)V{Qg3SY0UX=_0aPfh3 zo#rTQN&A;C)2&&CCs{aiIGjp$?vZZ<1RQqo@-#dqL6QbyH-CuUuP^9~`lgH;yuAUA z$y`=E{&`+O0r~u#AOMmXXX7Ys`Nje?Jjz-0s-M+S*W(!bjK>%7jWdrgRAZ{qzu)j@ zu_l@qkun8~%ka|0q*T8(r>F!l-}x{5ozHAaAF9T~oYQw5zB>TE8gaVwQp( zOe*lmTEzJdpVD3V%yIf1FC7AjQ7aAaJvqB_?{Su|y#D}j2ch-9%rX4EQPl&HA=}cv z)wCbKHV)Mub>aLBByIBrkg8AYUR!~;UG>ipGuvydy4Q8>8g{=Pf`DAa(Ru^t=OUbi z)f`EC+l6%sDYBLTNHd6;_PJarS{xL!f06neK)X+(9<+!@{=k;|byI7Kbk=69VdbQ1 z)r?VhZHCl!P6=p7&=h6jHxP4?2ZksJ+dzd-?u2V|@_f)dzHGj`bH8d>g&gG_Xqf#X zx$mUzD^DV5TXO3nP$ZHLM-Dk`7pP+UZvGem-$+|%5`SJh^da5je;EW^)*o5`l;Tbz zoT9~n$3S}q)l16C6q@G^J*%x1Pn}`hyu47?^b-O2&fahWgXD3o4&rUX0!k7oJ;VUu z5i4I=Csn7u8#ev5NCe|Q9E@bsu~`!eBX9j8JZB@v^40oG}ft7fkD0zVx;cb658{* z;j2I$+zykjUa&fuaaZXgcz17NA`ghv6@8u%8fkzRDrNL-c94169J-OYXJLjN4D|wq zp!IFxrC*!YS;^)vRCZ6NU&Yp_+oA~9J*^C#VeV&yW9>Sr1Zx9%=VUs%MMzs!F_=|MiQ`2XP-{O&qZ>>9La|2{`3<%-(*jZ#`jih+ zSR_Nij8b_1i(K)@sxRC#l~+2qwc&Jd=jB{is<$_Ps2$}V67zcY0g$r%-)>`_0S~nn zIG$kb!5LyQSMG3HH(dwoX;;8qA3pGncm^0zby|B@&3SOwP^eR9Yk^DJw#OR{<1L0! z=^4HOS#JRO)di4WZFQsW&B8rMJ&NG8N>1@Pp(<7PNZ4(bZS}3}uIb#hWeb zy+}nRa7h<7EJ2HDJsKE>+dCRhhqSgDu1i?7mvL|IY-!q-(9G12(fLSA`XZWy63a2M zp|I?A9@CE!X=54|4T#a)2Mc-45MdU{5+#SUJfXy);PCwYT>y}#0mpZ)!Y){zHcB*j zb+kthBNR(}V>&raaoQCu6LM^do3AghXMdW54nr$3vfOQIMt&7!0X1^L1e3|574FVU zA@}=u%n506ie@cs!spbj1oa?sK>6-8P_lQbrwAuwimUwC$TuWM6=RYg;MaX9CiZEU*o)#7^xY8RI` z(^nCl_euRB*W)vuVK|jJ17x~FLb_xFvU%p-U<`IQ?;$NWpxo=I(!sMiB3mYFI(R>v z+B?61%5u?rosy3R^&2}Tip%Kz&NmmS?fs~svdq0Q zr96Y~VV2@cqy-!`c`NS7IKsj#LVGT1cx$wtc}`+oaXq?Q7heSH1}#A``s!73W8>y! zHFyIv2SKolxjJcWxz+lkdYFRuq()qLC=hP)oLAYeM#&Be;n7TsF?&uVhl?M`q=N!kT5Ajs+ylr+b-VWft1`=JmPyhSAQ9Caugk!>Y2IqzF4F zN32VOfxBORXinJaiW=Pd20d-u=z>hAIB8*JH&F(T-CH&~*$+GC0O|w01vhkQ zL!C8y3UM4m>@HkyjtWDj4rq%~XU?vvE`fsIG(69TwOiA_w7to~$|ld;CBhc1TYs}I zEs>l#H^1^n2Z44Ai1zEXeZ87C<1qj^oB6tafSR$m;?oQxRf;+m+By9`{gaVfez~Zm zDEp1RZ`n%KA%`NV-1FZIyb%8s9!LX5so6a+kBEa%`j|`{Bw&ln=|>`AJ^z=1`5xf} z7oM(%%pC2GfH~6#nEXa6BVc>uIENaRV{3>IMAa$LZY8Vo2qD>&6`InZ<3&A*>?r~& z1)@^-@m6#BY)Td`nV{je+{wPs3oKrq6L_u4+ScXd%n)7c$vG{$0T@ZVlx&O|9?{DIYi|oq^rJ3|h_IXI(S n5|ZakA)C5SEMyR>p`op~Y2Qa$Mf$f;5{Bsp8#&TA| zXKGTjg#^AQ2^)N!J`PN)goDiQw0&L9h-lD?tku4=L>v!~M3jo;P7i*H;GdOgaD%&c zIp5TRRaF-b`x_eed2Gakfww=^+rJ-p$Jif8k#y!BRiMX&O)x|HdTLlaw2M0ZOoUk? zS!wB41yYz&NR0+MoX0KNR(}V?ftQZ-~0N^Jr)_t6l96-wHf?c+A!Td$rSx$ z2lJSjavrqs7jeTg5=ZKDgGJANcm@$j2sa2f8;Gi{+}H+}!p4G97gg0(0u}l1jX(C( zGfZ0RCb{}s^%7X~l_lC-$Ia894F?uRY(GmWf;yRBPMe_3*3|;9wt?mx zn!C-k`Ph{Co6n?L;p{spuPpLYg1S}TM@SpKSvz_M)#%sk2~$(VrG9>D=wQvrPjx2@ zKvPuM1r}V{)ZIZXdzWIA#2DwLHfO1oEs1S$UK=^D#=B;<>Jk;bZ$jCywU9+b(9ykc zqoUboJNDV1Td$S@NKR`d>^gv?buNKdu$ z%XIZB{;|x~fo+A!6Z#q!hPu}ZB*V8=$T)dtgQNTUwU!Qfq+doZTe)lsGmpV*NSK`D za+<+i7!C_${B`8Du{qBVh4#I~cJosv1lV1(#luF>%TaA9oLp+pQm?)Fd!eke#frwA zdpjsd{s3t4+giq&#^uPsuZ2BE?mSPLG{u;RFVUtEZzFWl%rVu2&y&pp1u(s`^yW29 zBxXHFU!_V5+saOa2Q!|-rNATrF7hXPqFm_!w3j_Q0l>p zmmHazh@Xru!6qaH4JBX-qw^&V3j!i`T}W(A?XBEiqY>;>!!;wd7jHu*%UF=WP4Ku1 zlXpUfXtb%&VYTulJO6~V;{gc;@@wl%1XZ3FEPiLB&w<*wmsnn?v!i#Ygo>D^05fc| z8y^s-8Tx$+doQZ9>SO`m2sAPc^@edPh|ff?4e6}?|_BsY$CkOf8Q(d+Y@`fv67h z$_?uGT7irl+a6wvy~235x~Wuh+9_JnPyJ_y2`Tj!DJS(UKHV!`bcfDBfo4-(qM4cb3DfBM#FtR4l@Y8tjE`<^unz`ow^T zvvn13*j%I0Eni360r+1;Zd*4cSeELf_3b7nW_kssBrXqm<~u9}<4$~oCaW4$wum@X zG-1OR+DXcu6;8q{Ic`PY(Kqm_GM;L?Wn}ofw79lzyRMs2?7OeCYCwypmq<0(`u1kr zUpFI~4qs!n_jU=3=QTiWBPW`TQ$q@btYb5y$P(IvX(7Foll)CIt5|vXVQ}Hilco_B zW**Q5AChd>XZ5S%xLH2fQt6IGX?%~`aMONd=k^H7aF@-LZL;7!d$p1qrHc}4Q;pKYqzz10HbNpRIBI|Gw@GR8i63$5QXV5J(q(_YSI z1M^`eqqMDziP0KdtBWT^aC*hC1p2cZnFGcbDixzX{GD4(aJ7UmUel1Fs(o9VG-_V8 zol&1(OYxV`Hv=v03;LEM?jF!7O}B7)l8Py(=MkVsI89Am-gY5m3Gz4@RUJ7;Ohgr^ z@cKTN%ftpWx1_3B<&IDeMZiM>z4{Gt{W-Vho3(M36V9fx{ z;TI|Y*kfkhhzl_~(!BA*sX)RbDcjlPSy;D=&|1WD=K=h>C<`dEJ>9qze~CdntfoSu zivSJ`igt=FZ*STZaqltC`S~P^@U{a%kTH;eqfllDL~D2lisv1Fm}HQqBP+DD_g$3s zX4KKbgyHORqd{m9`uM}74O%oZb@3r5LS<2W)(Z=p)i7bYi*S#DIcj}Dr)44`)Zu;m zL%L}IwV=G@o4G6n%`k5;P(|+e$tI<|-`en+JD+Xwu0^Sl|9n!tqj4W>r(FH)I&muU zacM9knd#t76Y0F29gsryA1-odQA{>lJyy;OmtttYXmb;_m?qXm6L6NF1j2X1?Aecn zSGFF(Y;TLvA9!GxG#fP=`?>lSbe(@5##ixGR-EI48qV>Odj%$zP;H!{DYC~0>K+#C zQD;jXB7e7Y>h$Q?MOQXG>k-=bblr^H3v~I?5@$SXlu#h0{sS2FPlzWck z0Ry#9QnfWaL{yHD`qx$YCLb&|HG0QdfWQrSB4qM0-pamn8JhGg@|iaks4xqTVmM2k zaJsvb&smSl0n1-hJ5hqVj{TJ58pHh3Uc!-!mf$teC0q#kQF9@Q_!6 zJrrB~>4DzHfg!$fDv)Df+7rhmIeW=m!ik<`IjBBGuga*GJLUG(v<%08ha;ec*%vi8L+_-8F@N4^Ho~SFGP6Q zjkzV?x+Er>3}=Wh(+eHF?785A?w@v+)#`01c3R}DZQTcav$#HrT_93xeNb&}?G@+B z#KkB;qb)XgK$Px)A4dVy6++&VUSG{f&br{hT$PJKDXeaL2p4X4wz7E}E!Bs>O~)%f zK2P638Wvu(Rn1b7m)%ro3GdQM-3#QNHlUs`m9(zw{D|4Rc6L|ZtGJjwXUD$1vEUX$ zWi19ISsk#D>ViqU`fWBivj;!raA3g0<_ENgFUdbppEamRc)=#)C#8(}vTv^pO$T!M-OAWs*g`8F;>2W`#tSja z{@UKw{CJ`!;U%Cg_uU?kP#(K%-*NXN%f`V3VHJARgeGmzo<&(va`W)sGQ#gM4hr!$ zTT9^U#hwS+mS%c(a|x@Qk*EqBehwrb&pXjQI@;Gf-Qte+Qqo_}>4AGhYgr5m0TPe&)G z`I_tb)jj4hq7XU1ZphCrp=%%Nhtg?S$>L=LS2y}s-so|%BpM2khpZfD1dx}Ya-}}}vt!yDqAr(HTWcBxyl>h37*N_fhu$~t? z!Vd&Wa6&6TDaTwh1 zw-J4PCy^l3-WsXdR9H~z?Tf}p2~RFV8WlozLv0YyvIE>3(MqcR*77-Vj)dd}!9yo- zNdmIwrW+^|3O|kat?uvZGwZ#5Ee}A6T5Yg5^F86+_ zPIJee`Zm|JECfZ7ET3@#mF--ZE`0Wl+`V9uH;-|a?TdMjB8aoR!3Vex--1jFn?_fp zlyC9qc*Gn+iVAIti{gW8r4M^IIog08;^nviT5!`0v(_Y-nbo0tS9bQS#5ZbE4d}PJ z=bqnuAh#QmX%nN_Gm>Q()9s>~Yo3QZteBs{e|g~;d;U?cb%yVvC9nLwa-C}-_N_pO zuq~sj%X1`Tv_EM71|=X}U-ICka_`M{76*yNo`!C4d2!u7N5q$W$;-CQeJL>n!IjJZ zf7*NNpt`oLPm~CUI|O%li=3Nt z&v)PJS6yB8`gL_Rf20C??X~6_W6Uv!{)VM${H3%}o^5|IncTPh#I9g>_c-p7^5l^- zhl%cnjR*jB$(n~&@`T@e$6N{kQx+M+)R*wLy7i zveE?6*JY|cx^Znz@q0p0boYT%W~{8i1}?R}?5}d~e&mj=FItdqO)u7Tq05QNIdf=C zN%AdKc|zz%oL+o3`*`P}uB|2Q1cwd@ax^dD@O+g?$F$_I`A(*4N+i@!$lWf5J$I26N?an@KVLXEb?6q8;7N_x&nU%qf>A= z(TKi?1llVJnMFP*^DzQ}-MbuTq(b@fs022Iz(KYqvJ-?jvtP@n@522;a8_TFQBS`> zdlzBAkEvokd}P|Kwa9>1Otdl77NBlr6=o6dN(q)MkR^KW_8AzFHH|)S5${#XdH@L{ z5sxWg{u*waVO-plPKJ6o#YD7w)al^jP~a8Z(xZ$6L8wop8ww5ee&2|$g|)P{MS0!K zL6m(vI5U!9q~~0jWQB5t{*pp4!kfr}z}D_vhN(?CVNVlpfTl;WvvpJ=V{Dk=q(%6Q;UTzr^Dk z*bg)~bS9EAmsQ2!qZl1@kI{C+IWHj)tRD6axaox!jnv`W%W&IeyvpDI_)do7<>aDs z_>dfcurVO9wd@$v6dfU-o$g+=Q3a#`G}^Ph*TMC4)!$p^=-GZZX>4Fuj!j%2xKm zbYmaK3!+gjj9`U2V>)=j!7O*NL1E&|NWKh2(6NLJA5m91Q;h2}8Di1#tM0qP%Q6t@JZlanf`J3dPVUlgK7LuHqfY=}AFo7aWbc(h zPk`cwR(+s={ZRsdEcc8*kXd=`szJv2E4&EQCPWD!P?ZDK*#CnUin2$tX!UynH*W(f zEdKQz;5T%Yzn}1!UVr^BF?{_0|FjQZmRPU%8;E{-HN?~Tfu`WGR~`6y9@hxq25=PW zdcO8*9?K9#_AO8R3%piXp-=06LK)~+I(}HRkNnTi8r+Z9fOelLdY$~rEp*Ye-y{WY zh^}%B^cIbej+SMozcwEw3l#pH^aeg5&0QP%BB14>Wn1=9$U#VU(%wqV2hi}X=0;J)8Pflh@e}Y048%SsfjRiPC`WK#hX8QrAG3|Prl6+9d@zg_y;V$gD}D#FF|wdK0tq6lC0Mnoax_Y1IK z|5=jH2RLvGV}nck3s-=eKRZ6wISj>^60mE5e)}2iFE79}Co z$1)w*zbIf+U8=6$&NVms&Aw-he>Te-Q|6FRjr)H(?klJcyQ8Bl^8&@_a zr13;#TQu9*QN}q@Limx>$kqQKnV5<|U@1Jcyq|IcGJdqz3`3wVMIn z(=PlZvFAuyn><(@C@?waqqkY9yel#TIQoc$U8rjrG;?a|I; z%iV|VcN7#ZLc9tMF0bLhnOp;*q+U18(Rv;>nmy+fOxk%v`_1J#qX5dsmghrOllyx4 zk_A&{g1DFoNq9sWv{`?p`K_MLWus(I{BWDt$jNG$xhZ>W@j~r>xK+IKU)*8(`>DXd zA7k`U{dQIbIIyM=h8?C6yH${<^=Ml9&uBdhCSzung4;Ki&%4;+zy$;6J8A;8TZDj5 zV^`_85XV+&+5W5Gp{BZez(Vl1s`LG<|CD9?aeqI535}|;xz#@(-6tB^whvvbO{Xn^ zez!Rdf(37^iLivXiFLHEX8UsL_<=g&!8VQRE!9VxZn)5$+rO4b|7VH4*37nAp@R1f zBdQ~J&77b5E-W#ajFFG-<~RvFA^bPJ{(9g;`~|I$BTslRY(;~m%@xafo7issZmO%G z8@%K|3a@AK4MEc2>Tit$r~XG9Ge_x*Y+&TiqTmYpg9^$vts5(yqEnQiO;tam&B&v; zfuTxy=FXZ_)41;mWJh}`Ec<&*UEsj1kqY4vd+eLHv$%uyf5~9^9~mTVXzNU%zR%?B zD_XiQ(Qszm4RH#f_3$2#uj10>*J!@UoVnn?NxHNH=Et`eOHz$o-zQXwTbS@bUAzZU zHXK)9sS=F5xH}NFkmh11@?2ttf0Xe~QW<7nsQIH7gu{d&3%hS7{H8XZHi!zHt>~X> zHueww|JJYn?MvR#{!4qdQU0Oze_Nl@{C|9j4;CyqJQ8=kr#8Ks<$fwyZ$1qrsgahm zlT|xs3->10*?u&z929y?&7X%8yHb+cWYC^bt~&P%tDtBQ#e!=lkgvgfG~@OrRrXHK zVNTuCrP-lxwWYenn67(@I&+n&UvEO+bs+U-iV#RL==##_{&R;wwN0cJFaT#;*pNdlef%;5OO%(U?G z{(~&Kwm;}3Lx&9`QeR|lZAQk7NMrti1;j#ROOx5*@QCsEOsB>=Sxp65Xle@zl-XmFK@m1zCFWRH#xZJL7Vo>(&{50>4)F9WvW`8!N#3V3ky3}uW;^Ot zem539?dl@OY*Oj_O(BNA4`Fs)7K|#Awg9WNcno!WL<`L2vZvSB=-mxRpflfVhRp4z z9a;rRI3U3&4j^3ZDN1W0KhPXyO4p-QKx6)Xv@jgF+NU)LEd!S+H^!yJXM5o?;Ha`4 z3-ZG#NGvHHm@C#_jZ@WAgeb=nV9IQun`}h*Ga94z+q3H(+76 zDYfzK53Naq+5!Yc;XUBNvk0m{eM5{{OKOjT)I*9ET& zfNKAoo^E3$NeJhFcrvG#_$Wyy{w2Xwe}u1z@7H9Cg1sk#(8V43k@WS|5789KokEG-(jO7h2(HDi_x{`#obR}`>)bPJ(NW8X`?L2u31b}k&*bPV}*qV}EX;Wt)AugOF>-canE_jrbFWCJ#Ec3eEp?Vf2_;JiPZ>A_r$%}^qTHoGsG!lO|M zxGLFiM^Z?^x_VK>RlBHTcAmbQ-H+m9hy$4D>FjIPbd1&jZi=;ab_M9sp?&mL+NRd0 z{BvhORF#*cM7UU#Khz&jpTUgRXjF^w z(dYgz#yoJzBU~B7YEKWo9T0q>`cL~G08VoRo&Q3})!XDV`vio$`QL;63DOu#eglu68E4r9pS3^%c=i|lT!;QWc5cE@xS9B?NwziD>=`z1YZ2{AA6aK;X zdIYS!e!)-RSsu5HZ^GZ_AvyHBH)ld;QeL%c0jFl+I*uooU{R6cCcjqRB5K&m7^hs> z@xe19_avTr$80$g$dKKk`Nxg$eM$d$0q8GAn(^7$rOAtE5C^n$PvPyfKDmM7Ry`cJ zIL5-FpeWrv+-^?Y>T*rCJgxKB7CHU;wg!P?UHUWNWbF9ctj05AHef$rk*%ff&+w*3 zr3xtysnI#R>YOW^T1uUyN%lL!6F3{H3EyBTszZ)xrfubcGJpQ4 zp10FwrdgjgA2#LE7!}9rTx&@Fnui8z;a7aJrs##!8ap4pM zDwX3?zsd@U%@Mdv1=cicmzY>-I$A64T4~mBdP5@3o05(|4v18PR)@K|=6>cjyoW>s z0TGHpeG~9tR2lY?IA>2~juKrDjiaw-h*X%9-JKne_In5EAJYU+0Z{1G5Kd?iT6CR1 zicE|==Y}KwiB!X|7Lm=k+qR~mTaxOE=C=c0j^MBH#?sz~ObpMtq0EQ2;AVeNO*jVw zT{HNsZu^<%qy!La0bT+bs2=*4^Q^80>6OTy{dTHk@fMN3!j5C1#*Wf5)~%-dWBH3c zo(TWG=it%!#zh6LaQ)dN-~_^?2imDzEep*x2wLv9uO^sEj{Klw* zo5Bv#%-RyUByDVPU!bkBFHSl#e#$jRpn4~`qSPxLUf%sYg@8a$)uVtxJSXx{m0)!z z)76UgQB&y0|JkU>%+T*)oF@+SPka~a+0Z+xx(yl~P1#wEet3h_BuY7z&p4g1BA51+ zbL$0pkHp>Kz@Tgn+}Kv7K&1paD*AQwu6fStK@A2P&#x$*TG!IuIpHN0s>ib)$yCl( z#dP)+sA|DOJc_S1rZlLl5je5Z|s=qgb46e&o@tSpHjG{dc%b|L2nbVQ=&QMmk~U$lp*LFz7TuZ11-l z>%-mr`>Z9WQsE@?10ZSs)eiPMGX7UU_D=x&U$uU}u`+?M`TzOw|0g0mCx_OO3O(f0 zl>h^Jew*Wq_+lY(xJjg(VwMrdst}=7eZ;tb=N(A>g8Q5os+?@n|2wE|

iC-%XgT+onB_=QlF*Td3-=Uu&{p)CNhTYtV&5a(=kP65qL z=9~Tz^^n9dz>l9T{qGzMa<+bZGLi@Ir6$X6WEQc?9kpWZFTjAB1A1DF0=2VV3Sy)k zNoJPDH2wWVjG(G4Q`DBP=CSDO~8l^4>zaj#$6{h;m9aexZTI&bqS-up~S(}$~aOAqqyhn z*RcGk{$y20yo}Yb+WKW!;J=sNSD;mA2LR?t$iwgN??(=%9y-#3;lH@^#Z8Nibq^q2w5L!bHWmNXn>4J83EgMbSnFK`B6e+SQn zglcJNIUmfsPR6N)BpAxxAEti-(qt5-MI()N2lGGhLnd9Ww{wi<=+I+AXAmE`d)gz! z$!Rd0P2z(G87cDKLdpV0r`i(eDRjn7;T22dv_?g|z-)TwM>9 zl~q2dw*XL?yH)%E=)F(|jtyEYSehy6!knXG=zhvVL-^J0&ZwkOX1c;Zl4*0*R!MxO z!?hV1IVm3HzUvtsex|m*V0dFSU?qlUO?NURUmj z^J%bu2HuRe8{P!d@>oVr5ylZD&B=Zzr1HDA)oY z|M=lVs4HrwU|tdh%5nDNE{Y`=oqIj5_UDAEFY{9Y#jV0s4n;=zM=@d!!Nc7JQ`Kf zYMpMGqvAB%{aN=MIqam;qpQs>NN0;EW1~VCMMy4SdlXjC7#v*y{jz1lD_K8m0YGVV zW5(__=s$#lEwMplG|FpZJ4d`igWbVspSJ=?5y{grFkH@z7>b555{vEXK`5# z5*YymccWE)R_|s!Jc1mzQS#NSEBug~9C=`(jyxV@Z?Q>>eL{R!hvpt#*17DIh|dY{ z!gikBsSwWqfI%1MTEt8G^+-YnzXV-;U4>sl9iKvl0O&$%ZCwMi!V#udufh_#se)#@ z%aKK2LTf!&I;*QWjWs9`k!q~wsHVmdh;GAH@gPB2NhQY$R2U7yv2Q(fbS$5j!E;&7 zQdZwx?-sKK>O=m$snvN~Dwj%s7THpZ(IQ z(magR#9D&==dNmT80^cIWG;DRkNRp(&bVbT2Zuz#J)kM)>+EyNv5aC8=yidLSc!AFb@-!;!cW9ITU!c<8eyaxu98y0 zuB3?n)_P+j`t4iDN|gbCUL~^Fp$5kH;4g024t5yfQvj0-?f_w?+Z^^Z7^f;r3!E$% zO3>j~RHlFn84VNfL>Cp4(XHI<;swCnPgd3^8V)(Bt^h?eIX&5Qdpz*qJ|fVK-Xn|t z>ZOq!qT#D_|5V%9r?zw?Cxsz4b;iIXUPG!f>M&hE#Q^RzS1}^Xe<2$sE+h!*buV;OZvAL_O_+u~q_33YL&f_eGBSn~}@@iT- zRk=0oT?{jq+F4--*R&rg5I-vKz@IriPl;iebdF$5R+#Y+LH(t7Ty#=38t}GZJ<4mm zqDi8l66Nc$qr$f3cR}s&Pir!tm?DtBPcM`==I1wO_KYN0q8jWWoh*Vd`jVqgfyqI| zxz-1tnPPq_80`oJHt7_dP0m_=JR#td?`!K@cJ3-ygTJoV+}TZxy!;{5UH!rOh2<@P zCCpJ|eK2)M;8BWz`|Rv2fS=b{nNdV60}lwD7@u~dMM|VjkG_c6 zpe*~DI9R8V{ooLtX}Uz~V!-tz002kDWCA421j21{YBrjWOJ^<-_DkC+bJYIZi2jP- ztw-6FVUYUEGLWHY=f>VV&|ooDRQA1Xgj*Ujy)n!jl`3GxXmW9pF%SxzDwhR%U2gNj zk1?>1@y8zF$3@3g@Bpm9_9~7_)d1Oxm5J`Lpz5E!vE5tFqH6o7DC*IW>fI@WBS}YbAnnHurmQ!v zG7tGtOV|UdL_x%Fx;~91EAk$Y8fhsxScA;Tg9WLITMp_IIBhr++cH>M+$Qq@859H{ zs->QepVd3x>B3Q6OzP)xJ1q9i`=kQ9X`&>k|4-+FoN^{DNH2uRgG%X50r2? zACbST$OO&>qXS>-xJm__5g8FwKqUWkV&ejcp>rQYK2qL>Pq+U zrP>=_f6$$KuP!OjZI`nj#wq?thHF~)R=;3?3?Ig)m8_F-QRpmCyRb8W_g{(Yru`V* zTbhx-P`-!`y#J`a$OEd~Jd%#p(1W-8^uz2_^!`KDZFvGN^E(W@=LUC(qev5YoKy-G z`s&{?gOrZ3rV4)fl~Wy|;$aABE?OgBgTw_UQZ`x9n zxd^L-e5+O8j|@|gc43|$R`_n`UgS>R%oxV2p&yqgLoZ9qesy-*TwJXjdV2#{Knf z-bM}vlb|oIc6P1>oAu!aA#0MHc8Uerp~egc=H9$Up~p#o;6i0rYu%5P_F7e!gWNWh zCPRDMN_bgpDX9pA&~F~nJj{Kdp`0v7#D)hva**WnwTMhjH^w<{!P{UE+!I&^YAP?J zT46Bw!Et}~;?pOgyHjx3a?9;X=+~Be$BTjzU`&=f%~ekf^=5g-vYN~N&A~zflU}pS z(TaD=&4Ks9lFjYD%>pn3=l+%_cK+_xKIizDjID)6pNgu}7P`}!IrHv^&=wlC9ZLQW zA1>qbpF`QJYImpl%>4ot`kW}q<)DYQ_!gG_^_KAc2Gc|+G~73jT>w9#&i|`~UObWS z8~yKyrj?|GGkZV31@&B6IwYU#g%`cApvB7Wq3{kZk4j>UYmt3TVI(o}q+cUUQ%!s! zxzoUj7vCq==UeY;2|w|24Nl}0IpQcuId2O;#D_wZRZVY-taqwDa_7vw=%CFf2AC{{ zu2wP)o^89w^x4v5(!$oq{!l<&$J`qebr)f=?-1*!Bpg^wBqu4^&OAS2ebT28n{zi^ zHz>W*b(6MHAeH@<%V<@?CE$B zZZ%nuAZ(I9%L{A-WkS{2S$~p#Op;b?1Z`>7$PEwt_OKCM(JMY_K~uvFbzIKmV9+VH zT+6VKs>Z#|XlPnwDh%K2$S1wd%196hmMe8T!FFz)KJ~?Qmn;ODf_VSRgGmBj0mbG! za){AuC;RJg5vkEjbGT|@*nsD2JcW9*W5~}er33nDBFwOcyv5*2*eSmWo zBBDfS&ynndJ>fNcNpgl1G7y~uqP*1PYxj70d?N#8?r(lm?Hdd-<0H?iR1yQTa_RSH zElLrWM-VwZLoBDJDwV8-Qx3RIwhyjYeJTrSRldGEbZ)uc0f5@^P(QgGbO2c&>$`QP zmLfI^u7}H>!{(cVu29_8r!eI8SnYeQ}?yVno6%hr7004PTOY32=p|zg6DgwW0UszF81n5j&WcUs72gzpqLdIA{>Zs7 zZm>BbXCZYpLR1d&Tnw?NpCPkCVHq~Sz?;{L&RVuN*vPbY99p$$I8ovh6*+gq*?2FJ zFc{d=U_PC_#I7PD7>SvU2@;+%st5@`na1*ot+MlG7I@G5)$k;E-*P^JetBG5OjL07 zOEPW(zYq&XIugmL(g%EModQXn;w=FxeFq1eglu^ zNz~@`1_z74Gk%!0@ib9(FedU}z-qjnEVpoHRF|JD%c_~-N#}iD;yxD5er#~aN)rLQ zT4}p*?L8Fq5qvR3NAT9e=1!N155+fcY9b+J4O29|#S3paUOe02;$XmHQs4vv*U@)q zogbn*5v6eK{pmv(YFp}$lY!Vd{N3T z0r55fxi3^eVCQFbXQ>F%ULj@0a$~z}n@nFF-s_AClTOmtluoOUqtr=77hi=g*e3tP z6!sdf>vUZ^O8>bfV@qgaSF|WV zEGsYR-?TBM`PtH8>sh9Ms+OV2igX7euG^XsS}rMG5JFl65?^F+u!grJF_%l7lKVVZ zkLZF%ztB_aYRIJpq%j@Ozhb{HAA6ezAF4KGQ-JA+jkBgJ{ObA4dD{mMUZNK1xw#eb ztcIC8B`C<38GLrmXd=YLcXJ`L($i4>YdM>e#Wy+FODb9Qz4P>$&3MyBHb0|GD6eo` zw4e#zp^(o*M!l8_n-TmQ?0}c+MW1wziv6v@<45mw)~pOjBX`l}s_019J~u+6j`u_` z)<+LDh}!7Tz``&3t|dT~dY+V5O0#-(b7&C}i^F4levZN4o}-x5$(Xozv&36yIqyso zsMA(zIL{ZTLJ1LMoo7o{Uz>eD6P(FkC{TMFlIKt)zhAzA&x{>|zFqj#>*?pO5&NG{ zY)wTU6j*a5FjgEDUOUU@n0}#uW_!BkV_?uW-Y+h@l5p03+9`*Xy!EQt|8bj@YDGqFg;!x|B+xGVNYMCN^%JKIoW_`G2KiiQwvT!*${lDD5R0dJx!YwUfx zR?425ctPJ;;2?W;=Bo8wqDxsSCVNY2bskDyCthq&ixm#wQagOJ2ccD@^Fz!{nA9ot-a!TJS-TKdLHe^xU=A@W^?5@(C^?K6$dsS= zlBLauFkWUk?`plj=BJ_}rJl19lrBQJu$KuS=ksx`qbHAwCgd9AhZwwSK`^<8q`I$zv?R-s~YQKzMwS1j;F-km~p&9 zsh=g>Nf06SW-tHYDBQ_ME*=A;?jNx2`=%-#-BC6*IBo7-UwHioxmIS-S$yY}T^94q zTo5z%vE1F?N*3uH`^CleG@(yZLpXjoSA7Cid zPbxDM7_bSSjrY~Cr|QrAs8xH}v_%{C6d&=$w4JW@@k9$ueyI4iA8ZqQ9{a>KLa@1; z)~yP|$5z|3tSTKoq#jV{FQVt;Id*TOc3BpOPwRKhxYp~im_CTPQs0gfyFJ->Ar2E8 zOe-bQs;qJSbWoDP_F5n3O#Sr`>MrSuLgds>PuiyT6n~gTYZ*V*vY#m0)hjgm##Y1d zC@gT*E4VO|YHrX4&ld=gGV2$734FZby5+mXWgN@*1@c#Dd-#+O&Y&J&_EsjzEG=s` zbo)3Dm#DV3Kh? zvGwUVN%iWzMy;UH0kO^g+cIHAyi8I=rwE;B`Cj~Y7zy@_wodfaRPiG{x4(7rh7~1> z%8E%043~q!(Cr7)CpiRR@#{Gibm7f}7RI0#D3#?Cl<0LQCq8&TLksySqQuE6tR?*P z@)ta5V8SBY7~o7qI{r0rhKZB0tVu~j+T24Ej_IHl@3HK_yj!`6d$;UNh z@_{Uewj+sM^g?z%kViZ(@~V;Tx~dx>Eq;HBGZuprR`aVfit*O2hrx)$#1myK6E15) zbMc-OR`*_U8Skbf366O^EU`2Kw>Q=Uc>pBUjdUIH@u$p}Gut0&Bri~0R9`JCVG;}x zwfAD*2=tnhh*t_r2eS;T)H|SRHHj_h14cNJxzD1Z#VpM zIAXoC^Y{u%!g><=^t8(aq4Tr!oZjVTAmcTuleuHXOehbFYiQXei;e);j@W2Gla~lPi}n6FU*6) z2^TpxEyD0UC-{uIjRwMBKMm;&L1-Nxp@u+QePD#&1RBh2_|}Jg=I0pXx;qH*$6f%Qw~wc0?2b>raR(Yra`vpKJ>U zDa>X?5e1GsXUXFrBjP7@Zs+=DOuoULGaOl(zuZFJOBX-%M^mC$p61z!vtzLw4DhHP z=X*ct{b{rx{-kNb*6e9SY9}qhD#0PaReJOn0TpP@SV@tyUx5+M2jvu**H@-%;;z)E zmEsiWvB3qfzZq>mz=Fz4;jM9LO-HpBK3zC9uZUBRWqM1CRG}ePs9e8bEAITg2$W}X z3-=U!_R-HSQE<-kfX_y1oatkO^3xo>$&|hpyS4$LSA={UMayxkXhl<{5u4AOwW=CTYH{8+`P=&PbnrM zbga{~%sZ$wK^(}!xs5QrcEybG>VyNq!Rse1Lhm+kVYfnu(D4wPw6_vb6KBkY`}!q{ z?4gI&MOo;__T<9`tLic+m2138Y;=fX2eubrUDh89{C1_ySFS_le4zM{9I@0~= ztBK?Id_7Ydw&Ywj8|8Y@l;VLT3Hjob;460c{PHHOr!nGnKZ6qEJf*FwP`pImnSVgf z&wMkf^EtpK7M)%BhX{RSw-ZQ2scxK(H}7D5KcTHwYR+^O|TT3L>cBh-(9P2jVx0>cfjZt(KeqCq) zBeE_S@}CyIUY(dc+ge#{}r8m%+;TEM*iOs~*BLkc%eIsTZyHI15)xl$P*pwbK zj3+MF&r~-P;1OT~-r>a;bxS!X$-zJWsZqM4F0O1_VSAnX3bkWEA)n-9%O(1e6Gg6q zPqXNR>#3xPrWPZ-zC$Oe+&PA%h-@uy->v$-@OYHHIH3%+aCCkHC1{C;%q;uE6BYhQ zL;4A;+djdgCGP&wF85I^O{P{Bq0ekDJ9$3eir*#7h+v@b{DY<94$GwT(~bA{*?PEw zPDKn6>0Pi`b?)SiUi$`59?xTyWGbe9zK>t3d?vLSVE7|MP*~T;UyCLmn~~B^hYTyg zJaUvy&wf6G1&`oGwh-rY&<@DOWIajT-PX1S}R&o{j7l+l`l-im|< zn8;xwUiN5+jej4m;2pt#_8dlp6}dm&XJGhA!Q9u?;QE^6dSnaBioP_8NE%!OIgr`P zzJx=1i`5+Le#a+d4)Nkic{8n^oUd33Rt0b0Ae#+&SQEv|!6X)szLdI@0EsXhC~>*KOtH&Av4V>qQ~_1ch+5IoarC$u+JI z@;#A5I{KbPr3fV-F@g`>&(2Nh?b}efC&FJ&PEawCYH3HZe%e$KM4rf*?em;CW72>n zk)Bd+8p^LH$fND9e^Xi5y|8V&Ez{&xd_I++bTeqeWz6s;PVVNSzwdpO`$SLGwLe(R z+(p5{iChBDw-swU-gaGWc#2ponvDw}?khq%L+zKDqZ`ze>ODyBqy+=lpS#-n3yu}O zXdBW%N_8V${=G*X*qsqhs=vO%GGlm^)fuKZn78doh;X$@y^nElSZheq)RxPaMmog| zlWrHj^%!L_;$bQC5Zw4Qc$?wFYD2;7LZ<4ErbG21)W&+goiuvFoxBs@6vIFcwC+w0 z%m~mSwcFf$!pUsXFe3z}e5!%PgwtPlQk;zS$kveg@f7~;?^r56b~ql$-DmWRus_{? zeo7T^4JhA#gYg8B@{lq+g(DCFVSrhs*axxvV{*8NXm>Upp1lNPkV_7RXW&Yr&~3_$ zJ$&b0){yMxH(`8`va3rkN}3|!X?>B+zKRoP=27Hgu)e9|JCDG3SORh*Z^1O*%cOdd zewq1I_h=D!wBC$i)k)Um{M>ux6A?b)T${*+gt#^E`b$LshB!O-~70A+-7>_%1dRfwydI#2Z5zU9Stcnz4Kd;g51syK$ zTQ%Q{tlE(N7wMp=C#I}DB}3gq_{blM;vHLPBYlh_9R|b*DAbX}hv#9K9$?$cQkPw} zp8(r2JhBcZHnL4W2$yM#Vki-#+?z`#*v~WzlksKQ=sZ~%e$56Q>_$iS{3pCAO)+Vy zX6vSLu!Kc&dPIa_(@7DHbEL7s1d#%|1)h&WorD7rk(en~yz+Z_kN&6g59SI4%(f&0o3tpoEd1yN<-<40<%) zF!nFMi!d{pRFAl5(t9g@TI2VAdGpCmUFyTz9zOm4uWcDV!Vx^W<9U>Fx&N+IFd@GW&Z?qbQh3zl6Jher$!;X^wYq ze>p%sSE#yVZ}{Sl!zJ=_DJIt<3S;AS_Q%D8eFoyXeQV3S-FS(KD{H#mE4ZKLYN0cu z;Gx9no0ud2I^Ysk%InxA^>t$-=Et#7E-;l6%jj-$pxk*pN%kY<+w=pTM|-Py{TB6e&pqsSxE7zLnY08=Tt>ga@%pGRK0YJJ{3VwUb1MN0 z)63l{u~v@>_wUk*i=)f3j;oc6iOxB-I z`Q_K+#s?McjIDN2Q)FiMPBW%tpL@^}F>V!xn`VHl&}CGC_{ef}E*h_Z^Fo02P>8I} z=IW_plhHgy(&3NL1y(1<9%7%pgoO=%Pe)v%Kf2)=lOK>o<^JaVLCQVRXW>}n539EmGXo&I*{nd0UA_mVUrfJt)%>$aX|L}RP84mel!?TpQoLI4l HzTf`^Bu^YP diff --git a/docs/src/developers_guide/find_main.png b/docs/src/developers_guide/find_main.png deleted file mode 100755 index 8a7af306cda6ef7b4035fd2ce522fa84429edf80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38567 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 diff --git a/docs/src/developers_guide/fork_banner.png b/docs/src/developers_guide/fork_banner.png deleted file mode 100755 index 0d140c9fc6d8f10daffe136df1915e868a30d1a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18871 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 BAqxNi diff --git a/docs/src/developers_guide/imagehash_index.rst b/docs/src/developers_guide/imagehash_index.rst deleted file mode 100644 index a11ae8a531..0000000000 --- a/docs/src/developers_guide/imagehash_index.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. include:: ../common_links.inc - -.. _testing.imagehash_index: - -Graphical Test Hash Index -************************* - -The iris test suite produces plots of data using matplotlib and cartopy. -The images produced are compared to known "good" output, the images for -which are kept in `scitools/test-iris-imagehash `_. - -For an overview of iris' graphics tests, see :ref:`testing.graphics` - -Typically running the iris test suite will output the rendered -images to ``$PROJECT_DIR/iris_image_test_output``. -The known good output for each test can be seen at the links below -for comparison. - - -.. imagetest-list:: \ No newline at end of file diff --git a/docs/src/developers_guide/propose_changes.png b/docs/src/developers_guide/propose_changes.png deleted file mode 100755 index d4e367ce1db71d34d652c1750be9e4610b4dc536..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24197 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 + that are missing, then please let us know by raising a `GitHub Documentation Issue`_ on `SciTools/Iris`_. * :doc:`metadata` * :doc:`lenient_metadata` * :doc:`lenient_maths` -* :ref:`ugrid` +.. _GitHub Documentation Issue: https://github.com/SciTools/iris/issues/new?assignees=&labels=New%3A+Documentation%2C+Type%3A+Documentation&template=documentation.md&title= .. _SciTools/iris: https://github.com/SciTools/iris diff --git a/docs/src/further_topics/lenient_maths.rst b/docs/src/further_topics/lenient_maths.rst index 818efe4763..643bd37e76 100644 --- a/docs/src/further_topics/lenient_maths.rst +++ b/docs/src/further_topics/lenient_maths.rst @@ -84,10 +84,10 @@ represents the output of an low-resolution global atmospheric ``experiment``, forecast_reference_time 2009-09-09 17:10:00 time 2009-09-09 17:10:00 Attributes: - Conventions 'CF-1.5' + Conventions CF-1.5 STASH m01s00i004 - experiment-id 'RT3 50' - source 'Data from Met Office Unified Model 7.04' + experiment-id RT3 50 + source Data from Met Office Unified Model 7.04 Consider also the following :class:`~iris.cube.Cube`, which has the same global spatial extent, and acts as a ``control``, @@ -103,9 +103,9 @@ spatial extent, and acts as a ``control``, model_level_number 1 time 2009-09-09 17:10:00 Attributes: - Conventions 'CF-1.7' + Conventions CF-1.7 STASH m01s00i004 - source 'Data from Met Office Unified Model 7.04' + source Data from Met Office Unified Model 7.04 Now let's subtract these cubes in order to calculate a simple ``difference``, @@ -129,8 +129,8 @@ Now let's subtract these cubes in order to calculate a simple ``difference``, forecast_reference_time 2009-09-09 17:10:00 time 2009-09-09 17:10:00 Attributes: - experiment-id 'RT3 50' - source 'Data from Met Office Unified Model 7.04' + experiment-id RT3 50 + source Data from Met Office Unified Model 7.04 Note that, cube maths automatically takes care of broadcasting the dimensionality of the ``control`` up to that of the ``experiment``, in order to @@ -218,7 +218,7 @@ time perform **strict** cube maths instead, Scalar coordinates: time 2009-09-09 17:10:00 Attributes: - source 'Data from Met Office Unified Model 7.04' + source Data from Met Office Unified Model 7.04 Although the numerical result of this strict cube maths operation is identical, it is not as rich in metadata as the :ref:`lenient alternative `. diff --git a/docs/src/further_topics/metadata.rst b/docs/src/further_topics/metadata.rst index 1b81f7055c..79e9c164a0 100644 --- a/docs/src/further_topics/metadata.rst +++ b/docs/src/further_topics/metadata.rst @@ -38,8 +38,8 @@ Collectively, the aforementioned classes will be known here as the Iris .. hint:: If there are any `CF Conventions`_ metadata missing from Iris that you - care about, then please let us know by raising a :issue:`GitHub Issue` - on `SciTools/iris`_ + care about, then please let us know by raising a `GitHub Issue`_ on + `SciTools/iris`_ Common Metadata @@ -120,10 +120,10 @@ For example, given the following :class:`~iris.cube.Cube`, Cell methods: mean time (6 hour) Attributes: - Conventions 'CF-1.5' - Model scenario 'A1B' + Conventions CF-1.5 + Model scenario A1B STASH m01s03i236 - source 'Data from Met Office Unified Model 6.05' + source Data from Met Office Unified Model 6.05 We can easily get all of the associated metadata of the :class:`~iris.cube.Cube` using the ``metadata`` property: @@ -990,6 +990,7 @@ values. All other metadata members will be left unaltered. .. _CF Conventions: https://cfconventions.org/ .. _Cell Measures: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#cell-measures .. _Flags: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags +.. _GitHub Issue: https://github.com/SciTools/iris/issues/new/choose .. _mapping: https://docs.python.org/3/glossary.html#term-mapping .. _namedtuple: https://docs.python.org/3/library/collections.html#collections.namedtuple .. _namedtuple._make: https://docs.python.org/3/library/collections.html#collections.somenamedtuple._make diff --git a/docs/src/further_topics/ugrid/data_model.rst b/docs/src/further_topics/ugrid/data_model.rst deleted file mode 100644 index 4a2f64f627..0000000000 --- a/docs/src/further_topics/ugrid/data_model.rst +++ /dev/null @@ -1,566 +0,0 @@ -.. include:: ../../common_links.inc - -.. _ugrid model: - -The Mesh Data Model -******************* - -.. important:: - - This page is intended to summarise the essentials that Iris users need - to know about meshes. For exhaustive details on UGRID itself: - `visit the official UGRID conventions site`__. - -Evolution, not revolution -========================= -Mesh support has been designed wherever possible to fit within the existing -Iris model. Meshes concern only the spatial geography of data, and can -optionally be limited to just the horizontal geography (e.g. X and Y). Other -dimensions such as time or ensemble member (and often vertical levels) -retain their familiar structured format. - -The UGRID conventions themselves are designed as an addition to the existing CF -conventions, which are at the core of Iris' philosophy. - -What's Different? -================= - -The mesh format represents data's geography using an **unstructured -mesh**. This has significant pros and cons when compared to a structured grid. - -.. contents:: - :local: - -The Detail ----------- -.. - The diagram images are SVG's, so editable by any graphical software - (e.g. Inkscape). They were originally made in MS PowerPoint. - - Uses the IBM Colour Blind Palette (see - http://ibm-design-language.eu-de.mybluemix.net/design/language/resources/color-library - ) - -Structured Grids (the old world) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Assigning data to locations using a structured grid is essentially an act of -matching coordinate arrays to each dimension of the data array. The data can -also be represented as an area (instead of a point) by including a bounds array -for each coordinate array. :numref:`data_structured_grid` visualises an -example. - -.. _data_structured_grid: -.. figure:: images/data_structured_grid.svg - :alt: Diagram of how data is represented on a structured grid - :align: right - :width: 1280 - - Data on a structured grid. - - 1D coordinate arrays (pink circles) are combined to construct a structured - grid of points (pink crosses). 2D bounds arrays (blue circles) can also be - used to describe the 1D boundaries (blue lines) at either side of each - rank of points; each point therefore having four bounds (x+y, upper+lower), - together describing a quadrilateral area around that point. Data from the - 2D data array (orange circles) can be assigned to these point locations - (orange diamonds) or area locations (orange quads) by matching the relative - positions in the data array to the relative spatial positions - see the - black outlined shapes as examples of this in action. - -Unstructured Meshes (the new world) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -A mesh is made up of different types of **element**: - -.. list-table:: - :widths: 15, 15, 70 - - * - 0D - - ``node`` - - The 'core' of the mesh. A point position in space, constructed from - 2 or 3 coordinates (2D or 3D space). - * - 1D - - ``edge`` - - Constructed by connecting 2 nodes. - * - 2D - - ``face`` - - Constructed by connecting 3 or more nodes. - * - 3D - - ``volume`` - - Constructed by connecting 4 or more nodes (which must each have 3 - coordinates - 3D space). - -Every node in the mesh is defined by indexing the 1-dimensional X and Y (and -optionally Z) coordinate arrays (the ``node_coordinates``) - e.g. -``(x[3], y[3])`` gives the position of the fourth node. Note that this means -each node has its own coordinates, independent of every other node. - -Any higher dimensional element - an edge/face/volume - is described by a -sequence of the indices of the nodes that make up that element. E.g. a -triangular face made from connecting the first, third and fourth nodes: -``[0, 2, 3]``. These 1D sequences combine into a 2D array enumerating **all** -the elements of that type - edge/face/volume - called a **connectivity**. -E.g. we could make a mesh of 4 nodes, with 2 triangles described using this -``face_node_connectivity``: ``[[0, 2, 3], [3, 2, 1]]`` (note the shared nodes). - -.. note:: More on Connectivities: - - * The element type described by a connectivity is known as its - **location**; ``edge`` in ``edge_node_connectivity``. - * According to the UGRID conventions, the nodes in a face should be - listed in "anti-clockwise order from above". - * Connectivities also exist to connect the higher dimensional elements, - e.g. ``face_edge_connectivity``. These are optional conveniences to - speed up certain operations and will not be discussed here. - -.. important:: - - **Meshes are unstructured**. The mesh elements - represented in the - coordinate and connectivity arrays detailed above - are enumerated - along a single **unstructured dimension**. An element's position along - this dimension has nothing to do with its spatial position. - -A data variable associated with a mesh has a **location** of either ``node``, -``edge``, ``face`` or ``volume``. The data is stored in a 1D array with one -datum per element, matched to its element by matching the datum index with the -coordinate or connectivity index along the **unstructured dimension**. So for -an example data array called ``foo``: -``foo[3]`` would be at position ``(x[3], y[3])`` if it were node-located, or at -``faces[3]`` if it were face-located. :numref:`data_ugrid_mesh` visualises an -example of what is described above. - -.. _data_ugrid_mesh: -.. figure:: images/data_ugrid_mesh.svg - :alt: Diagram of how data is represented on an unstructured mesh - :align: right - :width: 1280 - - Data on an unstructured mesh - - 1D coordinate arrays (pink circles) describe node positions in space (pink - crosses). A 2D connectivity array (blue circles) describes faces by - connecting four nodes - by referencing their indices - into a face outline - (blue outlines on the map). Data from the 1D data array (orange circles) - can be assigned to these node locations (orange diamonds) or face locations - (orange quads) by matching the indices in the data array to the indices in - the coordinate arrays (for nodes) or connectivity array (for faces). See - the black outlined shapes as examples of index matching in action, and the - black stippled shapes to demonstrate that relative array position confers - no relative spatial information. - ----- - -The mesh model also supports edges/faces/volumes having associated 'centre' -coordinates - to allow point data to be assigned to these elements. 'Centre' is -just a convenience term - the points can exist anywhere within their respective -elements. See :numref:`ugrid_element_centres` for a visualised example. - -.. _ugrid_element_centres: -.. figure:: images/ugrid_element_centres.svg - :alt: Diagram demonstrating mesh face-centred data. - :align: right - :width: 1280 - - Data can be assigned to mesh edge/face/volume 'centres' - - 1D *node* coordinate arrays (pink circles) describe node positions in - space (pink crosses). A 2D connectivity array (blue circles) describes - faces by connecting four nodes into a face outline (blue outlines on the - map). Further 1D *face* coordinate arrays (pink circles) describe a - 'centre' point position (pink stars) for each face enumerated in the - connectivity array. - -Mesh Flexibility -++++++++++++++++ -Above we have seen how one could replicate data on a structured grid using -a mesh instead. But the utility of a mesh is the extra flexibility it offers. -Here are the main examples: - -Every node is completely independent - every one can have unique X andY (and Z) coordinate values. See :numref:`ugrid_node_independence`. - -.. _ugrid_node_independence: -.. figure:: images/ugrid_node_independence.svg - :alt: Diagram demonstrating the independence of each mesh node - :align: right - :width: 300 - - Every mesh node is completely independent - - The same array shape and structure used to describe the node positions - (pink crosses) in a regular grid (left-hand maps) is equally able to - describe **any** position for these nodes (e.g. the right-hand maps), - simply by changing the array values. The quadrilateral faces (blue - outlines) can therefore be given any quadrilateral shape by re-positioning - their constituent nodes. - -Faces and volumes can have variable node counts, i.e. different numbers of -sides. This is achieved by masking the unused 'slots' in the connectivity -array. See :numref:`ugrid_variable_faces`. - -.. _ugrid_variable_faces: -.. figure:: images/ugrid_variable_faces.svg - :alt: Diagram demonstrating mesh faces with variable node counts - :align: right - :width: 300 - - Mesh faces can have different node counts (using masking) - - The 2D connectivity array (blue circles) describes faces by connecting - nodes (pink crosses) to make up a face (blue outlines). The faces can use - different numbers of nodes by shaping the connectivity array to accommodate - the face with the most nodes, then masking unused node 'slots' - (black circles) for faces with fewer nodes than the maximum. - -Data can be assigned to lines (edges) just as easily as points (nodes) or -areas (faces). See :numref:`ugrid_edge_data`. - -.. _ugrid_edge_data: -.. figure:: images/ugrid_edge_data.svg - :alt: Diagram demonstrating data assigned to mesh edges - :align: right - :width: 300 - - Data can be assigned to mesh edges - - The 2D connectivity array (blue circles) describes edges by connecting 2 - nodes (pink crosses) to make up an edge (blue lines). Data can be assigned - to the edges (orange lines) by matching the indices of the 1D data array - (not shown) to the indices in the connectivity array. - -.. _ugrid implications: - -What does this mean? --------------------- -Meshes can represent much more varied spatial arrangements -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The highly specific way of recording position (geometry) and shape -(topology) allows meshes to represent essentially **any** spatial arrangement -of data. There are therefore many new applications that aren't possible using a -structured grid, including: - -* `The UK Met Office's LFRic cubed-sphere `_ -* `Oceanic model outputs `_ - -.. todo: - a third example! - -Mesh 'payload' is much larger than with structured grids -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Coordinates are recorded per-node, and connectivities are recorded per-element. -This is opposed to a structured grid, where a single coordinate value is shared -by every data point/area along that line. - -For example: representing the surface of a cubed-sphere using a mesh leads to -coordinates and connectivities being **~8 times larger than the data itself**, -as opposed to a small fraction of the data size when dividing a spherical -surface using a structured grid of longitudes and latitudes. - -This further increases the emphasis on lazy loading and processing of data -using packages such as Dask. - -.. note:: - - The large, 1D data arrays associated with meshes are a very different - shape to what Iris users and developers are used to. It is suspected - that optimal performance will need new chunking strategies, but at time - of writing (``Jan 2022``) experience is still limited. - -.. todo: - Revisit when we have more information. - -Spatial operations on mesh data are more complex -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Detail: :doc:`operations` - -Indexing a mesh data array cannot be used for: - -#. Region selection -#. Neighbour identification - -This is because - unlike with a structured data array - relative position in -a mesh's 1-dimensional data arrays has no relation to relative position in -space. We must instead perform specialised operations using the information in -the mesh's connectivities, or by translating the mesh into a format designed -for mesh analysis such as VTK. - -Such calculations can still be optimised to avoid them slowing workflows, but -the important take-away here is that **adaptation is needed when working mesh -data**. - - -How Iris Represents This -======================== - -.. - Include API links to the various classes - - Include Cube/Mesh printout(s) - -.. seealso:: - - Remember this is a prose summary. Precise documentation is at: - :mod:`iris.experimental.ugrid`. - -.. note:: - - At time of writing (``Jan 2022``), neither 3D meshes nor 3D elements - (volumes) are supported. - -The Basics ----------- -The Iris :class:`~iris.cube.Cube` has several new members: - -* | :attr:`~iris.cube.Cube.mesh` - | The :class:`iris.experimental.ugrid.Mesh` that describes the - :class:`~iris.cube.Cube`\'s horizontal geography. -* | :attr:`~iris.cube.Cube.location` - | ``node``/``edge``/``face`` - the mesh element type with which this - :class:`~iris.cube.Cube`\'s :attr:`~iris.cube.Cube.data` is associated. -* | :meth:`~iris.cube.Cube.mesh_dim` - | The :class:`~iris.cube.Cube`\'s **unstructured dimension** - the one that - indexes over the horizontal :attr:`~iris.cube.Cube.data` positions. - -These members will all be ``None`` for a :class:`~iris.cube.Cube` with no -associated :class:`~iris.experimental.ugrid.Mesh`. - -This :class:`~iris.cube.Cube`\'s unstructured dimension has multiple attached -:class:`iris.experimental.ugrid.MeshCoord`\s (one for each axis e.g. -``x``/``y``), which can be used to infer the points and bounds of any index on -the :class:`~iris.cube.Cube`\'s unstructured dimension. - -.. testsetup:: ugrid_summaries - - import numpy as np - - from iris.coords import AuxCoord, DimCoord - from iris.cube import Cube - from iris.experimental.ugrid import Connectivity, Mesh - - node_x = AuxCoord( - points=[0.0, 5.0, 0.0, 5.0, 8.0], - standard_name="longitude", - units="degrees_east", - ) - node_y = AuxCoord( - points=[3.0, 3.0, 0.0, 0.0, 0.0], - standard_name="latitude", - units="degrees_north", - ) - - edge_node_c = Connectivity( - indices=[[0, 1], [0, 2], [1, 3], [1, 4], [2, 3], [3, 4]], - cf_role="edge_node_connectivity", - ) - - face_indices = np.ma.masked_equal([[0, 1, 3, 2], [1, 4, 3, 999]], 999) - face_node_c = Connectivity( - indices=face_indices, cf_role="face_node_connectivity" - ) - - def centre_coords(conn): - indexing = np.ma.filled(conn.indices, 0) - x, y = [ - AuxCoord( - node_coord.points[indexing].mean(axis=conn.connected_axis), - node_coord.standard_name, - units=node_coord.units, - ) - for node_coord in (node_x, node_y) - ] - return [(x, "x"), (y, "y")] - - my_mesh = Mesh( - long_name="my_mesh", - topology_dimension=2, - node_coords_and_axes=[(node_x, "x"), (node_y, "y")], - connectivities=[edge_node_c, face_node_c], - edge_coords_and_axes=centre_coords(edge_node_c), - face_coords_and_axes=centre_coords(face_node_c), - ) - - vertical_levels = DimCoord([0, 1, 2], "height") - - def location_cube(conn): - location = conn.location - mesh_coord_x, mesh_coord_y = my_mesh.to_MeshCoords(location) - data_shape = (conn.shape[conn.location_axis], len(vertical_levels.points)) - data_array = np.arange(np.prod(data_shape)).reshape(data_shape) - - return Cube( - data=data_array, - long_name=f"{location}_data", - units="K", - dim_coords_and_dims=[(vertical_levels, 1)], - aux_coords_and_dims=[(mesh_coord_x, 0), (mesh_coord_y, 0)], - ) - - edge_cube = location_cube(edge_node_c) - face_cube = location_cube(face_node_c) - -.. doctest:: ugrid_summaries - - >>> print(edge_cube) - edge_data / (K) (-- : 6; height: 3) - Dimension coordinates: - height - x - Mesh coordinates: - latitude x - - longitude x - - - >>> print(edge_cube.location) - edge - - >>> print(edge_cube.mesh_dim()) - 0 - - >>> print(edge_cube.mesh.summary(shorten=True)) - - -The Detail ----------- -How UGRID information is stored -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -* | :class:`iris.experimental.ugrid.Mesh` - | Contains all information about the mesh. - | Includes: - - * | :attr:`~iris.experimental.ugrid.Mesh.topology_dimension` - | The maximum dimensionality of shape (1D=edge, 2D=face) supported - by this :class:`~iris.experimental.ugrid.Mesh`. Determines which - :class:`~iris.experimental.ugrid.Connectivity`\s are required/optional - (see below). - - * 1-3 collections of :class:`iris.coords.AuxCoord`\s: - - * | **Required**: :attr:`~iris.experimental.ugrid.Mesh.node_coords` - | The nodes that are the basis for the mesh. - * | Optional: :attr:`~iris.experimental.ugrid.Mesh.edge_coords`, - :attr:`~iris.experimental.ugrid.Mesh.face_coords` - | For indicating the 'centres' of the edges/faces. - | **NOTE:** generating a :class:`~iris.experimental.ugrid.MeshCoord` from - a :class:`~iris.experimental.ugrid.Mesh` currently (``Jan 2022``) - requires centre coordinates for the given ``location``; to be rectified - in future. - - * 1 or more :class:`iris.experimental.ugrid.Connectivity`\s: - - * | **Required for 1D (edge) elements**: - :attr:`~iris.experimental.ugrid.Mesh.edge_node_connectivity` - | Define the edges by connecting nodes. - * | **Required for 2D (face) elements**: - :attr:`~iris.experimental.ugrid.Mesh.face_node_connectivity` - | Define the faces by connecting nodes. - * Optional: any other connectivity type. See - :attr:`iris.experimental.ugrid.mesh.Connectivity.UGRID_CF_ROLES` for the - full list of types. - -.. doctest:: ugrid_summaries - - >>> print(edge_cube.mesh) - Mesh : 'my_mesh' - topology_dimension: 2 - node - node_dimension: 'Mesh2d_node' - node coordinates - - - edge - edge_dimension: 'Mesh2d_edge' - edge_node_connectivity: - edge coordinates - - - face - face_dimension: 'Mesh2d_face' - face_node_connectivity: - face coordinates - - - long_name: 'my_mesh' - -* | :class:`iris.experimental.ugrid.MeshCoord` - | Described in detail in `MeshCoords`_. - | Stores the following information: - - * | :attr:`~iris.experimental.ugrid.MeshCoord.mesh` - | The :class:`~iris.experimental.ugrid.Mesh` associated with this - :class:`~iris.experimental.ugrid.MeshCoord`. This determines the - :attr:`~iris.cube.Cube.mesh` attribute of any :class:`~iris.cube.Cube` - this :class:`~iris.experimental.ugrid.MeshCoord` is attached to (see - `The Basics`_) - - * | :attr:`~iris.experimental.ugrid.MeshCoord.location` - | ``node``/``edge``/``face`` - the element detailed by this - :class:`~iris.experimental.ugrid.MeshCoord`. This determines the - :attr:`~iris.cube.Cube.location` attribute of any - :class:`~iris.cube.Cube` this - :class:`~iris.experimental.ugrid.MeshCoord` is attached to (see - `The Basics`_). - -.. _ugrid MeshCoords: - -MeshCoords -~~~~~~~~~~ -Links a :class:`~iris.cube.Cube` to a :class:`~iris.experimental.ugrid.Mesh` by -attaching to the :class:`~iris.cube.Cube`\'s unstructured dimension, in the -same way that all :class:`~iris.coords.Coord`\s attach to -:class:`~iris.cube.Cube` dimensions. This allows a single -:class:`~iris.cube.Cube` to have a combination of unstructured and structured -dimensions (e.g. horizontal mesh plus vertical levels and a time series), -using the same logic for every dimension. - -:class:`~iris.experimental.ugrid.MeshCoord`\s are instantiated using a given -:class:`~iris.experimental.ugrid.Mesh`, ``location`` -("node"/"edge"/"face") and ``axis``. The process interprets the -:class:`~iris.experimental.ugrid.Mesh`\'s -:attr:`~iris.experimental.ugrid.Mesh.node_coords` and if appropriate the -:attr:`~iris.experimental.ugrid.Mesh.edge_node_connectivity`/ -:attr:`~iris.experimental.ugrid.Mesh.face_node_connectivity` and -:attr:`~iris.experimental.ugrid.Mesh.edge_coords`/ -:attr:`~iris.experimental.ugrid.Mesh.face_coords` -to produce a :class:`~iris.coords.Coord` -:attr:`~iris.coords.Coord.points` and :attr:`~iris.coords.Coord.bounds` -representation of all the :class:`~iris.experimental.ugrid.Mesh`\'s -nodes/edges/faces for the given axis. - -The method :meth:`iris.experimental.ugrid.Mesh.to_MeshCoords` is available to -create a :class:`~iris.experimental.ugrid.MeshCoord` for -every axis represented by that :class:`~iris.experimental.ugrid.Mesh`, -given only the ``location`` argument - -.. doctest:: ugrid_summaries - - >>> for coord in edge_cube.coords(mesh_coords=True): - ... print(coord) - MeshCoord : latitude / (degrees_north) - mesh: - location: 'edge' - points: [3. , 1.5, 1.5, 1.5, 0. , 0. ] - bounds: [ - [3., 3.], - [3., 0.], - [3., 0.], - [3., 0.], - [0., 0.], - [0., 0.]] - shape: (6,) bounds(6, 2) - dtype: float64 - standard_name: 'latitude' - axis: 'y' - MeshCoord : longitude / (degrees_east) - mesh: - location: 'edge' - points: [2.5, 0. , 5. , 6.5, 2.5, 6.5] - bounds: [ - [0., 5.], - [0., 0.], - [5., 5.], - [5., 8.], - [0., 5.], - [5., 8.]] - shape: (6,) bounds(6, 2) - dtype: float64 - standard_name: 'longitude' - axis: 'x' - - -__ CF-UGRID_ \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/data_structured_grid.svg b/docs/src/further_topics/ugrid/images/data_structured_grid.svg deleted file mode 100644 index 2f3a1ce342..0000000000 --- a/docs/src/further_topics/ugrid/images/data_structured_grid.svg +++ /dev/null @@ -1 +0,0 @@ -23, 28-19,-21101525-5-15-20-30xyCoordinate ArraysxyCoordinate Arrays23, 28-19, -21xyBounds Arraysderive point locationsassign data using dimensional indices,position in array == relative spatial positionderive area locations & shapesPoint DataArea DataData Array(bounded coordsalways have points too)my_variable* x+yare not lons+lats, just a demonstration! \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/data_ugrid_mesh.svg b/docs/src/further_topics/ugrid/images/data_ugrid_mesh.svg deleted file mode 100644 index ab7302346b..0000000000 --- a/docs/src/further_topics/ugrid/images/data_ugrid_mesh.svg +++ /dev/null @@ -1 +0,0 @@ -5, 7, 8, 14`xy1212`node_coordinates`every node has its own x + y coordinatesderive node locations1515xy`node_coordinates`[5][7][8][14]construct faces by connecting nodesderive ‘corner’ node locationsassign data using 1D indexing,position in array unrelated to spatial positionmatch indices with facesmatch indices with nodesNode DataFace Data12Data Arraymy_variable12 ×4`face_node_connectivity`face_nodes \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/geovistalogo.svg b/docs/src/further_topics/ugrid/images/geovistalogo.svg deleted file mode 100644 index 4c68f0ee3f..0000000000 --- a/docs/src/further_topics/ugrid/images/geovistalogo.svg +++ /dev/null @@ -1,573 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - Cartographic rendering and mesh analytics powered by PyVista. - GeoVista - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - GeoVista - - \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/iris-esmf-regrid.svg b/docs/src/further_topics/ugrid/images/iris-esmf-regrid.svg deleted file mode 100644 index e70a9386a7..0000000000 --- a/docs/src/further_topics/ugrid/images/iris-esmf-regrid.svg +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - Iris - - diff --git a/docs/src/further_topics/ugrid/images/plotting_basic.png b/docs/src/further_topics/ugrid/images/plotting_basic.png deleted file mode 100644 index ba2b0b3329d8930508ece378d1317940f964f58e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42338 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 z 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 ×2 \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/ugrid_element_centres.svg b/docs/src/further_topics/ugrid/images/ugrid_element_centres.svg deleted file mode 100644 index 13b885d600..0000000000 --- a/docs/src/further_topics/ugrid/images/ugrid_element_centres.svg +++ /dev/null @@ -1 +0,0 @@ -`face_node_connectivity`xy`node_coordinates`xy`face_coordinates`151512 ×41212`face_coordinates``node_coordinates` \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/ugrid_node_independence.svg b/docs/src/further_topics/ugrid/images/ugrid_node_independence.svg deleted file mode 100644 index ba72c42ffa..0000000000 --- a/docs/src/further_topics/ugrid/images/ugrid_node_independence.svg +++ /dev/null @@ -1 +0,0 @@ -` \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/ugrid_variable_faces.svg b/docs/src/further_topics/ugrid/images/ugrid_variable_faces.svg deleted file mode 100644 index 378978abc3..0000000000 --- a/docs/src/further_topics/ugrid/images/ugrid_variable_faces.svg +++ /dev/null @@ -1 +0,0 @@ -`face_node_connectivity`12 ×6 \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/index.rst b/docs/src/further_topics/ugrid/index.rst deleted file mode 100644 index 81ba24428a..0000000000 --- a/docs/src/further_topics/ugrid/index.rst +++ /dev/null @@ -1,54 +0,0 @@ -.. include:: ../../common_links.inc - -.. _ugrid: - -Mesh Support -************ - -Iris includes specialised handling of mesh-located data (as opposed to -grid-located data). Iris and its :ref:`partner packages ` are -designed to make working with mesh-located data as simple as possible, with new -capabilities being added all the time. More detail is in this section and in -the :mod:`iris.experimental.ugrid` API documentation. - -This mesh support is based on the `CF-UGRID Conventions`__; UGRID-conformant -meshes + data can be loaded from a file into Iris' data model, and meshes + -data represented in Iris' data model can be saved as a UGRID-conformant file. - ----- - -Meshes are different - Mesh-located data is fundamentally different to grid-located data. - Many of Iris' existing operations need adapting before they can work with - mesh-located data, and in some cases entirely new concepts are needed. - **Read the detail in these pages before jumping into your own code.** -Iris' mesh support is experimental - This is a rapidly evolving part of the codebase at time of writing - (``Jan 2022``), as we continually expand the operations that work with mesh - data. **Be prepared for breaking changes even in minor releases.** -:ref:`Get involved! ` - We know meshes are an exciting new area for much of Earth science, so we hope - there are a lot of you with new files/ideas/wishlists, and we'd love to hear - more 🙂. - ----- - -Read on to find out more... - -* :doc:`data_model` - learn why the mesh experience is so different. -* :doc:`partner_packages` - meet some optional dependencies that provide powerful mesh operations. -* :doc:`operations` - experience how your workflows will look when written for mesh data. - -.. - Need an actual TOC to get Sphinx working properly, but have hidden it in - favour of the custom bullets above. - -.. toctree:: - :hidden: - :maxdepth: 1 - - data_model - partner_packages - operations - -__ CF-UGRID_ diff --git a/docs/src/further_topics/ugrid/operations.rst b/docs/src/further_topics/ugrid/operations.rst deleted file mode 100644 index f96e3e406c..0000000000 --- a/docs/src/further_topics/ugrid/operations.rst +++ /dev/null @@ -1,995 +0,0 @@ -.. _ugrid operations: - -Working with Mesh Data -********************** - -.. note:: Several of the operations below rely on the optional dependencies - mentioned in :doc:`partner_packages`. - -Operations Summary ------------------- -.. list-table:: - :align: left - :widths: 35, 75 - - * - `Making a Mesh`_ - - |tagline: making a mesh| - * - `Making a Cube`_ - - |tagline: making a cube| - * - `Save`_ - - |tagline: save| - * - `Load`_ - - |tagline: load| - * - `Plotting`_ - - |tagline: plotting| - * - `Region Extraction`_ - - |tagline: region extraction| - * - `Regridding`_ - - |tagline: regridding| - * - `Equality`_ - - |tagline: equality| - * - `Combining Cubes`_ - - |tagline: combining cubes| - * - `Arithmetic`_ - - |tagline: arithmetic| - -.. - Below: use demo code over prose wherever workable. Headings aren't an - exhaustive list (can you think of any other popular operations?). - -Making a Mesh -------------- -.. |tagline: making a mesh| replace:: |new| - -.. rubric:: |tagline: making a mesh| - -**Already have a file?** Consider skipping to `Load`_. - -Creating Iris objects from scratch is a highly useful skill for testing code -and improving understanding of how Iris works. This knowledge will likely prove -particularly useful when converting data into the Iris mesh data model from -structured formats and non-UGRID mesh formats. - -The objects created in this example will be used where possible in the -subsequent example operations on this page. - -.. dropdown:: :opticon:`code` - - .. doctest:: ugrid_operations - - >>> import numpy as np - - >>> from iris.coords import AuxCoord - >>> from iris.experimental.ugrid import Connectivity, Mesh - - # Going to create the following mesh - # (node indices are shown to aid understanding): - # - # 0----1 - # | |\ - # | + |+\ - # 2----3--4 - - >>> node_x = AuxCoord( - ... points=[0.0, 5.0, 0.0, 5.0, 8.0], - ... standard_name="longitude", - ... units="degrees_east", - ... long_name="node_x_coordinates", - ... ) - >>> node_y = AuxCoord(points=[3.0, 3.0, 0.0, 0.0, 0.0], standard_name="latitude") - - >>> face_x = AuxCoord([2.0, 6.0], "longitude") - >>> face_y = AuxCoord([1.0, 1.0], "latitude") - - >>> edge_node_c = Connectivity( - ... indices=[[0, 1], [0, 2], [1, 3], [1, 4], [2, 3], [3, 4]], - ... cf_role="edge_node_connectivity", - ... attributes={"demo": "Supports every standard CF property"}, - ... ) - - # Create some dead-centre edge coordinates. - >>> edge_x, edge_y = [ - ... AuxCoord( - ... node_coord.points[edge_node_c.indices_by_location()].mean(axis=1), - ... node_coord.standard_name, - ... ) - ... for node_coord in (node_x, node_y) - ... ] - - >>> face_indices = np.ma.masked_equal([[0, 1, 3, 2], [1, 4, 3, 999]], 999) - >>> face_node_c = Connectivity( - ... indices=face_indices, cf_role="face_node_connectivity" - ... ) - - >>> my_mesh = Mesh( - ... long_name="my_mesh", - ... topology_dimension=2, # Supports 2D (face) elements. - ... node_coords_and_axes=[(node_x, "x"), (node_y, "y")], - ... connectivities=[edge_node_c, face_node_c], - ... edge_coords_and_axes=[(edge_x, "x"), (edge_y, "y")], - ... face_coords_and_axes=[(face_x, "x"), (face_y, "y")], - ... ) - - >>> print(my_mesh) - Mesh : 'my_mesh' - topology_dimension: 2 - node - node_dimension: 'Mesh2d_node' - node coordinates - - - edge - edge_dimension: 'Mesh2d_edge' - edge_node_connectivity: - edge coordinates - - - face - face_dimension: 'Mesh2d_face' - face_node_connectivity: - face coordinates - - - long_name: 'my_mesh' - - -.. _making a cube: - -Making a Cube (with a Mesh) ---------------------------- -.. |tagline: making a cube| replace:: |unchanged| - -.. rubric:: |tagline: making a cube| - -Creating a :class:`~iris.cube.Cube` is unchanged; the -:class:`~iris.experimental.ugrid.Mesh` is linked via a -:class:`~iris.experimental.ugrid.MeshCoord` (see :ref:`ugrid MeshCoords`): - -.. dropdown:: :opticon:`code` - - .. doctest:: ugrid_operations - - >>> import numpy as np - - >>> from iris.coords import DimCoord - >>> from iris.cube import Cube, CubeList - - >>> vertical_levels = DimCoord([0, 1, 2], "height") - - >>> my_cubelist = CubeList() - >>> for conn in (edge_node_c, face_node_c): - ... location = conn.location - ... mesh_coord_x, mesh_coord_y = my_mesh.to_MeshCoords(location) - ... data_shape = (len(conn.indices_by_location()), len(vertical_levels.points)) - ... data_array = np.arange(np.prod(data_shape)).reshape(data_shape) - ... - ... my_cubelist.append( - ... Cube( - ... data=data_array, - ... long_name=f"{location}_data", - ... units="K", - ... dim_coords_and_dims=[(vertical_levels, 1)], - ... aux_coords_and_dims=[(mesh_coord_x, 0), (mesh_coord_y, 0)], - ... ) - ... ) - - >>> print(my_cubelist) - 0: edge_data / (K) (-- : 6; height: 3) - 1: face_data / (K) (-- : 2; height: 3) - - >>> for cube in my_cubelist: - ... print(f"{cube.name()}: {cube.mesh.name()}, {cube.location}") - edge_data: my_mesh, edge - face_data: my_mesh, face - - >>> print(my_cubelist.extract_cube("edge_data")) - edge_data / (K) (-- : 6; height: 3) - Dimension coordinates: - height - x - Mesh coordinates: - latitude x - - longitude x - - - -Save ----- -.. |tagline: save| replace:: |unchanged| - -.. rubric:: |tagline: save| - -.. note:: UGRID saving support is limited to the NetCDF file format. - -The Iris saving process automatically detects if the :class:`~iris.cube.Cube` -has an associated :class:`~iris.experimental.ugrid.Mesh` and automatically -saves the file in a UGRID-conformant format: - -.. dropdown:: :opticon:`code` - - .. doctest:: ugrid_operations - - >>> from subprocess import run - - >>> from iris import save - - >>> cubelist_path = "my_cubelist.nc" - >>> save(my_cubelist, cubelist_path) - - >>> ncdump_result = run(["ncdump", "-h", cubelist_path], capture_output=True) - >>> print(ncdump_result.stdout.decode().replace("\t", " ")) - netcdf my_cubelist { - dimensions: - Mesh2d_node = 5 ; - Mesh2d_edge = 6 ; - Mesh2d_face = 2 ; - height = 3 ; - my_mesh_face_N_nodes = 4 ; - my_mesh_edge_N_nodes = 2 ; - variables: - int my_mesh ; - my_mesh:cf_role = "mesh_topology" ; - my_mesh:topology_dimension = 2 ; - my_mesh:long_name = "my_mesh" ; - my_mesh:node_coordinates = "longitude latitude" ; - my_mesh:edge_coordinates = "longitude_0 latitude_0" ; - my_mesh:face_coordinates = "longitude_1 latitude_1" ; - my_mesh:face_node_connectivity = "mesh2d_face" ; - my_mesh:edge_node_connectivity = "mesh2d_edge" ; - double longitude(Mesh2d_node) ; - longitude:units = "degrees_east" ; - longitude:standard_name = "longitude" ; - longitude:long_name = "node_x_coordinates" ; - double latitude(Mesh2d_node) ; - latitude:standard_name = "latitude" ; - double longitude_0(Mesh2d_edge) ; - longitude_0:standard_name = "longitude" ; - double latitude_0(Mesh2d_edge) ; - latitude_0:standard_name = "latitude" ; - double longitude_1(Mesh2d_face) ; - longitude_1:standard_name = "longitude" ; - double latitude_1(Mesh2d_face) ; - latitude_1:standard_name = "latitude" ; - int64 mesh2d_face(Mesh2d_face, my_mesh_face_N_nodes) ; - mesh2d_face:_FillValue = -1LL ; - mesh2d_face:cf_role = "face_node_connectivity" ; - mesh2d_face:start_index = 0LL ; - int64 mesh2d_edge(Mesh2d_edge, my_mesh_edge_N_nodes) ; - mesh2d_edge:demo = "Supports every standard CF property" ; - mesh2d_edge:cf_role = "edge_node_connectivity" ; - mesh2d_edge:start_index = 0LL ; - int64 edge_data(Mesh2d_edge, height) ; - edge_data:long_name = "edge_data" ; - edge_data:units = "K" ; - edge_data:mesh = "my_mesh" ; - edge_data:location = "edge" ; - int64 height(height) ; - height:standard_name = "height" ; - int64 face_data(Mesh2d_face, height) ; - face_data:long_name = "face_data" ; - face_data:units = "K" ; - face_data:mesh = "my_mesh" ; - face_data:location = "face" ; - - // global attributes: - :Conventions = "CF-1.7" ; - } - - -The :func:`iris.experimental.ugrid.save_mesh` function allows -:class:`~iris.experimental.ugrid.Mesh`\es to be saved to file without -associated :class:`~iris.cube.Cube`\s: - -.. dropdown:: :opticon:`code` - - .. doctest:: ugrid_operations - - >>> from subprocess import run - - >>> from iris.experimental.ugrid import save_mesh - - >>> mesh_path = "my_mesh.nc" - >>> save_mesh(my_mesh, mesh_path) - - >>> ncdump_result = run(["ncdump", "-h", mesh_path], capture_output=True) - >>> print(ncdump_result.stdout.decode().replace("\t", " ")) - netcdf my_mesh { - dimensions: - Mesh2d_node = 5 ; - Mesh2d_edge = 6 ; - Mesh2d_face = 2 ; - my_mesh_face_N_nodes = 4 ; - my_mesh_edge_N_nodes = 2 ; - variables: - int my_mesh ; - my_mesh:cf_role = "mesh_topology" ; - my_mesh:topology_dimension = 2 ; - my_mesh:long_name = "my_mesh" ; - my_mesh:node_coordinates = "longitude latitude" ; - my_mesh:edge_coordinates = "longitude_0 latitude_0" ; - my_mesh:face_coordinates = "longitude_1 latitude_1" ; - my_mesh:face_node_connectivity = "mesh2d_face" ; - my_mesh:edge_node_connectivity = "mesh2d_edge" ; - double longitude(Mesh2d_node) ; - longitude:units = "degrees_east" ; - longitude:standard_name = "longitude" ; - longitude:long_name = "node_x_coordinates" ; - double latitude(Mesh2d_node) ; - latitude:standard_name = "latitude" ; - double longitude_0(Mesh2d_edge) ; - longitude_0:standard_name = "longitude" ; - double latitude_0(Mesh2d_edge) ; - latitude_0:standard_name = "latitude" ; - double longitude_1(Mesh2d_face) ; - longitude_1:standard_name = "longitude" ; - double latitude_1(Mesh2d_face) ; - latitude_1:standard_name = "latitude" ; - int64 mesh2d_face(Mesh2d_face, my_mesh_face_N_nodes) ; - mesh2d_face:_FillValue = -1LL ; - mesh2d_face:cf_role = "face_node_connectivity" ; - mesh2d_face:start_index = 0LL ; - int64 mesh2d_edge(Mesh2d_edge, my_mesh_edge_N_nodes) ; - mesh2d_edge:demo = "Supports every standard CF property" ; - mesh2d_edge:cf_role = "edge_node_connectivity" ; - mesh2d_edge:start_index = 0LL ; - - // global attributes: - :Conventions = "CF-1.7" ; - } - - -Load ----- -.. |tagline: load| replace:: |different| - UGRID parsing is opt-in - -.. rubric:: |tagline: load| - -.. note:: UGRID loading support is limited to the NetCDF file format. - -While Iris' UGRID support remains :mod:`~iris.experimental`, parsing UGRID when -loading a file remains **optional**. To load UGRID data from a file into the -Iris mesh data model, use the -:const:`iris.experimental.ugrid.PARSE_UGRID_ON_LOAD` context manager: - -.. dropdown:: :opticon:`code` - - .. doctest:: ugrid_operations - - >>> from iris import load - >>> from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD - - >>> with PARSE_UGRID_ON_LOAD.context(): - ... loaded_cubelist = load(cubelist_path) - - # Sort CubeList to ensure consistent result. - >>> loaded_cubelist.sort(key=lambda cube: cube.name()) - >>> print(loaded_cubelist) - 0: edge_data / (K) (-- : 6; height: 3) - 1: face_data / (K) (-- : 2; height: 3) - -All the existing loading functionality still operates on UGRID-compliant -data - :class:`~iris.Constraint`\s, callbacks, :func:`~iris.load_cube` -etcetera: - -.. dropdown:: :opticon:`code` - - .. doctest:: ugrid_operations - - >>> from iris import Constraint, load_cube - - >>> with PARSE_UGRID_ON_LOAD.context(): - ... ground_cubelist = load(cubelist_path, Constraint(height=0)) - ... face_cube = load_cube(cubelist_path, "face_data") - - # Sort CubeList to ensure consistent result. - >>> ground_cubelist.sort(key=lambda cube: cube.name()) - >>> print(ground_cubelist) - 0: edge_data / (K) (-- : 6) - 1: face_data / (K) (-- : 2) - - >>> print(face_cube) - face_data / (K) (-- : 2; height: 3) - Dimension coordinates: - height - x - Mesh coordinates: - latitude x - - longitude x - - Attributes: - Conventions 'CF-1.7' - -.. note:: - - We recommend caution if constraining on coordinates associated with a - :class:`~iris.experimental.ugrid.Mesh`. An individual coordinate value - might not be shared by any other data points, and using a coordinate range - will demand notably higher performance given the size of the dimension - versus structured grids - (:ref:`see the data model detail `). - -The :func:`iris.experimental.ugrid.load_mesh` and -:func:`~iris.experimental.ugrid.load_meshes` functions allow only -:class:`~iris.experimental.ugrid.Mesh`\es to be loaded from a file without -creating any associated :class:`~iris.cube.Cube`\s: - -.. dropdown:: :opticon:`code` - - .. doctest:: ugrid_operations - - >>> from iris.experimental.ugrid import load_mesh - - >>> with PARSE_UGRID_ON_LOAD.context(): - ... loaded_mesh = load_mesh(cubelist_path) - - >>> print(loaded_mesh) - Mesh : 'my_mesh' - topology_dimension: 2 - node - node_dimension: 'Mesh2d_node' - node coordinates - shape(5,)> - shape(5,)> - edge - edge_dimension: 'Mesh2d_edge' - edge_node_connectivity: shape(6, 2)> - edge coordinates - shape(6,)> - shape(6,)> - face - face_dimension: 'Mesh2d_face' - face_node_connectivity: shape(2, 4)> - face coordinates - shape(2,)> - shape(2,)> - long_name: 'my_mesh' - var_name: 'my_mesh' - -Plotting --------- -.. |tagline: plotting| replace:: |different| - plot with GeoVista - -.. rubric:: |tagline: plotting| - -The Cartopy-Matplotlib combination is not optimised for displaying the high -number of irregular shapes associated with meshes. Thankfully mesh -visualisation is already popular in many other fields (e.g. CGI, gaming, -SEM microscopy), so there is a wealth of tooling available, which -:ref:`ugrid geovista` harnesses for cartographic plotting. - -GeoVista's default behaviour is to convert lat-lon information into full XYZ -coordinates so the data is visualised on the surface of a 3D globe. The plots -are interactive by default, so it's easy to explore the data in detail. - -2D projections have also been demonstrated in proofs of concept, and will -be added to API in the near future. - -This first example uses GeoVista to plot the ``face_cube`` that we created -earlier: - -.. dropdown:: :opticon:`code` - - .. code-block:: python - - >>> from geovista import GeoPlotter, Transform - >>> from geovista.common import to_xyz - - - # We'll re-use this to plot some real global data later. - >>> def cube_faces_to_polydata(cube): - ... lons, lats = cube.mesh.node_coords - ... face_node = cube.mesh.face_node_connectivity - ... indices = face_node.indices_by_location() - ... - ... mesh = Transform.from_unstructured( - ... lons.points, - ... lats.points, - ... indices, - ... data=cube.data, - ... name=f"{cube.name()} / {cube.units}", - ... start_index=face_node.start_index, - ... ) - ... return mesh - - >>> print(face_cube) - face_data / (K) (-- : 2; height: 3) - Dimension coordinates: - height - x - Mesh coordinates: - latitude x - - longitude x - - Attributes: - Conventions 'CF-1.7' - - # Convert our mesh+data to a PolyData object. - # Just plotting a single height level. - >>> face_polydata = cube_faces_to_polydata(face_cube[:, 0]) - >>> print(face_polydata) - PolyData (0x7ff4861ff4c0) - N Cells: 2 - N Points: 5 - X Bounds: 9.903e-01, 1.000e+00 - Y Bounds: 0.000e+00, 1.392e-01 - Z Bounds: 6.123e-17, 5.234e-02 - N Arrays: 2 - - # Create the GeoVista plotter and add our mesh+data to it. - >>> my_plotter = GeoPlotter() - >>> my_plotter.add_coastlines(color="black") - >>> my_plotter.add_base_layer(color="grey") - >>> my_plotter.add_mesh(face_polydata) - - # Centre the camera on the data. - >>> camera_region = to_xyz( - ... face_cube.coord("longitude").points, - ... face_cube.coord("latitude").points, - ... radius=3, - ... ) - >>> camera_pos = camera_region.mean(axis=0) - >>> my_plotter.camera.position = camera_pos - - >>> my_plotter.show() - - .. image:: images/plotting_basic.png - :alt: A GeoVista plot of the basic example Mesh. - - This artificial data makes West Africa rather chilly! - -Here's another example using a global cubed-sphere data set: - -.. dropdown:: :opticon:`code` - - .. code-block:: python - - >>> from iris import load_cube - >>> from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD - - # Demonstrating with a global data set. - # You could also download this file from github.com/SciTools/iris-test-data. - >>> from iris.tests import get_data_path - >>> file_path = get_data_path( - ... [ - ... "NetCDF", - ... "unstructured_grid", - ... "lfric_surface_mean.nc", - ... ] - ... ) - >>> with PARSE_UGRID_ON_LOAD.context(): - ... global_cube = load_cube(file_path, "tstar_sea") - >>> print(global_cube) - sea_surface_temperature / (K) (-- : 1; -- : 13824) - Mesh coordinates: - latitude - x - longitude - x - Auxiliary coordinates: - time x - - Cell methods: - mean time (300 s) - mean time_counter - Attributes: - Conventions UGRID - description Created by xios - interval_operation 300 s - interval_write 1 d - name lfric_surface - online_operation average - timeStamp 2020-Feb-07 16:23:14 GMT - title Created by xios - uuid 489bcef5-3d1c-4529-be42-4ab5f8c8497b - - >>> global_polydata = cube_faces_to_polydata(global_cube) - >>> print(global_polydata) - PolyData (0x7f761b536160) - N Cells: 13824 - N Points: 13826 - X Bounds: -1.000e+00, 1.000e+00 - Y Bounds: -1.000e+00, 1.000e+00 - Z Bounds: -1.000e+00, 1.000e+00 - N Arrays: 2 - - >>> my_plotter = GeoPlotter() - >>> my_plotter.add_coastlines() - >>> my_plotter.add_mesh(global_polydata, show_edges=True) - - >>> my_plotter.show() - - .. image:: images/plotting_global.png - :alt: A GeoVista plot of a global sea surface temperature Mesh. - -Region Extraction ------------------ -.. |tagline: region extraction| replace:: |different| - use GeoVista for mesh analysis - -.. rubric:: |tagline: region extraction| - -As described in :doc:`data_model`, indexing for a range along a -:class:`~iris.cube.Cube`\'s :meth:`~iris.cube.Cube.mesh_dim` will not provide -a contiguous region, since **position on the unstructured dimension is -unrelated to spatial position**. This means that subsetted -:class:`~iris.experimental.ugrid.MeshCoord`\s cannot be reliably interpreted -as intended, and subsetting a :class:`~iris.experimental.ugrid.MeshCoord` is -therefore set to return an :class:`~iris.coords.AuxCoord` instead - breaking -the link between :class:`~iris.cube.Cube` and -:class:`~iris.experimental.ugrid.Mesh`: - -.. dropdown:: :opticon:`code` - - .. doctest:: ugrid_operations - - >>> edge_cube = my_cubelist.extract_cube("edge_data") - >>> print(edge_cube) - edge_data / (K) (-- : 6; height: 3) - Dimension coordinates: - height - x - Mesh coordinates: - latitude x - - longitude x - - - # Sub-setted MeshCoords have become AuxCoords. - >>> print(edge_cube[:-1]) - edge_data / (K) (-- : 5; height: 3) - Dimension coordinates: - height - x - Auxiliary coordinates: - latitude x - - longitude x - - -Extracting a region therefore requires extra steps - to determine the spatial -position of the data points before they can be analysed as inside/outside the -selected region. The recommended way to do this is using tools provided by -:ref:`ugrid geovista`, which is optimised for performant mesh analysis. - -This approach centres around using :meth:`geovista.geodesic.BBox.enclosed` to -get the subset of the original mesh that is inside the -:class:`~geovista.geodesic.BBox`. This subset :class:`pyvista.PolyData` object -includes the original indices of each datapoint - the ``vtkOriginalCellIds`` -array, which can be used to index the original :class:`~iris.cube.Cube`. Since -we **know** that this subset :class:`~iris.cube.Cube` represents a regional -mesh, we then reconstruct a :class:`~iris.experimental.ugrid.Mesh` from the -:class:`~iris.cube.Cube`\'s :attr:`~iris.cube.Cube.aux_coords` using -:meth:`iris.experimental.ugrid.Mesh.from_coords`: - -.. - Not using doctest here as want to keep GeoVista as optional dependency. - -.. dropdown:: :opticon:`code` - - .. code-block:: python - - >>> from geovista import Transform - >>> from geovista.geodesic import BBox - >>> from iris import load_cube - >>> from iris.experimental.ugrid import Mesh, PARSE_UGRID_ON_LOAD - - # Need a larger dataset to demonstrate this operation. - # You could also download this file from github.com/SciTools/iris-test-data. - >>> from iris.tests import get_data_path - >>> file_path = get_data_path( - ... [ - ... "NetCDF", - ... "unstructured_grid", - ... "lfric_ngvat_2D_72t_face_half_levels_main_conv_rain.nc", - ... ] - ... ) - - >>> with PARSE_UGRID_ON_LOAD.context(): - ... global_cube = load_cube(file_path, "conv_rain") - >>> print(global_cube) - surface_convective_rainfall_rate / (kg m-2 s-1) (-- : 72; -- : 864) - Mesh coordinates: - latitude - x - longitude - x - Auxiliary coordinates: - time x - - Cell methods: - point time - Attributes: - Conventions UGRID - description Created by xios - interval_operation 300 s - interval_write 300 s - name lfric_ngvat_2D_72t_face_half_levels_main_conv_rain - online_operation instant - timeStamp 2020-Oct-18 21:18:35 GMT - title Created by xios - uuid b3dc0fb4-9828-4663-a5ac-2a5763280159 - - # Convert the Mesh to a GeoVista PolyData object. - >>> lons, lats = global_cube.mesh.node_coords - >>> face_node = global_cube.mesh.face_node_connectivity - >>> indices = face_node.indices_by_location() - >>> global_polydata = Transform.from_unstructured( - ... lons.points, lats.points, indices, start_index=face_node.start_index - ... ) - - # Define a region of 4 corners connected by great circles. - # Specialised sub-classes of BBox are also available e.g. panel/wedge. - >>> region = BBox(lons=[0, 70, 70, 0], lats=[-25, -25, 45, 45]) - # 'Apply' the region to the PolyData object. - >>> region_polydata = region.enclosed(global_polydata, preference="center") - # Get the remaining face indices, to use for indexing the Cube. - >>> indices = region_polydata["vtkOriginalCellIds"] - - >>> print(type(indices)) - - # 101 is smaller than the original 864. - >>> print(len(indices)) - 101 - >>> print(indices[:10]) - [ 6 7 8 9 10 11 18 19 20 21] - - # Use the face indices to subset the global cube. - >>> region_cube = global_cube[:, indices] - - # In this case we **know** the indices correspond to a contiguous - # region, so we will convert the sub-setted Cube back into a - # Cube-with-Mesh. - >>> new_mesh = Mesh.from_coords(*region_cube.coords(dimensions=1)) - >>> new_mesh_coords = new_mesh.to_MeshCoords(global_cube.location) - >>> for coord in new_mesh_coords: - ... region_cube.remove_coord(coord.name()) - ... region_cube.add_aux_coord(coord, 1) - - # A Mesh-Cube with a subset (101) of the original 864 faces. - >>> print(region_cube) - surface_convective_rainfall_rate / (kg m-2 s-1) (-- : 72; -- : 101) - Mesh coordinates: - latitude - x - longitude - x - Auxiliary coordinates: - time x - - Cell methods: - point time - Attributes: - Conventions UGRID - description Created by xios - interval_operation 300 s - interval_write 300 s - name lfric_ngvat_2D_72t_face_half_levels_main_conv_rain - online_operation instant - timeStamp 2020-Oct-18 21:18:35 GMT - title Created by xios - uuid b3dc0fb4-9828-4663-a5ac-2a5763280159 - -Regridding ----------- -.. |tagline: regridding| replace:: |different| - use iris-esmf-regrid for mesh regridders - -.. rubric:: |tagline: regridding| - -Regridding to or from a mesh requires different logic than Iris' existing -regridders, which are designed for structured grids. For this we recommend -ESMF's powerful regridding tools, which integrate with Iris' mesh data model -via the :ref:`ugrid iris-esmf-regrid` package. - -.. todo: inter-sphinx links when available. - -Regridding is achieved via the -:class:`esmf_regrid.experimental.unstructured_scheme.MeshToGridESMFRegridder` -and -:class:`~esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder` -classes. Regridding from a source :class:`~iris.cube.Cube` to a target -:class:`~iris.cube.Cube` involves initialising and then calling one of these -classes. Initialising is done by passing in the source and target -:class:`~iris.cube.Cube` as arguments. The regridder is then called by passing -the source :class:`~iris.cube.Cube` as an argument. We can demonstrate this -with the -:class:`~esmf_regrid.experimental.unstructured_scheme.MeshToGridESMFRegridder`: - -.. - Not using doctest here as want to keep iris-esmf-regrid as optional dependency. - -.. dropdown:: :opticon:`code` - - .. code-block:: python - - >>> from esmf_regrid.experimental.unstructured_scheme import MeshToGridESMFRegridder - >>> from iris import load, load_cube - >>> from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD - - # You could also download these files from github.com/SciTools/iris-test-data. - >>> from iris.tests import get_data_path - >>> mesh_file = get_data_path( - ... ["NetCDF", "unstructured_grid", "lfric_surface_mean.nc"] - ... ) - >>> grid_file = get_data_path( - ... ["NetCDF", "regrid", "regrid_template_global_latlon.nc"] - ... ) - - # Load a list of cubes defined on the same Mesh. - >>> with PARSE_UGRID_ON_LOAD.context(): - ... mesh_cubes = load(mesh_file) - - # Extract a specific cube. - >>> mesh_cube1 = mesh_cubes.extract_cube("sea_surface_temperature") - >>> print(mesh_cube1) - sea_surface_temperature / (K) (-- : 1; -- : 13824) - Mesh coordinates: - latitude - x - longitude - x - Auxiliary coordinates: - time x - - Cell methods: - mean time (300 s) - mean time_counter - Attributes: - Conventions UGRID - description Created by xios - interval_operation 300 s - interval_write 1 d - name lfric_surface - online_operation average - timeStamp 2020-Feb-07 16:23:14 GMT - title Created by xios - uuid 489bcef5-3d1c-4529-be42-4ab5f8c8497b - - # Load the target grid. - >>> sample_grid = load_cube(grid_file) - >>> print(sample_grid) - sample_grid / (unknown) (latitude: 180; longitude: 360) - Dimension coordinates: - latitude x - - longitude - x - Attributes: - Conventions 'CF-1.7' - - # Initialise the regridder. - >>> rg = MeshToGridESMFRegridder(mesh_cube1, sample_grid) - - # Regrid the mesh cube cube. - >>> result1 = rg(mesh_cube1) - >>> print(result1) - sea_surface_temperature / (K) (-- : 1; latitude: 180; longitude: 360) - Dimension coordinates: - latitude - x - - longitude - - x - Auxiliary coordinates: - time x - - - Cell methods: - mean time (300 s) - mean time_counter - Attributes: - Conventions UGRID - description Created by xios - interval_operation 300 s - interval_write 1 d - name lfric_surface - online_operation average - timeStamp 2020-Feb-07 16:23:14 GMT - title Created by xios - uuid 489bcef5-3d1c-4529-be42-4ab5f8c8497b - -.. note:: - - **All** :class:`~iris.cube.Cube` :attr:`~iris.cube.Cube.attributes` are - retained when regridding, so watch out for any attributes that reference - the format (there are several in these examples) - you may want to manually - remove them to avoid later confusion. - -The initialisation process is computationally expensive so we use caching to -improve performance. Once a regridder has been initialised, it can be used on -any :class:`~iris.cube.Cube` which has been defined on the same -:class:`~iris.experimental.ugrid.Mesh` (or on the same **grid** in the case of -:class:`~esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`). -Since calling a regridder is usually a lot faster than initialising, reusing -regridders can save a lot of time. We can demonstrate the reuse of the -previously initialised regridder: - -.. dropdown:: :opticon:`code` - - .. code-block:: python - - # Extract a different cube defined on te same Mesh. - >>> mesh_cube2 = mesh_cubes.extract_cube("precipitation_flux") - >>> print(mesh_cube2) - precipitation_flux / (kg m-2 s-1) (-- : 1; -- : 13824) - Mesh coordinates: - latitude - x - longitude - x - Auxiliary coordinates: - time x - - Cell methods: - mean time (300 s) - mean time_counter - Attributes: - Conventions UGRID - description Created by xios - interval_operation 300 s - interval_write 1 d - name lfric_surface - online_operation average - timeStamp 2020-Feb-07 16:23:14 GMT - title Created by xios - uuid 489bcef5-3d1c-4529-be42-4ab5f8c8497b - - # Regrid the new mesh cube using the same regridder. - >>> result2 = rg(mesh_cube2) - >>> print(result2) - precipitation_flux / (kg m-2 s-1) (-- : 1; latitude: 180; longitude: 360) - Dimension coordinates: - latitude - x - - longitude - - x - Auxiliary coordinates: - time x - - - Cell methods: - mean time (300 s) - mean time_counter - Attributes: - Conventions UGRID - description Created by xios - interval_operation 300 s - interval_write 1 d - name lfric_surface - online_operation average - timeStamp 2020-Feb-07 16:23:14 GMT - title Created by xios - uuid 489bcef5-3d1c-4529-be42-4ab5f8c8497b - -Support also exists for saving and loading previously initialised regridders - -:func:`esmf_regrid.experimental.io.save_regridder` and -:func:`~esmf_regrid.experimental.io.load_regridder` - so that they can be -re-used by future scripts. - -Equality --------- -.. |tagline: equality| replace:: |unchanged| - -.. rubric:: |tagline: equality| - -:class:`~iris.experimental.ugrid.Mesh` comparison is supported, and comparing -two ':class:`~iris.experimental.ugrid.Mesh`-:class:`~iris.cube.Cube`\s' will -include a comparison of the respective -:class:`~iris.experimental.ugrid.Mesh`\es, with no extra action needed by the -user. - -.. note:: - - Keep an eye on memory demand when comparing large - :class:`~iris.experimental.ugrid.Mesh`\es, but note that - :class:`~iris.experimental.ugrid.Mesh`\ equality is enabled for lazy - processing (:doc:`/userguide/real_and_lazy_data`), so if the - :class:`~iris.experimental.ugrid.Mesh`\es being compared are lazy the - process will use less memory than their total size. - -Combining Cubes ---------------- -.. |tagline: combining cubes| replace:: |pending| - -.. rubric:: |tagline: combining cubes| - -Merging or concatenating :class:`~iris.cube.Cube`\s (described in -:doc:`/userguide/merge_and_concat`) with two different -:class:`~iris.experimental.ugrid.Mesh`\es is not possible - a -:class:`~iris.cube.Cube` must be associated with just a single -:class:`~iris.experimental.ugrid.Mesh`, and merge/concatenate are not yet -capable of combining multiple :class:`~iris.experimental.ugrid.Mesh`\es into -one. - -:class:`~iris.cube.Cube`\s that include -:class:`~iris.experimental.ugrid.MeshCoord`\s can still be merged/concatenated -on dimensions other than the :meth:`~iris.cube.Cube.mesh_dim`, since such -:class:`~iris.cube.Cube`\s will by definition share the same -:class:`~iris.experimental.ugrid.Mesh`. - -.. seealso:: - - You may wish to investigate - :func:`iris.experimental.ugrid.recombine_submeshes`, which can be used - for a very specific type of :class:`~iris.experimental.ugrid.Mesh` - combination not detailed here. - -Arithmetic ----------- -.. |tagline: arithmetic| replace:: |pending| - -.. rubric:: |tagline: arithmetic| - -:class:`~iris.cube.Cube` Arithmetic (described in :doc:`/userguide/cube_maths`) -has not yet been adapted to handle :class:`~iris.cube.Cube`\s that include -:class:`~iris.experimental.ugrid.MeshCoord`\s. - - -.. todo: - Enumerate other popular operations that aren't yet possible - (and are they planned soon?) - -.. |new| replace:: ✨ New -.. |unchanged| replace:: ♻️ Unchanged -.. |different| replace:: ⚠️ Different -.. |pending| replace:: 🚧 Support Pending \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/partner_packages.rst b/docs/src/further_topics/ugrid/partner_packages.rst deleted file mode 100644 index 8e36f4ffc2..0000000000 --- a/docs/src/further_topics/ugrid/partner_packages.rst +++ /dev/null @@ -1,100 +0,0 @@ -.. _ugrid partners: - -Iris' Mesh Partner Packages -**************************** -Python is an easy to use language and has formed a very strong collaborative -scientific community, which is why Iris is written in Python. *Performant* -Python relies on calls down to low level languages like C, which is ideal for -structured grid work since -they can be directly represented as NumPy arrays. This is more difficult when -working with unstructured meshes where extra steps are needed to determine data -position (:ref:`see the data model detail `), and we need -to find ways of again passing the operations down to more optimised languages. - -The Iris team are therefore developing 'wrapper' packages, which make it quick -and easy to analyse Iris mesh data via some popular Python packages that use -powerful tools under the hood, working in C and other languages. - -These solutions have been placed in their own 'partner packages' for several -reasons: - -* Can be useful to others who are not using Iris. - - * Everyone working with multi-dimensional geographic datasets shares common - problems that need solving. - * Wider user base = stronger community = better solutions. - -* Only some Iris users will need them - they are **optional** Iris dependencies. - - * They introduce a lot of new API. - * They introduce new large dependencies that take time to install and need - disk space. - -Below you can learn more about the partner packages and how they are useful. -Specifics of what operations would require their installation can be found in: -:doc:`operations`. - -.. important:: **Experimental** - - As with Iris' mesh support, these packages are still in the - experimental stages. They would love your feedback, but as immature - packages their API, documentation, test coverage and CI are still - 'under construction'. - - -.. _`ugrid geovista`: - -`GeoVista`_ -=========== -.. image:: images/geovistalogo.svg - :width: 300 - :class: no-scaled-link - -.. rubric:: "Cartographic rendering and mesh analytics powered by `PyVista`_" - -PyVista is described as "VTK for humans" - VTK is a very powerful toolkit for -working with meshes, and PyVista brings that power into the Python ecosystem. -GeoVista in turn makes it easy to use PyVista specifically for cartographic -work, designed from the start with the Iris -:class:`~iris.experimental.ugrid.Mesh` in mind. - -Applications ------------- -* Interactively plot mesh data: - - * On a 3D globe. - * On your favourite projection. - -* Extract a specific region from a mesh. -* Combine multiple meshes into one. - -.. _`ugrid iris-esmf-regrid`: - -`iris-esmf-regrid`_ -=================== -.. image:: images/iris-esmf-regrid.svg - :width: 300 - :class: no-scaled-link - -.. rubric:: "A collection of structured and unstructured ESMF regridding schemes for Iris" - -ESMF provide a sophisticated, performant regridding utility that supports a -variety of regridding types with both structured grids and unstructured meshes, -and this also has a flexible Python interface - ESMPy. iris-esmf-regrid takes -advantage of having a specific use-case - regridding Iris -:class:`~iris.cube.Cube`\s - to provide ESMPy-Iris wrappers that make the -process as easy as possible, with highly optimised performance. - -Applications ------------- -* Regrid structured to unstructured. -* Regrid unstructured to structured. -* Regrid with dask integration, computing in parallel and maintaining data - laziness. -* | Save a prepared regridder for re-use in subsequent runs. - | Regridders can even be re-used on sources with different masks - a - significant efficiency gain. - -.. _GeoVista: https://github.com/bjlittle/geovista -.. _PyVista: https://docs.pyvista.org/index.html -.. _iris-esmf-regrid: https://github.com/SciTools-incubator/iris-esmf-regrid diff --git a/docs/src/index.rst b/docs/src/index.rst index e6a787a220..8c3455aba9 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -98,15 +98,6 @@ For **Iris 2.4** and earlier documentation please see the generated/gallery/index -.. toctree:: - :maxdepth: 1 - :caption: What's New in Iris - :hidden: - - whatsnew/latest - Archive - - .. toctree:: :maxdepth: 1 :caption: User Guide @@ -140,11 +131,10 @@ For **Iris 2.4** and earlier documentation please see the further_topics/metadata further_topics/lenient_metadata further_topics/lenient_maths - further_topics/ugrid/index .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Developers Guide :name: development_index :hidden: @@ -163,5 +153,6 @@ For **Iris 2.4** and earlier documentation please see the :hidden: generated/api/iris + whatsnew/index techpapers/index copyright diff --git a/docs/src/installing.rst b/docs/src/installing.rst index 37a8942ab3..e358bb42c9 100644 --- a/docs/src/installing.rst +++ b/docs/src/installing.rst @@ -85,12 +85,10 @@ local copy of Iris:: conda env create --force --file=requirements/ci/iris.yml conda activate iris-dev -.. note:: - - The ``--force`` option, used when creating the environment, first removes - any previously existing ``iris-dev`` environment of the same name. This is - particularly useful when rebuilding your environment due to a change in - requirements. +The ``--force`` option is used when creating the environment, this is optional +and will force the any existing ``iris-dev`` conda environment to be deleted +first if present. This is useful when rebuilding your environment due to a +change in requirements. The ``requirements/ci/iris.yml`` file defines the Iris development conda environment *name* and all the relevant *top level* `conda-forge` package diff --git a/docs/src/sphinxext/image_test_output.py b/docs/src/sphinxext/image_test_output.py deleted file mode 100644 index 9e492a5be9..0000000000 --- a/docs/src/sphinxext/image_test_output.py +++ /dev/null @@ -1,78 +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. - -import json -import re -from typing import Dict, List - -from docutils import nodes -from sphinx.application import Sphinx -from sphinx.util.docutils import SphinxDirective - -ImageRepo = Dict[str, List[str]] - -HASH_MATCH = re.compile(r"([^\/]+)\.png$") - - -def hash_from_url(url: str) -> str: - match = HASH_MATCH.search(url) - if not match: - raise ValueError(f"url {url} does not match form `http...hash.png`") - else: - return match.groups()[0] - - -class ImageTestDirective(SphinxDirective): - def run(self): - with open(self.config["image_test_json"], "r") as fh: - imagerepo = json.load(fh) - enum_list = nodes.enumerated_list() - nodelist = [] - nodelist.append(enum_list) - for test in sorted(imagerepo): - link_node = nodes.raw( - "", - f'{test}', - format="html", - ) - li_node = nodes.list_item("") - li_node += link_node - enum_list += li_node - return nodelist - - -def collect_imagehash_pages(app: Sphinx): - """Generate pages for each entry in the imagerepo.json""" - with open(app.config["image_test_json"], "r") as fh: - imagerepo: ImageRepo = json.load(fh) - pages = [] - for test, hashfiles in imagerepo.items(): - hashstrs = [hash_from_url(h) for h in hashfiles] - pages.append( - ( - f"generated/image_test/{test}", - {"test": test, "hashfiles": zip(hashstrs, hashfiles)}, - "imagehash.html", - ) - ) - return pages - - -def setup(app: Sphinx): - app.add_config_value( - "image_test_json", - "../../lib/iris/tests/results/imagerepo.json", - "html", - ) - - app.add_directive("imagetest-list", ImageTestDirective) - app.connect("html-collect-pages", collect_imagehash_pages) - - return { - "version": "0.1", - "parallel_read_safe": True, - "parallel_write_safe": True, - } diff --git a/docs/src/userguide/cube_maths.rst b/docs/src/userguide/cube_maths.rst index e8a1744a44..78490cd749 100644 --- a/docs/src/userguide/cube_maths.rst +++ b/docs/src/userguide/cube_maths.rst @@ -63,9 +63,9 @@ but with the data representing their difference: forecast_reference_time 1859-09-01 06:00:00 height 1.5 m Attributes: - Conventions 'CF-1.5' - Model scenario 'E1' - source 'Data from Met Office Unified Model 6.05' + Conventions CF-1.5 + Model scenario E1 + source Data from Met Office Unified Model 6.05 .. note:: diff --git a/docs/src/userguide/cube_statistics.rst b/docs/src/userguide/cube_statistics.rst index 980f1e132f..ac66ff4e53 100644 --- a/docs/src/userguide/cube_statistics.rst +++ b/docs/src/userguide/cube_statistics.rst @@ -53,8 +53,8 @@ For instance, suppose we have a cube: forecast_reference_time 2009-11-19 04:00:00 Attributes: STASH m01s00i004 - source 'Data from Met Office Unified Model' - um_version '7.3' + source Data from Met Office Unified Model + um_version 7.3 In this case we have a 4 dimensional cube; @@ -84,8 +84,8 @@ we can pass the coordinate name and the aggregation definition to the mean model_level_number Attributes: STASH m01s00i004 - source 'Data from Met Office Unified Model' - um_version '7.3' + source Data from Met Office Unified Model + um_version 7.3 Similarly other analysis operators such as ``MAX``, ``MIN`` and ``STD_DEV`` @@ -143,8 +143,8 @@ These areas can now be passed to the ``collapsed`` method as weights: mean grid_longitude, grid_latitude Attributes: STASH m01s00i004 - source 'Data from Met Office Unified Model' - um_version '7.3' + source Data from Met Office Unified Model + um_version 7.3 Several examples of area averaging exist in the gallery which may be of interest, including an example on taking a :ref:`global area-weighted mean @@ -229,7 +229,7 @@ Printing this cube now shows that two extra coordinates exist on the cube: Cell methods: mean month, year Attributes: - Conventions 'CF-1.5' + Conventions CF-1.5 STASH m01s00i024 diff --git a/docs/src/userguide/interpolation_and_regridding.rst b/docs/src/userguide/interpolation_and_regridding.rst index f590485606..5573c4aa8e 100644 --- a/docs/src/userguide/interpolation_and_regridding.rst +++ b/docs/src/userguide/interpolation_and_regridding.rst @@ -79,7 +79,7 @@ Let's take the air temperature cube we've seen previously: mean over years time Attributes: STASH m01s16i203 - source 'Data from Met Office Unified Model' + source Data from Met Office Unified Model We can interpolate specific values from the coordinates of the cube: @@ -98,7 +98,7 @@ We can interpolate specific values from the coordinates of the cube: mean over years time Attributes: STASH m01s16i203 - source 'Data from Met Office Unified Model' + source Data from Met Office Unified Model As we can see, the resulting cube is scalar and has longitude and latitude coordinates with the values defined in our sample points. diff --git a/docs/src/userguide/iris_cubes.rst b/docs/src/userguide/iris_cubes.rst index d13dee369c..64a9bfd822 100644 --- a/docs/src/userguide/iris_cubes.rst +++ b/docs/src/userguide/iris_cubes.rst @@ -172,8 +172,8 @@ output as this is the quickest way of inspecting the contents of a cube. Here is forecast_reference_time 2009-11-19 04:00:00 Attributes: STASH m01s00i004 - source 'Data from Met Office Unified Model' - um_version '7.3' + source Data from Met Office Unified Model + um_version 7.3 Using this output we can deduce that: diff --git a/docs/src/userguide/loading_iris_cubes.rst b/docs/src/userguide/loading_iris_cubes.rst index fb938975e8..a66af12b9c 100644 --- a/docs/src/userguide/loading_iris_cubes.rst +++ b/docs/src/userguide/loading_iris_cubes.rst @@ -51,14 +51,15 @@ The ``air_potential_temperature`` cubes were 4 dimensional with: .. note:: - The result of :func:`iris.load` is **always** a :class:`iris.cube.CubeList` - (even if it only contains one :class:`iris.cube.Cube` - see - :ref:`strict-loading`). Anything that can be done with a Python - :class:`list` can be done with an :class:`iris.cube.CubeList`. - - The order of this list should not be relied upon. Ways of loading a - specific cube or cubes are covered in :ref:`constrained-loading` and - :ref:`strict-loading`. + The result of :func:`iris.load` is **always** a + :class:`list of cubes `. + Anything that can be done with a Python :class:`list` can be done + with the resultant list of cubes. It is worth noting, however, that + there is no inherent order to this + :class:`list of cubes `. + Because of this, indexing may be inconsistent. A more consistent way to + extract a cube is by using the :class:`iris.Constraint` class as + described in :ref:`constrained-loading`. .. hint:: @@ -100,8 +101,8 @@ list indexing can be used: forecast_reference_time 2009-11-19 04:00:00 Attributes: STASH m01s00i004 - source 'Data from Met Office Unified Model' - um_version '7.3' + source Data from Met Office Unified Model + um_version 7.3 Notice that the result of printing a **cube** is a little more verbose than it was when printing a **list of cubes**. In addition to the very short summary @@ -304,21 +305,13 @@ for ease of calendar-based testing. >>> cube_all = iris.load_cube(filename, 'air_potential_temperature') >>> print('All times :\n' + str(cube_all.coord('time'))) All times : - DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) - points: [2009-11-19 10:00:00, 2009-11-19 11:00:00, 2009-11-19 12:00:00] - shape: (3,) - dtype: float64 - standard_name: 'time' + DimCoord([2009-11-19 10:00:00, 2009-11-19 11:00:00, 2009-11-19 12:00:00], standard_name='time', calendar='gregorian') >>> # Define a function which accepts a datetime as its argument (this is simplified in later examples). >>> hour_11 = iris.Constraint(time=lambda cell: cell.point.hour == 11) >>> cube_11 = cube_all.extract(hour_11) >>> print('Selected times :\n' + str(cube_11.coord('time'))) Selected times : - DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) - points: [2009-11-19 11:00:00] - shape: (1,) - dtype: float64 - standard_name: 'time' + DimCoord([2009-11-19 11:00:00], standard_name='time', calendar='gregorian') Secondly, the :class:`iris.time` module provides flexible time comparison facilities. An :class:`iris.time.PartialDateTime` object can be compared to @@ -343,11 +336,7 @@ The previous constraint example can now be written as: >>> print(iris.load_cube( ... iris.sample_data_path('uk_hires.pp'), ... 'air_potential_temperature' & the_11th_hour).coord('time')) - DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) - points: [2009-11-19 11:00:00] - shape: (1,) - dtype: float64 - standard_name: 'time' + DimCoord([2009-11-19 11:00:00], standard_name='time', calendar='gregorian') It is common that a cube will need to be constrained between two given dates. In the following example we construct a time sequence representing the first @@ -367,13 +356,10 @@ day of every week for many years: :options: +NORMALIZE_WHITESPACE, +ELLIPSIS >>> print(long_ts.coord('time')) - DimCoord : time / (days since 2007-04-09, gregorian calendar) - points: [ - 2007-04-09 00:00:00, 2007-04-16 00:00:00, ..., - 2010-02-08 00:00:00, 2010-02-15 00:00:00] - shape: (150,) - dtype: int64 - standard_name: 'time' + DimCoord([2007-04-09 00:00:00, 2007-04-16 00:00:00, 2007-04-23 00:00:00, + ... + 2010-02-01 00:00:00, 2010-02-08 00:00:00, 2010-02-15 00:00:00], + standard_name='time', calendar='gregorian') Given two dates in datetime format, we can select all points between them. @@ -386,13 +372,9 @@ Given two dates in datetime format, we can select all points between them. ... time=lambda cell: d1 <= cell.point < d2) >>> within_st_swithuns_07 = long_ts.extract(st_swithuns_daterange_07) >>> print(within_st_swithuns_07.coord('time')) - DimCoord : time / (days since 2007-04-09, gregorian calendar) - points: [ - 2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, - 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00] - shape: (6,) - dtype: int64 - standard_name: 'time' + DimCoord([2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, + 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00], + standard_name='time', calendar='gregorian') Alternatively, we may rewrite this using :class:`iris.time.PartialDateTime` objects. @@ -406,13 +388,9 @@ objects. ... time=lambda cell: pdt1 <= cell.point < pdt2) >>> within_st_swithuns_07 = long_ts.extract(st_swithuns_daterange_07) >>> print(within_st_swithuns_07.coord('time')) - DimCoord : time / (days since 2007-04-09, gregorian calendar) - points: [ - 2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, - 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00] - shape: (6,) - dtype: int64 - standard_name: 'time' + DimCoord([2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, + 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00], + standard_name='time', calendar='gregorian') A more complex example might require selecting points over an annually repeating date range. We can select points within a certain part of the year, in this case @@ -425,24 +403,17 @@ PartialDateTime this becomes simple: ... time=lambda cell: PartialDateTime(month=7, day=15) <= cell < PartialDateTime(month=8, day=25)) >>> within_st_swithuns = long_ts.extract(st_swithuns_daterange) ... - >>> # Note: using summary(max_values) to show more of the points - >>> print(within_st_swithuns.coord('time').summary(max_values=100)) - DimCoord : time / (days since 2007-04-09, gregorian calendar) - points: [ - 2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, - 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00, - 2008-07-21 00:00:00, 2008-07-28 00:00:00, 2008-08-04 00:00:00, - 2008-08-11 00:00:00, 2008-08-18 00:00:00, 2009-07-20 00:00:00, - 2009-07-27 00:00:00, 2009-08-03 00:00:00, 2009-08-10 00:00:00, - 2009-08-17 00:00:00, 2009-08-24 00:00:00] - shape: (17,) - dtype: int64 - standard_name: 'time' + >>> print(within_st_swithuns.coord('time')) + DimCoord([2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, + 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00, + 2008-07-21 00:00:00, 2008-07-28 00:00:00, 2008-08-04 00:00:00, + 2008-08-11 00:00:00, 2008-08-18 00:00:00, 2009-07-20 00:00:00, + 2009-07-27 00:00:00, 2009-08-03 00:00:00, 2009-08-10 00:00:00, + 2009-08-17 00:00:00, 2009-08-24 00:00:00], standard_name='time', calendar='gregorian') Notice how the dates printed are between the range specified in the ``st_swithuns_daterange`` and that they span multiple years. -.. _strict-loading: Strict Loading -------------- diff --git a/docs/src/userguide/navigating_a_cube.rst b/docs/src/userguide/navigating_a_cube.rst index c5924a61c6..74b47b258e 100644 --- a/docs/src/userguide/navigating_a_cube.rst +++ b/docs/src/userguide/navigating_a_cube.rst @@ -33,9 +33,9 @@ We have already seen a basic string representation of a cube when printing: forecast_reference_time 2006-06-15 00:00:00 time 2006-06-15 00:00:00 Attributes: - Conventions 'CF-1.5' + Conventions CF-1.5 STASH m01s16i222 - source 'Data from Met Office Unified Model 6.01' + source Data from Met Office Unified Model 6.01 This representation is equivalent to passing the cube to the :func:`str` function. This function can be used on @@ -169,9 +169,9 @@ We can add and remove coordinates via :func:`Cube.add_dim_coord`_ package in order to generate +`Matplotlib `_ package in order to generate high quality, production ready 1D and 2D plots. -The functionality of the Matplotlib :py:mod:`~matplotlib.pyplot` module has +The functionality of the Matplotlib +`pyplot `_ module has been extended within Iris to facilitate easy visualisation of a cube's data. @@ -217,7 +218,7 @@ Plotting 2-Dimensional Cubes Creating Maps ------------- Whenever a 2D plot is created using an :class:`iris.coord_systems.CoordSystem`, -a cartopy :class:`~cartopy.mpl.geoaxes.GeoAxes` instance is created, which can be +a cartopy :class:`~cartopy.mpl.GeoAxes` instance is created, which can be accessed with the :func:`matplotlib.pyplot.gca` function. Given the current map, you can draw gridlines and coastlines amongst other @@ -225,8 +226,8 @@ things. .. seealso:: - :meth:`cartopy's gridlines() `, - :meth:`cartopy's coastlines() `. + :meth:`cartopy's gridlines() `, + :meth:`cartopy's coastlines() `. Cube Contour diff --git a/docs/src/userguide/saving_iris_cubes.rst b/docs/src/userguide/saving_iris_cubes.rst index 2ffc8c47d3..c801a1fbea 100644 --- a/docs/src/userguide/saving_iris_cubes.rst +++ b/docs/src/userguide/saving_iris_cubes.rst @@ -51,7 +51,6 @@ The :py:func:`iris.save` function passes all other keywords through to the saver >>> # Save a cube list to a PP file, appending to the contents of the file >>> # if it already exists >>> iris.save(cubes, "myfile.pp", append=True) - >>> # Save a cube to netCDF, defaults to NETCDF4 file format >>> iris.save(cubes[0], "myfile.nc") >>> # Save a cube list to netCDF, using the NETCDF3_CLASSIC storage option @@ -74,12 +73,6 @@ See for more details on supported arguments for the individual savers. -.. note:: - - The existence of a keyword argument for one saver does not guarantee the - same works for all savers. For example, it isn't possible to pass an - ``append`` keyword argument to the netCDF saver (see :ref:`netcdf_save`). - Customising the Save Process ---------------------------- @@ -109,7 +102,6 @@ Similarly a PP field may need to be written out with a specific value for LBEXP. yield field iris.fileformats.pp.save_fields(tweaked_fields(cubes[0]), '/tmp/app.pp') -.. _netcdf_save: NetCDF ^^^^^^ diff --git a/docs/src/userguide/subsetting_a_cube.rst b/docs/src/userguide/subsetting_a_cube.rst index 5112d9689a..1c68cafb8d 100644 --- a/docs/src/userguide/subsetting_a_cube.rst +++ b/docs/src/userguide/subsetting_a_cube.rst @@ -30,7 +30,7 @@ A subset of a cube can be "extracted" from a multi-dimensional cube in order to Scalar coordinates: grid_latitude 0.0 degrees Attributes: - Conventions 'CF-1.5' + Conventions CF-1.5 In this example we start with a 3 dimensional cube, with dimensions of ``height``, ``grid_latitude`` and ``grid_longitude``, @@ -97,8 +97,8 @@ same way as loading with constraints: time 2009-11-19 10:00:00 Attributes: STASH m01s00i004 - source 'Data from Met Office Unified Model' - um_version '7.3' + source Data from Met Office Unified Model + um_version 7.3 Cube Iteration diff --git a/docs/src/whatsnew/1.4.rst b/docs/src/whatsnew/1.4.rst index 989198296c..858f985ec6 100644 --- a/docs/src/whatsnew/1.4.rst +++ b/docs/src/whatsnew/1.4.rst @@ -182,7 +182,8 @@ Cubes With no Vertical Coord can now be Exported to GRIB -------------------------------------------------------- Iris can now export cubes with no vertical coord to GRIB. -The solution is still under discussion: See :issue:`519`. +The solution is still under discussion: See +https://github.com/SciTools/iris/issues/519. .. _simple_cfg: diff --git a/docs/src/whatsnew/3.0.rst b/docs/src/whatsnew/3.0.rst index 771a602954..77458c70e9 100644 --- a/docs/src/whatsnew/3.0.rst +++ b/docs/src/whatsnew/3.0.rst @@ -35,8 +35,8 @@ This document explains the changes made to Iris for this release :ref:`incompatible changes ` and :ref:`deprecations `. - And finally, get in touch with us on :issue:`GitHub` if you have - any issues or feature requests for improving Iris. Enjoy! + And finally, get in touch with us on `GitHub`_ if you have any issues or + feature requests for improving Iris. Enjoy! v3.0.1 (27 Jan 2021) @@ -617,6 +617,7 @@ v3.0.4 (22 July 2021) .. _xxHash: https://github.com/Cyan4973/xxHash .. _PyKE: https://pypi.org/project/scitools-pyke/ .. _@owena11: https://github.com/owena11 +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose .. _readthedocs: https://readthedocs.org/ .. _CF Conventions and Metadata: https://cfconventions.org/ .. _flake8: https://flake8.pycqa.org/en/stable/ diff --git a/docs/src/whatsnew/3.1.rst b/docs/src/whatsnew/3.1.rst index bd046a0a24..165e20d9bc 100644 --- a/docs/src/whatsnew/3.1.rst +++ b/docs/src/whatsnew/3.1.rst @@ -25,8 +25,8 @@ This document explains the changes made to Iris for this release * Multiple improvements to developer guide documentation. See entries in the :ref:`"Documentation" section `, below. - And finally, get in touch with us on :issue:`GitHub` if you have - any issues or feature requests for improving Iris. Enjoy! + And finally, get in touch with us on `GitHub`_ if you have any issues or + feature requests for improving Iris. Enjoy! 📢 Announcements @@ -315,6 +315,7 @@ This document explains the changes made to Iris for this release .. _blacken-docs: https://github.com/asottile/blacken-docs .. _conda-lock: https://github.com/conda-incubator/conda-lock .. _deprecated numpy 1.20 aliases for builtin types: https://numpy.org/doc/1.20/release/1.20.0-notes.html#using-the-aliases-of-builtin-types-like-np-int-is-deprecated +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose .. _Met Office: https://www.metoffice.gov.uk/ .. _numpy: https://numpy.org/doc/stable/release/1.20.0-notes.html .. |pre-commit.ci| image:: https://results.pre-commit.ci/badge/github/SciTools/iris/main.svg diff --git a/docs/src/whatsnew/index.rst b/docs/src/whatsnew/index.rst index f425e649b9..fabb056484 100644 --- a/docs/src/whatsnew/index.rst +++ b/docs/src/whatsnew/index.rst @@ -10,7 +10,7 @@ Iris versions. .. toctree:: :maxdepth: 1 - 3.2.rst + latest.rst 3.1.rst 3.0.rst 2.4.rst diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst deleted file mode 120000 index 2bdbea5d85..0000000000 --- a/docs/src/whatsnew/latest.rst +++ /dev/null @@ -1 +0,0 @@ -3.2.rst \ No newline at end of file diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst new file mode 100644 index 0000000000..2ddcdb9b34 --- /dev/null +++ b/docs/src/whatsnew/latest.rst @@ -0,0 +1,265 @@ +.. include:: ../common_links.inc + +|iris_version| |build_date| [unreleased] +**************************************** + +This document explains the changes made to Iris for this release +(:doc:`View all changes `.) + + +.. dropdown:: :opticon:`report` |iris_version| Release Highlights + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + :open: + + The highlights for this minor release of Iris include: + + * We've added support for `UGRID`_ meshes which can now be loaded and attached + to a cube. + + And finally, get in touch with us on `GitHub`_ if you have any issues or + feature requests for improving Iris. Enjoy! + + +📢 Announcements +================ + +#. Welcome to `@wjbenfold`_, `@tinyendian`_, `@larsbarring`_, `@akuhnregnier`_, + `@bsherratt`_ and `@aaronspring`_ who made their first contributions to Iris. + The first of many we hope! +#. Congratulations to `@wjbenfold`_ who has become a core developer for Iris! 🎉 + + +✨ Features +=========== + +#. `@bjlittle`_, `@pp-mo`_, `@trexfeathers`_ and `@stephenworsley`_ added + support for unstructured meshes, as described by `UGRID`_. This involved + adding a data model (:pull:`3968`, :pull:`4014`, :pull:`4027`, :pull:`4036`, + :pull:`4053`, :pull:`4439`) and API (:pull:`4063`, :pull:`4064`), and + supporting representation (:pull:`4033`, :pull:`4054`) of data on meshes. + Most of this new API can be found in :mod:`iris.experimental.ugrid`. The key + objects introduced are :class:`iris.experimental.ugrid.mesh.Mesh`, + :class:`iris.experimental.ugrid.mesh.MeshCoord` and + :obj:`iris.experimental.ugrid.load.PARSE_UGRID_ON_LOAD`. + A :class:`iris.experimental.ugrid.mesh.Mesh` contains a full description of a UGRID + type mesh. :class:`~iris.experimental.ugrid.mesh.MeshCoord`\ s are coordinates that + reference and represent a :class:`~iris.experimental.ugrid.mesh.Mesh` for use + on a :class:`~iris.cube.Cube`. :class:`~iris.cube.Cube`\ s are also given the + property :attr:`~iris.cube.Cube.mesh` which returns a + :class:`~iris.experimental.ugrid.mesh.Mesh` if one is attached to the + :class:`~iris.cube.Cube` via a :class:`~iris.experimental.ugrid.mesh.MeshCoord`. + +#. `@trexfeathers`_ added support for loading unstructured mesh data from netcdf data, + for files using the `UGRID`_ conventions. + The context manager :obj:`~iris.experimental.ugrid.load.PARSE_UGRID_ON_LOAD` + provides a way to load UGRID files so that :class:`~iris.cube.Cube`\ s can be + returned with a :class:`~iris.experimental.ugrid.mesh.Mesh` attached. + (:pull:`4058`). + +#. `@pp-mo`_ added support to save cubes with meshes to netcdf files, using the + `UGRID`_ conventions. + The existing :meth:`iris.save` function now does this, when saving cubes with meshes. + A routine :meth:`iris.experimental.ugrid.save.save_mesh` allows saving + :class:`~iris.experimental.ugrid.mesh.Mesh` objects to netcdf *without* any associated data + (i.e. not attached to cubes). + (:pull:`4318` and :pull:`4339`). + +#. `@trexfeathers`_ added :meth:`iris.experimental.ugrid.mesh.Mesh.from_coords` + for inferring a :class:`~iris.experimental.ugrid.mesh.Mesh` from an + appropriate collection of :class:`iris.coords.Coord`\ s. + +#. `@larsbarring`_ updated :func:`~iris.util.equalise_attributes` to return a list of dictionaries + containing the attributes removed from each :class:`~iris.cube.Cube`. (:pull:`4357`) + +#. `@trexfeathers`_ enabled streaming of **all** lazy arrays when saving to + NetCDF files (was previously just :class:`~iris.cube.Cube` + :attr:`~iris.cube.Cube.data`). This is + important given the much greater size of + :class:`~iris.coords.AuxCoord` :attr:`~iris.coords.AuxCoord.points` and + :class:`~iris.experimental.ugrid.mesh.Connectivity` + :attr:`~iris.experimental.ugrid.mesh.Connectivity.indices` under the + `UGRID`_ model. (:pull:`4375`) + +#. `@bsherratt`_ added a `threshold` parameter to + :meth:`~iris.cube.Cube.intersection` (:pull:`4363`) + +#. `@wjbenfold`_ added test data to ci benchmarks so that it is accessible to + benchmark scripts. Also added a regridding benchmark that uses this data + (:pull:`4402`) + + +🐛 Bugs Fixed +============= + +#. `@rcomer`_ fixed :meth:`~iris.cube.Cube.intersection` for special cases where + one cell's bounds align with the requested maximum and negative minimum, fixing + :issue:`4221`. (:pull:`4278`) + +#. `@bsherratt`_ fixed further edge cases in + :meth:`~iris.cube.Cube.intersection`, including :issue:`3698` (:pull:`4363`) + +#. `@tinyendian`_ fixed the error message produced by :meth:`~iris.cube.CubeList.concatenate_cube` + when a cube list contains cubes with different names, which will no longer report + "Cube names differ: var1 != var1" if var1 appears multiple times in the list + (:issue:`4342`, :pull:`4345`) + +#. `@larsbarring`_ fixed :class:`~iris.coord_systems.GeoCS` to handle spherical ellipsoid + parameter inverse_flattening = 0 (:issue: `4146`, :pull:`4348`) + +#. `@pdearnshaw`_ fixed an error in the call to :class:`cftime.datetime` in + :mod:`~iris.fileformats.pp_save_rules` that prevented the saving to PP of climate + means for DJF (:pull:`4391`) + +#. `@wjbenfold`_ improved the error message for failure of :meth:`~iris.cube.CubeList.concatenate` + to indicate that the value of a scalar coordinate may be mismatched, rather than the metadata + (:issue:`4096`, :pull:`4387`) + +#. `@bsherratt`_ fixed a regression to the NAME file loader introduced in 3.0.4, + as well as some long-standing bugs with vertical coordinates and number + formats. (:pull:`4411`) + +#. `@rcomer`_ fixed :meth:`~iris.cube.Cube.subset` to alway return ``None`` if + no value match is found. (:pull:`4417`) + +#. `@wjbenfold`_ resolved an issue that previously caused regridding with lazy + data to take significantly longer than with real data. Relevant benchmark + shows a time decrease from >10s to 625ms. (:issue:`4280`, :pull:`4400`) + +#. `@wjbenfold`_ changed :meth:`iris.util.points_step` to stop it from warning + when applied to a single point (:issue:`4250`, :pull:`4367`) + +#. `@trexfeathers`_ changed :class:`~iris.coords._DimensionalMetadata` and + :class:`~iris.experimental.ugrid.Connectivity` equality methods to preserve + array laziness, allowing efficient comparisons even with larger-than-memory + objects. (:pull:`4439`) + + +💣 Incompatible Changes +======================= + +#. N/A + + +🚀 Performance Enhancements +=========================== + +#. N/A + + +🔥 Deprecations +=============== + +#. N/A + + +🔗 Dependencies +=============== + +#. `@bjlittle`_ introduced the ``cartopy >=0.20`` minimum pin. + (:pull:`4331`) + +#. `@trexfeathers`_ introduced the ``cf-units >=3`` and ``nc-time-axis >=1.3`` + minimum pins. (:pull:`4356`) + +#. `@bjlittle`_ introduced the ``numpy >=1.19`` minimum pin, in + accordance with `NEP-29`_ deprecation policy. (:pull:`4386`) + + +📚 Documentation +================ + +#. `@rcomer`_ updated the "Plotting Wind Direction Using Quiver" Gallery + example. (:pull:`4120`) + +#. `@trexfeathers`_ included `Iris GitHub Discussions`_ in + :ref:`get involved `. (:pull:`4307`) + +#. `@wjbenfold`_ improved readability in :ref:`userguide interpolation + section `. (:pull:`4314`) + +#. `@wjbenfold`_ added explanation about the absence of | operator for + :class:`iris.Constraint` to :ref:`userguide loading section + ` and to api reference documentation. (:pull:`4321`) + +#. `@trexfeathers`_ added more detail on making `iris-test-data`_ available + during :ref:`developer_running_tests`. (:pull:`4359`) + +#. `@lbdreyer`_ added a section to the release documentation outlining the role + of the :ref:`release_manager`. (:pull:`4413`) + +#. `@trexfeathers`_ encouraged contributors to include type hinting in code + they are working on - :ref:`code_formatting`. (:pull:`4390`) + + +💼 Internal +=========== + +#. `@trexfeathers`_ set the linkcheck to ignore + http://www.nationalarchives.gov.uk/doc/open-government-licence since this + always works locally, but never within CI. (:pull:`4307`) + +#. `@wjbenfold`_ netCDF integration tests now skip ``TestConstrainedLoad`` if + test data is missing (:pull:`4319`) + +#. `@wjbenfold`_ excluded ``Good First Issue`` labelled issues from being + marked stale. (:pull:`4317`) + +#. `@tkknight`_ added additional make targets for reducing the time of the + documentation build including ``html-noapi`` and ``html-quick``. + Useful for development purposes only. For more information see + :ref:`contributing.documentation.building` the documentation. (:pull:`4333`) + +#. `@rcomer`_ modified the ``animation`` test to prevent it throwing a warning + that sometimes interferes with unrelated tests. (:pull:`4330`) + +#. `@rcomer`_ removed a now redundant workaround in :func:`~iris.plot.contourf`. + (:pull:`4349`) + +#. `@trexfeathers`_ refactored :mod:`iris.experimental.ugrid` into sub-modules. + (:pull:`4347`). + +#. `@bjlittle`_ enabled the `sort-all`_ `pre-commit`_ hook to automatically + sort ``__all__`` entries into alphabetical order. (:pull:`4353`) + +#. `@rcomer`_ modified a NetCDF saver test to prevent it triggering a numpy + deprecation warning. (:issue:`4374`, :pull:`4376`) + +#. `@akuhnregnier`_ removed addition of period from + :func:`~iris.analysis.cartography.wrap_lons` and updated affected tests + using assertArrayAllClose following :issue:`3993`. + (:pull:`4421`) + +#. `@rcomer`_ updated some tests to work with Matplotlib v3.5. (:pull:`4428`) + +#. `@rcomer`_ applied minor fixes to some regridding tests. (:pull:`4432`) + +#. `@lbdreyer`_ corrected the license PyPI classifier. (:pull:`4435`) + +#. `@aaronspring `_ exchanged `dask` with + `dask-core` in testing environments reducing the number of dependencies + installed for testing. (:pull:`4434`) + +#. `@wjbenfold`_ prevented github action runs in forks (:issue:`4441`, + :pull:`4444`) + +.. comment + Whatsnew author names (@github name) in alphabetical order. Note that, + core dev names are automatically included by the common_links.inc: + +.. _@aaronspring: https://github.com/aaronspring +.. _@akuhnregnier: https://github.com/akuhnregnier +.. _@bsherratt: https://github.com/bsherratt +.. _@larsbarring: https://github.com/larsbarring +.. _@pdearnshaw: https://github.com/pdearnshaw +.. _@tinyendian: https://github.com/tinyendian + +.. comment + Whatsnew resources in alphabetical order: + +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose +.. _NEP-29: https://numpy.org/neps/nep-0029-deprecation_policy.html +.. _UGRID: http://ugrid-conventions.github.io/ugrid-conventions/ +.. _sort-all: https://github.com/aio-libs/sort-all diff --git a/docs/src/whatsnew/latest.rst.template b/docs/src/whatsnew/latest.rst.template new file mode 100644 index 0000000000..ced0778069 --- /dev/null +++ b/docs/src/whatsnew/latest.rst.template @@ -0,0 +1,112 @@ +.. include:: ../common_links.inc + +|iris_version| |build_date| [unreleased] +**************************************** + +This document explains the changes made to Iris for this release +(:doc:`View all changes `.) + + +.. dropdown:: :opticon:`report` |iris_version| Release Highlights + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + :open: + + The highlights for this major/minor release of Iris include: + + * N/A + + And finally, get in touch with us on `GitHub`_ if you have any issues or + feature requests for improving Iris. Enjoy! + + +NOTE: section below is a template for bugfix patches +==================================================== + (Please remove this section when creating an initial 'latest.rst') + +v3.X.X (DD MMM YYYY) +==================== + +.. dropdown:: :opticon:`alert` v3.X.X Patches + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + + The patches in this release of Iris include: + + #. N/A + +NOTE: section above is a template for bugfix patches +==================================================== + (Please remove this section when creating an initial 'latest.rst') + + + +📢 Announcements +================ + +#. N/A + + +✨ Features +=========== + +#. N/A + + +🐛 Bugs Fixed +============= + +#. N/A + + +💣 Incompatible Changes +======================= + +#. N/A + + +🚀 Performance Enhancements +=========================== + +#. N/A + + +🔥 Deprecations +=============== + +#. N/A + + +🔗 Dependencies +=============== + +#. N/A + + +📚 Documentation +================ + +#. N/A + + +💼 Internal +=========== + +#. N/A + + +.. comment + Whatsnew author names (@github name) in alphabetical order. Note that, + core dev names are automatically included by the common_links.inc: + + + + +.. comment + Whatsnew resources in alphabetical order: + +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose diff --git a/etc/cf-standard-name-table.xml b/etc/cf-standard-name-table.xml index bd76168192..5a19f8d5b1 100644 --- a/etc/cf-standard-name-table.xml +++ b/etc/cf-standard-name-table.xml @@ -1,7 +1,7 @@ - 78 - 2021-09-21T11:55:06Z + 77 + 2021-01-19T13:38:50Z Centre for Environmental Data Analysis support@ceda.ac.uk @@ -489,13 +489,6 @@ - - m2 s-2 - - - One-half the scalar product of the air velocity and vorticity vectors, where vorticity refers to the standard name atmosphere_upward_absolute_vorticity. Helicity is proportional to the strength of the flow, the amount of vertical wind shear, and the amount of turning in the flow. - - m2 s-1 35 @@ -2474,7 +2467,7 @@ 1 - The "beam_consistency_indicator" is the degree to which the received acoustic pulse is correlated with the transmitted pulse. It is used as a data quality assessment parameter in ADCP (acoustic doppler current profiler) instruments and is frequently referred to as "correlation magnitude". Convention is that the larger the value, the higher the signal to noise ratio and therefore the better the quality of the current vector measurements; the maximum value of the indicator is 128. + The "beam_consistency_indicator" is the degree to which the magnitudes of a collection (ensemble) of acoustic signals from multiple underwater acoustic transceivers relate to each other. It is used as a data quality assessment parameter in ADCP (acoustic doppler current profiler) instruments and is frequently referred to as "correlation magnitude". Convention is that the larger the value, the higher the signal to noise ratio and therefore the better the quality of the current vector measurements; the maximum value of the indicator is 128. @@ -2498,11 +2491,11 @@ The specification of a physical process by the phrase due_to_process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. Altitude is the (geometric) height above the geoid, which is the reference geopotential surface. The geoid is similar to mean sea level. "Bedrock" is the solid Earth surface beneath land ice, ocean water or soil. The zero of bedrock altitude change is arbitrary. Isostatic adjustment is the vertical movement of the lithosphere due to changing surface ice and water loads. - + - "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. The quantity with standard name biological_taxon_lsid is the machine-readable identifier based on a taxon registration system using the syntax convention specified for the Life Science Identifier (LSID) - urn:lsid:<Authority>:<Namespace>:<ObjectID>[:<Version>]. This includes the reference classification in the element and these are restricted by the LSID governance. It is strongly recommended in CF that the authority chosen is World Register of Marine Species (WoRMS) for oceanographic data and Integrated Taxonomic Information System (ITIS) for freshwater and terrestrial data. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. This identifier is a narrower equivalent to the scientificNameID field in the Darwin Core Standard. + "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. The quantity with standard name biological_taxon_identifier is the machine-readable identifier for the taxon registration in either WoRMS (the AphiaID) or ITIS (the taxonomic serial number or TSN), including namespace. The namespace strings are 'aphia:' or 'tsn:'. For example, Calanus finmarchicus is encoded as either 'aphia:104464' or 'tsn:85272'. For the marine domain WoRMS has more complete coverage and so aphia Ids are preferred. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. @@ -2694,13 +2687,6 @@ "Amount" means mass per unit area. Zero change in land ice amount is an arbitrary level. "Land ice" means glaciers, ice-caps and ice-sheets resting on bedrock and also includes ice-shelves. - - kg - - - Zero change in land ice mass is an arbitrary level. "Land ice" means glaciers, ice-caps and ice-sheets resting on bedrock and also includes ice-shelves. The horizontal domain over which the quantity is calculated is described by the associated coordinate variables and coordinate bounds or by a coordinate variable or scalar coordinate variable with the standard name of "region" supplied according to section 6.1.1 of the CF conventions. - - kg m-2 @@ -2936,7 +2922,7 @@ m-3 - "Colony forming unit" means an estimate of the viable bacterial or fungal numbers determined by counting colonies grown from a sample. "Number concentration" means the number of particles or other specified objects per unit volume. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Colony forming unit" means an estimate of the viable bacterial or fungal numbers determined by counting colonies grown from a sample. "Number concentration" means the number of particles or other specified objects per unit volume. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. @@ -3093,13 +3079,6 @@ Depth is the vertical distance below the surface. - - m - - - The phrase depth_at_base_of_unfrozen_ground is the instantaneous depth of the downward penetration of thaw from the ground surface at a given time. Permafrost is soil or rock that has remained at a temperature at or below zero degrees Celsius throughout the seasonal cycle for two or more consecutive years. The maximum measurable depth_at_base_of_unfrozen_ground value as recorded at the end of a thawing season corresponds to the permafrost_active_layer_thickness. - - m @@ -3163,13 +3142,6 @@ "Aerosol" means the system of suspended liquid or solid particles in air (except cloud droplets) and their carrier gas, the air itself. "Ambient_aerosol" means that the aerosol is measured or modelled at the ambient state of pressure, temperature and relative humidity that exists in its immediate environment. "Ambient aerosol particles" are aerosol particles that have taken up ambient water through hygroscopic growth. The extent of hygroscopic growth depends on the relative humidity and the composition of the particles. To specify the relative humidity and temperature at which the quantity described by the standard name applies, provide scalar coordinate variables with standard names of "relative_humidity" and "air_temperature". - - K - - - Sea surface temperature is usually abbreviated as "SST". It is the temperature of sea water near the surface (including the part under sea-ice, if any), not the skin or interface temperature, whose standard names are sea_surface_skin_temperature and surface_temperature, respectively. For the temperature of sea water at a particular depth or layer, a data variable of "sea_water_temperature" with a vertical coordinate axis should be used. Air temperature is the bulk temperature of the air, not the surface (skin) temperature. - - Pa @@ -3751,13 +3723,6 @@ A velocity is a vector quantity. "Eastward" indicates a vector component which is positive when directed eastward (negative westward). Flood water is water that covers land which is normally not covered by water. - - m s-1 - - - A velocity is a vector quantity. "Eastward" indicates a vector component which is positive when directed eastward (negative westward). Friction velocity is a reference wind velocity derived from the relationship between air density and downward stress and is usually applied at a level close to the surface where stress is assumed to independent of height and approximately proportional to the square of mean velocity. - - m s-1 @@ -4612,13 +4577,6 @@ "Area fraction" is the fraction of a grid cell's horizontal area that has some characteristic of interest. It is evaluated as the area of interest divided by the grid cell area. It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. "Layer" means any layer with upper and lower boundaries that have constant values in some vertical coordinate. There must be a vertical coordinate variable indicating the extent of the layer(s). If the layers are model layers, the vertical coordinate can be "model_level_number", but it is recommended to specify a physical coordinate (in a scalar or auxiliary coordinate variable) as well. Standard names also exist for high, medium and low cloud types. Standard names referring only to "cloud_area_fraction" should be used for quantities for the whole atmosphere column. Cloud area fraction is also called "cloud amount" and "cloud cover". - - 1 - - - ice_volume_in_frozen_ground_in_excess_of_pore_volume_in_unfrozen_ground_expressed_as_fraction_of_frozen_ground_volume represents the fractional amount of "excess ice" in frozen ground. Excess ice is the volume of ice in the ground which exceeds the total pore volume that the ground would have under natural unfrozen conditions. Due to the presence of ground ice, the total water content of a frozen soil may exceed that corresponding to its normally consolidated state when unfrozen. As a result, upon thawing, a soil containing excess ice will settle under its own weight until it attains its consolidated state. Reference: van Everdingen, R. O. editor 1998: Multi-language glossary of permafrost and related ground ice terms. International Permafrost Association. - - m3 s-1 @@ -4630,7 +4588,7 @@ m s-1 - Sea water velocity is a vector quantity that is the speed at which water travels in a specified direction. The "indicative error" is an estimate of the quality of a sea water velocity profile measured using an ADCP (acoustic doppler current profiler). It is determined by the difference between the vertical velocity calculated from two 3-beam solutions. The parameter is frequently referred to as the "error velocity". + Sea water velocity is a vector quantity that is the speed at which water travels in a specified direction. The "indicative error" is an estimate of the quality of a sea water velocity profile measured using an ADCP (acoustic doppler current profiler). It is determined by differencing duplicate error velocity measurements made using different pairs of beams. The parameter is frequently referred to as the "error velocity". @@ -7713,13 +7671,6 @@ "Content" indicates a quantity per unit area. - - J Kg-1 - - - The lightning_potential_index measures the potential for charge generation and separation that leads to lightning flashes in convective thunderstorms. It is derived from the model simulated grid-scale updraft velocity and the mass mixing-ratios of liquid water, cloud ice, snow, and graupel. - - J @@ -8130,21 +8081,21 @@ kg m-3 - "Mass concentration" means mass per unit volume and is used in the construction "mass_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. Mass concentration of biota expressed as carbon is also referred to as "carbon biomass". "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Mass concentration" means mass per unit volume and is used in the construction "mass_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. Mass concentration of biota expressed as carbon is also referred to as "carbon biomass". "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. kg m-3 - "Mass concentration" means mass per unit volume and is used in the construction "mass_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. Chlorophylls are the green pigments found in most plants, algae and cyanobacteria; their presence is essential for photosynthesis to take place. There are several different forms of chlorophyll that occur naturally. All contain a chlorin ring (chemical formula C20H16N4) which gives the green pigment and a side chain whose structure varies. The naturally occurring forms of chlorophyll contain between 35 and 55 carbon atoms. + "Mass concentration" means mass per unit volume and is used in the construction "mass_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. Chlorophylls are the green pigments found in most plants, algae and cyanobacteria; their presence is essential for photosynthesis to take place. There are several different forms of chlorophyll that occur naturally. All contain a chlorin ring (chemical formula C20H16N4) which gives the green pigment and a side chain whose structure varies. The naturally occurring forms of chlorophyll contain between 35 and 55 carbon atoms. kg m-3 - "Mass concentration" means mass per unit volume and is used in the construction "mass_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. Mass concentration of biota expressed as nitrogen is also referred to as "nitrogen biomass". "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Mass concentration" means mass per unit volume and is used in the construction "mass_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. Mass concentration of biota expressed as nitrogen is also referred to as "nitrogen biomass". "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. @@ -9498,13 +9449,6 @@ Mass fraction is used in the construction mass_fraction_of_X_in_Y, where X is a material constituent of Y. It means the ratio of the mass of X to the mass of Y (including X). A chemical species denoted by X may be described by a single term such as 'nitrogen' or a phrase such as 'nox_expressed_as_nitrogen'. Chlorophylls are the green pigments found in most plants, algae and cyanobacteria; their presence is essential for photosynthesis to take place. There are several different forms of chlorophyll that occur naturally; all contain a chlorin ring which gives the green pigment and a side chain whose structure varies. Chlorophyll-a is the most commonly occurring form of natural chlorophyll. - - 1 - - - "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y", where X is a material constituent of Y. It is evaluated as the mass of X divided by the mass of Y (including X). It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. Grain-size class distribution is based on the Udden-Wentworth scale. - - 1 @@ -9666,13 +9610,6 @@ Mass fraction is used in the construction mass_fraction_of_X_in_Y, where X is a material constituent of Y. It means the ratio of the mass of X to the mass of Y (including X). Graupel consists of heavily rimed snow particles, often called snow pellets; often indistinguishable from very small soft hail except when the size convention that hail must have a diameter greater than 5 mm is adopted. Reference: American Meteorological Society Glossary http://glossary.ametsoc.org/wiki/Graupel. There are also separate standard names for hail. Standard names for "graupel_and_hail" should be used to describe data produced by models that do not distinguish between hail and graupel. - - 1 - - - "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y'', where X is a material constituent of Y. It is evaluated as the mass of X divided by the mass of Y (including X). It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. Grain-size class distribution is based on the Udden-Wentworth scale. - - 1 @@ -9981,13 +9918,6 @@ "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y", where X is a material constituent of Y. It means the ratio of the mass of X to the mass of Y (including X). A chemical or biological species denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction A_expressed_as_B, where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Noy" describes a family of chemical species. The family usually includes atomic nitrogen (N), nitrogen monoxide (NO), nitrogen dioxide (NO2), dinitrogen pentoxide (N2O5), nitric acid (HNO3), peroxynitric acid (HNO4), bromine nitrate (BrONO2) , chlorine nitrate (ClONO2) and organic nitrates (most notably peroxyacetyl nitrate, sometimes referred to as PAN, (CH3COO2NO2)). The list of individual species that are included in a quantity having a group chemical standard name can vary between models. Where possible, the data variable should be accompanied by a complete description of the species represented, for example, by using a comment attribute. - - 1 - - - "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y", where X is a material constituent of Y. It is evaluated as the mass of X divided by the mass of Y (including X). It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. - - 1 @@ -10261,13 +10191,6 @@ The quantity with standard name mass_fraction_of_rainfall_falling_onto_surface_snow is the mass of rainfall falling onto snow as a fraction of the mass of rainfall falling within the area of interest. Surface snow refers to the snow on the solid ground or on surface ice cover, but excludes, for example, falling snowflakes and snow on plants. The surface called "surface" means the lower boundary of the atmosphere. Unless indicated in the cell_methods attribute, a quantity is assumed to apply to the whole area of each horizontal grid box. - - 1 - - - "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y'', where X is a material constituent of Y. It is evaluated as the mass of X divided by the mass of Y (including X). It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. Grain-size class distribution is based on the Udden-Wentworth scale. - - 1 @@ -10296,13 +10219,6 @@ "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y", where X is a material constituent of Y. It means the ratio of the mass of X to the mass of Y (including X). A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". Shallow convective cloud is nonprecipitating cumulus cloud with a cloud top below 3000m above the surface produced by the convection schemes in an atmosphere model. Some atmosphere models differentiate between shallow and deep convection. "Cloud liquid water" refers to the liquid phase of cloud water. A diameter of 0.2 mm has been suggested as an upper limit to the size of drops that shall be regarded as cloud drops; larger drops fall rapidly enough so that only very strong updrafts can sustain them. Any such division is somewhat arbitrary, and active cumulus clouds sometimes contain cloud drops much larger than this. Reference: AMS Glossary http://glossary.ametsoc.org/wiki/Cloud_drop. - - 1 - - - "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y'', where X is a material constituent of Y. It is evaluated as the mass of X divided by the mass of Y (including X). It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. Grain-size class distribution is based on the Udden-Wentworth scale. - - 1 @@ -10839,14 +10755,14 @@ mol m-3 - "Mole concentration" means number of moles per unit volume, also called "molarity", and is used in the construction "mole_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Mole concentration" means number of moles per unit volume, also called "molarity", and is used in the construction "mole_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. mol m-3 - "Mole concentration" means number of moles per unit volume, also called "molarity", and is used in the construction "mole_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Mole concentration" means number of moles per unit volume, also called "molarity", and is used in the construction "mole_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. @@ -12732,13 +12648,6 @@ The construction "moles_of_X_per_unit_mass_in_Y" is also called "molality" of X in Y, where X is a material constituent of Y. A chemical or biological species denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The chemical formula of CFC11 is CFCl3. The IUPAC name for CFC11 is trichloro(fluoro)methane. - - mol kg-1 - - - The construction "moles_of_X_per_unit_mass_in_Y" is also called "molality" of X in Y, where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". "Dissolved inorganic carbon" describes a family of chemical species in solution, including carbon dioxide, carbonic acid and the carbonate and bicarbonate anions. "Dissolved inorganic carbon" is the term used in standard names for all species belonging to the family that are represented within a given model. The list of individual species that are included in a quantity having a group chemical standard name can vary between models. Where possible, the data variable should be accompanied by a complete description of the species represented, for example, by using a comment attribute. - - mol kg-1 @@ -13145,13 +13054,6 @@ A velocity is a vector quantity. "Northward" indicates a vector component which is positive when directed northward (negative southward). Flood water is water that covers land which is normally not covered by water. - - m s-1 - - - A velocity is a vector quantity. "Northward" indicates a vector component which is positive when directed northward (negative southward). Friction velocity is a reference wind velocity derived from the relationship between air density and downward stress and is usually applied at a level close to the surface where stress is assumed to independent of height and approximately proportional to the square of mean velocity. - - W m-2 @@ -13443,7 +13345,7 @@ m-3 - "Number concentration" means the number of particles or other specified objects per unit volume. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Number concentration" means the number of particles or other specified objects per unit volume. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. @@ -13534,21 +13436,21 @@ 1 - Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". + Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". 1 - Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". + Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". 1 - The construction lwe_thickness_of_X_amount or _content means the vertical extent of a layer of liquid water having the same mass per unit area. "Precipitation" in the earth's atmosphere means precipitation of water in all phases. The abbreviation "lwe" means liquid water equivalent. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". + The construction lwe_thickness_of_X_amount or _content means the vertical extent of a layer of liquid water having the same mass per unit area. "Precipitation" in the earth's atmosphere means precipitation of water in all phases. The abbreviation "lwe" means liquid water equivalent. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". @@ -13562,7 +13464,7 @@ 1 - Wind is defined as a two-dimensional (horizontal) air velocity vector, with no vertical component. (Vertical motion in the atmosphere has the standard name upward_air_velocity.) The wind speed is the magnitude of the wind velocity. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". + Wind is defined as a two-dimensional (horizontal) air velocity vector, with no vertical component. (Vertical motion in the atmosphere has the standard name upward_air_velocity.) The wind speed is the magnitude of the wind velocity. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". @@ -17457,13 +17359,6 @@ The "reaction rate" is the rate at which the reactants of a chemical reaction form the products. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. The rate of "hydroxyl radical destruction due to reaction with nmvoc" is the nmvoc reactivity with regard to reactions with OH. It is the weighted sum of the reactivity of all individual nmvoc species with OH. The chemical formula for the hydroxyl radical is OH. In chemistry, a "radical" is a highly reactive, and therefore short lived, species. The abbreviation "nmvoc" means non methane volatile organic compounds; "nmvoc" is the term used in standard names to describe the group of chemical species having this classification that are represented within a given model. The list of individual species that are included in a quantity having a group chemical standard name can vary between models. Where possible, the data variable should be accompanied by a complete description of the species represented, for example, by using a comment attribute. - - 1 - - - The phrase "ratio_of_X_to_Y" means X/Y. "X_volume" means the volume occupied by X within the grid cell. Pore volume is the volume of the porosity of the ground under natural, unfrozen conditions. This is often known as "ice saturation index". - - K s-1 @@ -17496,7 +17391,7 @@ 1 - Realization is used to label a dimension that can be thought of as a statistical sample, e.g., labelling members of a model ensemble. + Realization is used to label a dimension that can be thought of asa statistical sample, e.g., labelling members of a model ensemble. @@ -17702,13 +17597,6 @@ The sea_floor_depth_below_sea_surface is the vertical distance between the sea surface and the seabed as measured at a given point in space including the variance caused by tides and possibly waves. - - m - - - The average size of grains (also known as particles) in a sediment sample. - - 1 @@ -17793,13 +17681,6 @@ The term sea_ice_extent means the total area of all grid cells in which the sea ice area fraction equals or exceeds a threshold, often chosen to be 15 per cent. The threshold must be specified by supplying a coordinate variable or scalar coordinate variable with the standard name of sea_ice_area_fraction. The horizontal domain over which sea ice extent is calculated is described by the associated coordinate variables and coordinate bounds or by a coordinate variable or scalar coordinate variable with the standard name of "region" supplied according to section 6.1.1 of the CF conventions. "Sea ice" means all ice floating in the sea which has formed from freezing sea water, rather than by other processes such as calving of land ice to form icebergs. - - m - - - "Sea ice" means all ice floating in the sea which has formed from freezing sea water, rather than by other processes such as calving of land ice to form icebergs. An ice floe is a flat expanse of sea ice, generally taken to be less than 10 km across. ice_floe_diameter corresponds to the diameter of a circle with the same area as the ice floe. - - m @@ -17975,20 +17856,6 @@ Sea surface density is the density of sea water near the surface (including the part under sea-ice, if any). - - Pa - - - The surface called "sea surface" means the upper boundary of the ocean. "Surface stress" means the shear stress (force per unit area) exerted at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, surface stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Eastward" indicates a vector component which is positive when directed northward (negative southward). "Downward eastward" indicates the ZX component of a tensor. A downward eastward stress is a downward flux of eastward momentum, which accelerates the lower medium eastward and the upper medium westward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. The phrase "dissipation_of_sea_surface_waves" means the stress associated with sea surface waves dissipation processes such as whitecapping. - - - - Pa - - - The surface called "sea surface" means the upper boundary of the ocean. "Surface stress" means the shear stress (force per unit area) exerted at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, surface stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Northward" indicates a vector component which is positive when directed northward (negative southward). "Downward northward" indicates the ZY component of a tensor. A downward northward stress is a downward flux of northward momentum, which accelerates the lower medium northward and the upper medium southward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. The phrase "dissipation_of_sea_surface_waves" means the stress associated with sea surface waves dissipation processes such as whitecapping. - - K @@ -18500,13 +18367,6 @@ Wave slope describes an aspect of sea surface wave geometry related to sea surface roughness. Mean square slope describes a derivation over multiple waves within a sea-state, for example calculated from moments of the wave directional spectrum. The phrase "y_slope" indicates that slope values are derived from vector components along the grid y-axis. - - m-1 - - - The wave directional spectrum can be written as a five dimensional function S(t,x,y,k,theta) where t is time, x and y are horizontal coordinates (such as longitude and latitude), k is wavenumber and theta is direction. S has the standard name sea_surface_wave_directional_variance_spectral_density. S can be integrated over direction to give S1= integral(S dtheta) and this quantity has the standard name sea_surface_wave_variance_spectral_density. Wavenumber is the number of oscillations of a wave per unit distance. Wavenumber moments, M(n) of S1 can then be calculated as follows: M(n) = integral(S1 k^n dk), where k^n is k to the power of n. The mean wavenumber, k(1), is calculated as the ratio M(1)/M(0). - - s @@ -18591,27 +18451,6 @@ Sea surface wave variance spectral density is the variance of wave amplitude within a range of wave frequency. - - Pa - - - "Sea surface wave radiation stress" describes the excess momentum flux caused by sea surface waves. Radiation stresses behave as a second-order tensor. "xx" indicates the component of the tensor along the grid x_ axis. - - - - Pa - - - "Sea surface wave radiation stress" describes the excess momentum flux caused by sea surface waves. Radiation stresses behave as a second-order tensor. "xy" indicates the lateral contributions to x_ and y_ components of the tensor. - - - - Pa - - - "Sea surface wave radiation stress" describes the excess momentum flux caused by sea surface waves. Radiation stresses behave as a second-order tensor. "yy" indicates the component of the tensor along the grid y_ axis. - - degree @@ -19249,13 +19088,6 @@ "Content" indicates a quantity per unit area. The "soil content" of a quantity refers to the vertical integral from the surface down to the bottom of the soil model. For the content between specified levels in the soil, standard names including content_of_soil_layer are used. Soil carbon is returned to the atmosphere as the organic matter decays. The decay process takes varying amounts of time depending on the composition of the organic matter, the temperature and the availability of moisture. A carbon "soil pool" means the carbon contained in organic matter which has a characteristic period over which it decays and releases carbon into the atmosphere. "Slow soil pool" refers to the decay of organic matter in soil with a characteristic period of more than a hundred years under reference climate conditions of a temperature of 20 degrees Celsius and no water limitations. - - 1 - - - "Area fraction" is the fraction of a grid cell's horizontal area that has some characteristic of interest. It is evaluated as the area of interest divided by the grid cell area. It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. Snow "viewable from above" refers to the snow on objects or the ground as viewed from above, which excludes, for example, falling snow flakes and snow obscured by a canopy, vegetative cover, or other features resting on the surface. - - kg m-2 @@ -19393,7 +19225,7 @@ K 85 - Soil temperature is the bulk temperature of the soil, not the surface (skin) temperature. "Soil" means the near-surface layer where plants sink their roots. For subsurface temperatures that extend beneath the soil layer or in areas where there is no surface soil layer, the standard name temperature_in_ground should be used. + Soil temperature is the bulk temperature of the soil, not the surface (skin) temperature. "Soil" means the near-surface layer where plants sink their roots. For subsurface temperatures that extend beneath the soil layer or in areas where there is no surface soil layer, the standard name solid_earth_subsurface_temperature should be used. @@ -19417,13 +19249,6 @@ A variable with the standard name of soil_type contains strings which indicate the character of the soil e.g. clay. These strings have not yet been standardised. Alternatively, the data variable may contain integers which can be translated to strings using flag_values and flag_meanings attributes. - - 1 - - - soil_water_ph is the measure of acidity of soil moisture, defined as the negative logarithm of the concentration of dissolved hydrogen ions in soil water. - - degree @@ -19459,6 +19284,13 @@ Solar zenith angle is the the angle between the line of sight to the sun and the local vertical. + + K + + + The quantity with standard name solid_earth_subsurface_temperature is the temperature at any depth (or in a layer) of the "solid" earth, excluding surficial snow and ice (but not permafrost or soil). For temperatures in surface lying snow and ice, the more specific standard names temperature_in_surface_snow and land_ice_temperature should be used. For temperatures measured or modelled specifically in the soil layer (the near-surface layer where plants sink their roots) the standard name soil_temperature should be used. + + kg m-2 s-1 @@ -19578,13 +19410,6 @@ "specific" means per unit mass. Potential energy is the sum of the gravitational potential energy relative to the geoid and the centripetal potential energy. (The geopotential is the specific potential energy.) - - J kg-1 K-1 - - - Thermal capacity, or heat capacity, is the amount of heat energy required to increase the temperature of 1 kg of material by 1 K. It is a property of the material. - - J kg-1 K-1 @@ -19645,28 +19470,28 @@ day - Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". + Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". day - Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". + Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". day - "Amount" means mass per unit area. "Precipitation" in the earth's atmosphere means precipitation of water in all phases. The construction lwe_thickness_of_X_amount or _content means the vertical extent of a layer of liquid water having the same mass per unit area. The abbreviation "lwe" means liquid water equivalent. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". + "Amount" means mass per unit area. "Precipitation" in the earth's atmosphere means precipitation of water in all phases. The construction lwe_thickness_of_X_amount or _content means the vertical extent of a layer of liquid water having the same mass per unit area. The abbreviation "lwe" means liquid water equivalent. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". day - "Amount" means mass per unit area. "Precipitation" in the earth's atmosphere means precipitation of water in all phases.The construction lwe_thickness_of_X_amount or _content means the vertical extent of a layer of liquid water having the same mass per unit area. The abbreviation "lwe" means liquid water equivalent. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". + "Amount" means mass per unit area. "Precipitation" in the earth's atmosphere means precipitation of water in all phases.The construction lwe_thickness_of_X_amount or _content means the vertical extent of a layer of liquid water having the same mass per unit area. The abbreviation "lwe" means liquid water equivalent. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". @@ -19802,13 +19627,6 @@ "Sea surface height" is a time-varying quantity. The steric change in sea surface height is the change in height that a water column of standard temperature zero degrees Celsius and practical salinity S=35.0 would undergo when its temperature and salinity are changed to the observed values. The sum of the quantities with standard names thermosteric_change_in_sea_surface_height and halosteric_change_in_sea_surface_height is the total steric change in the water column height, which has the standard name of steric_change_in_sea_surface_height. The sum of the quantities with standard names sea_water_mass_per_unit_area_expressed_as_thickness and steric_change_in_sea_surface_height is the total thickness of the sea water column. - - m s-1 - - - Storm motion speed is defined as a two dimensional velocity vector, with no vertical component. (Vertical motion in the atmosphere has the standard name upward_air_velocity.) It is defined as the average speed of a supercell, and the direction the storm will move from. It is not dependent on the orientation of the ground-relative winds. Storm motion speed generally follows the methodology outlined in Bunkers et al. (2000). - - 1 @@ -20110,20 +19928,6 @@ The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted by the wind at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, wind stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Eastward" indicates a vector component which is positive when directed eastward (negative westward). "Downward eastward" indicates the ZX component of a tensor. A downward eastward stress is a downward flux of eastward momentum, which accelerates the lower medium eastward and the upper medium westward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Boundary layer mixing" means turbulent motions that transport heat, water, momentum and chemical constituents within the atmospheric boundary layer and affect exchanges between the surface and the atmosphere. The atmospheric boundary layer is typically characterised by a well-mixed sub-cloud layer of order 500 metres, and by a more extended conditionally unstable layer with boundary-layer clouds up to 2 km. (Reference: IPCC Third Assessment Report, Working Group 1: The Scientific Basis, 7.2.2.3, https://archive.ipcc.ch/ipccreports/tar/wg1/273.htm). - - Pa - - - The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted by the wind at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, wind stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Eastward" indicates a vector component which is positive when directed northward (negative southward). "Downward eastward" indicates the ZX component of a tensor. A downward eastward stress is a downward flux of eastward momentum, which accelerates the lower medium eastward and the upper medium westward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Viscosity" means the stress associated with viscous effects at the sea surface and is equivalent to the turbulent stress just outside the viscous sublayer. - - - - Pa - - - The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted by the wind at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, wind stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Eastward" indicates a vector component which is positive when directed northward (negative southward). "Downward eastward" indicates the ZX component of a tensor. A downward eastward stress is a downward flux of eastward momentum, which accelerates the lower medium eastward and the upper medium westward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Sea surface waves" means the stress associated with form drag over sea surface waves. - - W m-2 @@ -20264,20 +20068,6 @@ The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted by the wind at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, wind stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Northward" indicates a vector component which is positive when directed northward (negative southward). "Downward northward" indicates the ZY component of a tensor. A downward northward stress is a downward flux of northward momentum, which accelerates the lower medium northward and the upper medium southward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Boundary layer mixing" means turbulent motions that transport heat, water, momentum and chemical constituents within the atmospheric boundary layer and affect exchanges between the surface and the atmosphere. The atmospheric boundary layer is typically characterised by a well-mixed sub-cloud layer of order 500 metres, and by a more extended conditionally unstable layer with boundary-layer clouds up to 2 km. (Reference: IPCC Third Assessment Report, Working Group 1: The Scientific Basis, 7.2.2.3, https://archive.ipcc.ch/ipccreports/tar/wg1/273.htm). - - Pa - - - The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted by the wind at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, wind stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Northward" indicates a vector component which is positive when directed northward (negative southward). "Downward northward" indicates the ZY component of a tensor. A downward northward stress is a downward flux of northward momentum, which accelerates the lower medium northward and the upper medium southward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Viscosity" means the stress associated with viscous effects at the sea surface and is equivalent to the turbulent stress just outside the viscous sublayer. - - - - Pa - - - The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted by the wind at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, wind stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Northward" indicates a vector component which is positive when directed northward (negative southward). "Downward northward" indicates the ZY component of a tensor. A downward northward stress is a downward flux of northward momentum, which accelerates the lower medium northward and the upper medium southward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Sea surface waves" means the stress associated with form drag over sea surface waves. - - W m-2 @@ -23533,13 +23323,6 @@ The surface called "surface" means the lower boundary of the atmosphere. "anomaly" means difference from climatology. The surface temperature is the (skin) temperature at the interface, not the bulk temperature of the medium above or below. - - Pa - - - The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted at the surface. An upward stress is an upward flux of momentum into the atmosphere. "Upward" indicates a vector component which is positive when directed upward (negative downward). "Eastward" indicates a vector component which is positive when directed northward (negative southward). "Upward eastward" indicates the ZX component of a tensor. An upward eastward stress is an upward flux of eastward momentum, which accelerates the upper medium eastward and the lower medium westward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Sea surface waves" means the stress associated with oscillatory motions of a wavy sea surface. - - W m-2 @@ -23813,13 +23596,6 @@ The surface called "surface" means the lower boundary of the atmosphere. "Upward" indicates a vector component which is positive when directed upward (negative downward). In accordance with common usage in geophysical disciplines, "flux" implies per unit area, called "flux density" in physics. The chemical formula for dimethyl sulfide is (CH3)2S. Dimethyl sulfide is sometimes referred to as DMS. - - Pa - - - The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted at the surface. An upward stress is an upward flux of momentum into the atmosphere. "Upward" indicates a vector component which is positive when directed upward (negative downward). "Northward" indicates a vector component which is positive when directed northward (negative southward). "Upward northward" indicates the ZY component of a tensor. An upward northward stress is an upward flux of northward momentum, which accelerates the upper medium northward and the lower medium southward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Sea surface waves" means the stress associated with oscillatory motions of a wavy sea surface. - - W m-2 122 E146 @@ -24023,13 +23799,6 @@ The quantity with standard name temperature_flux_due_to_runoff_expressed_as_heat_flux_into_sea_water is the heat carried by the transfer of water into the liquid ocean by the process of runoff. This quantity additionally includes melt water from sea ice and icebergs. It is calculated relative to the heat that would be transported by runoff water entering the sea at zero degrees Celsius. It is calculated as the product QrunoffCpTrunoff, where Q runoff is the mass flux of liquid runoff entering the sea water (kg m-2 s-1), Cp is the specific heat capacity of water, and Trunoff is the temperature in degrees Celsius of the runoff water. In accordance with common usage in geophysical disciplines, "flux" implies per unit area, called "flux density" in physics. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. Runoff is the liquid water which drains from land. If not specified, "runoff" refers to the sum of surface runoff and subsurface drainage. - - K - - - The temperature at any given depth (or in a layer) below the surface of the ground, excluding surficial snow and ice (but not permafrost or soil). For temperatures in surface lying snow and ice, the more specific standard names temperature_in_surface_snow and land_ice_temperature should be used. For temperatures measured or modelled specifically for the soil layer (the near-surface layer where plants sink their roots) the standard name soil_temperature should be used. - - K E238 @@ -28272,13 +28041,6 @@ The specification of a physical process by the phrase due_to_process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "tendency_of_X" means derivative of X with respect to time. "Content" indicates a quantity per unit area. "Layer" means any layer with upper and lower boundaries that have constant values in some vertical coordinate. There must be a vertical coordinate variable indicating the extent of the layer(s). If the layers are model layers, the vertical coordinate can be model_level_number, but it is recommended to specify a physical coordinate (in a scalar or auxiliary coordinate variable) as well. - - kg s-1 - - - The phrase "tendency_of_X" means derivative of X with respect to time. "Land ice" means glaciers, ice-caps and ice-sheets resting on bedrock and also includes ice-shelves. The horizontal domain over which the quantity is calculated is described by the associated coordinate variables and coordinate bounds or by a coordinate variable or scalar coordinate variable with the standard name of "region" supplied according to section 6.1.1 of the CF conventions. - - kg s-1 @@ -29861,13 +29623,6 @@ The specification of a physical process by the phrase due_to_process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "tendency_of_X" means derivative of X with respect to time. Speed is the magnitude of velocity. Wind is defined as a two-dimensional (horizontal) air velocity vector, with no vertical component. (Vertical motion in the atmosphere has the standard name upward_air_velocity.) The wind speed is the magnitude of the wind velocity. - - W m-1 K-1 - - - Thermal conductivity is the constant k in the formula q = -k grad T where q is the heat transfer per unit time per unit area of a surface normal to the direction of transfer and grad T is the temperature gradient. Thermal conductivity is a property of the material. - - J m-2 @@ -31472,108 +31227,12 @@ - - biological_taxon_lsid - - - - temperature_in_ground - - - - surface_snow_density - - - - soot_content_of_surface_snow - - - - liquid_water_content_of_surface_snow - - - - surface_snow_thickness - - - - thermal_energy_content_of_surface_snow - - - - temperature_in_surface_snow - - - - integral_wrt_time_of_surface_downward_eastward_stress - - integral_wrt_time_of_surface_downward_northward_stress - - tendency_of_atmosphere_mass_content_of_water_vapor_due_to_sublimation_of_surface_snow_and_ice - - - - atmosphere_upward_absolute_vorticity - - - - atmosphere_upward_relative_vorticity - - - - area_type - - - - area_type - - - - iron_growth_limitation_of_diazotrophic_phytoplankton - - - - growth_limitation_of_diazotrophic_phytoplankton_due_to_solar_irradiance - - - - tendency_of_mole_concentration_of_particulate_organic_matter_expressed_as_carbon_in_sea_water_due_to_net_primary_production_by_diazotrophic_phytoplankton - - - - mole_concentration_of_diazotrophic_phytoplankton_expressed_as_carbon_in_sea_water - - - - mass_fraction_of_liquid_precipitation_in_air - - - - mass_fraction_of_liquid_precipitation_in_air - - - - mass_concentration_of_diazotrophic_phytoplankton_expressed_as_chlorophyll_in_sea_water - - - - air_pseudo_equivalent_potential_temperature - - - - tendency_of_mass_fraction_of_stratiform_cloud_ice_in_air_due_to_melting_to_cloud_liquid_water - - - - tendency_of_mass_fraction_of_stratiform_cloud_ice_in_air_due_to_heterogeneous_nucleation_from_cloud_liquid_water - - - - tendency_of_mass_fraction_of_stratiform_cloud_ice_in_air_due_to_riming_from_cloud_liquid_water + + integral_wrt_time_of_surface_downward_eastward_stress @@ -31604,6 +31263,42 @@ effective_radius_of_cloud_liquid_water_particles_at_liquid_water_cloud_top + + mass_content_of_cloud_liquid_water_in_atmosphere_layer + + + + air_equivalent_potential_temperature + + + + number_concentration_of_stratiform_cloud_liquid_water_particles_at_stratiform_liquid_water_cloud_top + + + + number_concentration_of_convective_cloud_liquid_water_particles_at_convective_liquid_water_cloud_top + + + + wave_frequency + + + + upward_eastward_momentum_flux_in_air_due_to_nonorographic_eastward_gravity_waves + + + + tendency_of_troposphere_moles_of_carbon_monoxide + + + + tendency_of_atmosphere_moles_of_sulfate_dry_aerosol_particles + + + + tendency_of_atmosphere_mass_content_of_nitrate_dry_aerosol_particles_due_to_dry_deposition + + northward_heat_flux_in_air_due_to_eddy_advection @@ -31660,56 +31355,72 @@ atmosphere_mass_content_of_cloud_liquid_water - - mass_fraction_of_sulfate_dry_aerosol_particles_in_air + + mass_concentration_of_coarse_mode_ambient_aerosol_particles_in_air - - mass_fraction_of_nitric_acid_trihydrate_ambient_aerosol_particles_in_air + + sea_water_velocity_to_direction - - mass_fraction_of_ammonium_dry_aerosol_particles_in_air + + sea_water_velocity_to_direction - - tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_shallow_convection + + gross_primary_productivity_of_biomass_expressed_as_carbon - - tendency_of_mass_content_of_water_vapor_in_atmosphere_layer + + eastward_water_vapor_flux_in_air - - mass_content_of_cloud_ice_in_atmosphere_layer + + sea_water_velocity_from_direction - - mass_concentration_of_secondary_particulate_organic_matter_dry_aerosol_particles_in_air + + thickness_of_stratiform_snowfall_amount - - mass_concentration_of_mercury_dry_aerosol_particles_in_air + + optical_thickness_of_atmosphere_layer_due_to_ambient_aerosol_particles - - mass_concentration_of_coarse_mode_ambient_aerosol_particles_in_air + + optical_thickness_of_atmosphere_layer_due_to_ambient_aerosol_particles - - sea_water_velocity_to_direction + + lwe_thickness_of_stratiform_snowfall_amount - - sea_water_velocity_to_direction + + equivalent_thickness_at_stp_of_atmosphere_ozone_content - - gross_primary_productivity_of_biomass_expressed_as_carbon + + atmosphere_optical_thickness_due_to_water_in_ambient_aerosol_particles - - eastward_water_vapor_flux_in_air + + atmosphere_optical_thickness_due_to_dust_dry_aerosol_particles + + + + atmosphere_optical_thickness_due_to_dust_ambient_aerosol_particles + + + + atmosphere_optical_thickness_due_to_ambient_aerosol_particles + + + + atmosphere_optical_thickness_due_to_ambient_aerosol_particles + + + + atmosphere_net_upward_convective_mass_flux @@ -31724,6 +31435,94 @@ tendency_of_atmosphere_mass_content_of_water_vapor_due_to_advection + + thermal_energy_content_of_surface_snow + + + + liquid_water_content_of_surface_snow + + + + temperature_in_surface_snow + + + + tendency_of_atmosphere_mass_content_of_water_vapor_due_to_sublimation_of_surface_snow_and_ice + + + + surface_snow_thickness + + + + surface_snow_density + + + + soot_content_of_surface_snow + + + + atmosphere_upward_absolute_vorticity + + + + atmosphere_upward_relative_vorticity + + + + area_type + + + + area_type + + + + iron_growth_limitation_of_diazotrophic_phytoplankton + + + + growth_limitation_of_diazotrophic_phytoplankton_due_to_solar_irradiance + + + + tendency_of_mole_concentration_of_particulate_organic_matter_expressed_as_carbon_in_sea_water_due_to_net_primary_production_by_diazotrophic_phytoplankton + + + + mole_concentration_of_diazotrophic_phytoplankton_expressed_as_carbon_in_sea_water + + + + mass_fraction_of_liquid_precipitation_in_air + + + + mass_fraction_of_liquid_precipitation_in_air + + + + mass_concentration_of_diazotrophic_phytoplankton_expressed_as_chlorophyll_in_sea_water + + + + air_pseudo_equivalent_potential_temperature + + + + tendency_of_mass_fraction_of_stratiform_cloud_ice_in_air_due_to_melting_to_cloud_liquid_water + + + + tendency_of_mass_fraction_of_stratiform_cloud_ice_in_air_due_to_heterogeneous_nucleation_from_cloud_liquid_water + + + + tendency_of_mass_fraction_of_stratiform_cloud_ice_in_air_due_to_riming_from_cloud_liquid_water + + tendency_of_atmosphere_mass_content_of_water_vapor @@ -31812,68 +31611,256 @@ atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles - - mass_content_of_cloud_liquid_water_in_atmosphere_layer + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_waste_treatment_and_disposal - - air_equivalent_potential_temperature + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_savanna_and_grassland_fires - - number_concentration_of_stratiform_cloud_liquid_water_particles_at_stratiform_liquid_water_cloud_top + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_maritime_transport - - number_concentration_of_convective_cloud_liquid_water_particles_at_convective_liquid_water_cloud_top + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_land_transport - - wave_frequency + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_forest_fires - - upward_eastward_momentum_flux_in_air_due_to_nonorographic_eastward_gravity_waves + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_agricultural_waste_burning - - tendency_of_troposphere_moles_of_carbon_monoxide + + tendency_of_atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles_due_to_wet_deposition - - tendency_of_atmosphere_moles_of_sulfate_dry_aerosol_particles + + moles_of_cfc11_per_unit_mass_in_sea_water - - tendency_of_atmosphere_mass_content_of_nitrate_dry_aerosol_particles_due_to_dry_deposition + + atmosphere_moles_of_cfc11 - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_waste_treatment_and_disposal + + tendency_of_atmosphere_moles_of_cfc113 - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_savanna_and_grassland_fires + + atmosphere_moles_of_cfc113 - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_maritime_transport + + tendency_of_atmosphere_moles_of_cfc114 - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_land_transport + + atmosphere_moles_of_cfc114 - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_forest_fires + + tendency_of_atmosphere_moles_of_cfc115 - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_agricultural_waste_burning + + atmosphere_moles_of_cfc115 - - tendency_of_atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles_due_to_wet_deposition + + tendency_of_atmosphere_moles_of_cfc12 + + + + atmosphere_moles_of_cfc12 + + + + tendency_of_atmosphere_moles_of_halon1202 + + + + atmosphere_moles_of_halon1202 + + + + tendency_of_atmosphere_moles_of_halon1211 + + + + atmosphere_moles_of_halon1211 + + + + tendency_of_atmosphere_moles_of_halon1301 + + + + atmosphere_moles_of_halon1301 + + + + tendency_of_atmosphere_moles_of_halon2402 + + + + atmosphere_moles_of_halon2402 + + + + tendency_of_atmosphere_moles_of_hcc140a + + + + effective_radius_of_convective_cloud_rain_particles + + + + tendency_of_troposphere_moles_of_hcc140a + + + + tendency_of_middle_atmosphere_moles_of_hcc140a + + + + tendency_of_troposphere_moles_of_hcfc22 + + + + tendency_of_atmosphere_moles_of_hcfc22 + + + + atmosphere_moles_of_hcfc22 + + + + tendency_of_atmosphere_number_content_of_aerosol_particles_due_to_turbulent_deposition + + + + lagrangian_tendency_of_atmosphere_sigma_coordinate + + + + lagrangian_tendency_of_atmosphere_sigma_coordinate + + + + diameter_of_ambient_aerosol_particles + + + + effective_radius_of_stratiform_cloud_ice_particles + + + + effective_radius_of_convective_cloud_ice_particles + + + + effective_radius_of_stratiform_cloud_graupel_particles + + + + effective_radius_of_stratiform_cloud_rain_particles + + + + effective_radius_of_convective_cloud_snow_particles + + + + mass_fraction_of_sulfate_dry_aerosol_particles_in_air + + + + mass_fraction_of_nitric_acid_trihydrate_ambient_aerosol_particles_in_air + + + + mass_fraction_of_ammonium_dry_aerosol_particles_in_air + + + + tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_shallow_convection + + + + tendency_of_mass_content_of_water_vapor_in_atmosphere_layer + + + + mass_content_of_cloud_ice_in_atmosphere_layer + + + + mass_concentration_of_secondary_particulate_organic_matter_dry_aerosol_particles_in_air + + + + mass_concentration_of_mercury_dry_aerosol_particles_in_air + + + + product_of_eastward_wind_and_lagrangian_tendency_of_air_pressure + + + + carbon_mass_flux_into_litter_and_soil_due_to_anthropogenic_land_use_or_land_cover_change + + + + stratiform_cloud_area_fraction + + + + mass_fraction_of_mercury_dry_aerosol_particles_in_air + + + + atmosphere_moles_of_hcc140a + + + + floating_ice_shelf_area_fraction + + + + atmosphere_moles_of_carbon_tetrachloride + + + + net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_miscellaneous_phytoplankton + + + + mole_fraction_of_inorganic_bromine_in_air + + + + water_vapor_saturation_deficit_in_air + + + + tendency_of_atmosphere_mass_content_of_elemental_carbon_dry_aerosol_particles_due_to_emission_from_agricultural_waste_burning + + + + tendency_of_atmosphere_moles_of_carbon_tetrachloride + + + + tendency_of_atmosphere_moles_of_carbon_monoxide + + + + tendency_of_atmosphere_mass_content_of_nitrogen_compounds_expressed_as_nitrogen_due_to_wet_deposition @@ -32028,152 +32015,104 @@ tendency_of_atmosphere_moles_of_cfc11 - - moles_of_cfc11_per_unit_mass_in_sea_water + + mole_concentration_of_phytoplankton_expressed_as_nitrogen_in_sea_water - - atmosphere_moles_of_cfc11 + + net_primary_mole_productivity_of_biomass_expressed_as_carbon_due_to_nitrate_utilization - - tendency_of_atmosphere_moles_of_hcc140a + + net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_picophytoplankton - - effective_radius_of_convective_cloud_rain_particles + + net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_phytoplankton - - tendency_of_troposphere_moles_of_hcc140a + + net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_diatoms - - tendency_of_middle_atmosphere_moles_of_hcc140a + + net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_calcareous_phytoplankton - - tendency_of_troposphere_moles_of_hcfc22 + + mole_concentration_of_diatoms_expressed_as_nitrogen_in_sea_water - - tendency_of_atmosphere_moles_of_hcfc22 + + tendency_of_mole_concentration_of_dissolved_inorganic_phosphorus_in_sea_water_due_to_biological_processes - - atmosphere_moles_of_hcfc22 + + tendency_of_mole_concentration_of_dissolved_inorganic_silicon_in_sea_water_due_to_biological_processes - - tendency_of_atmosphere_number_content_of_aerosol_particles_due_to_turbulent_deposition + + tendency_of_atmosphere_mole_concentration_of_carbon_monoxide_due_to_chemical_destruction - - lagrangian_tendency_of_atmosphere_sigma_coordinate + + volume_extinction_coefficient_in_air_due_to_ambient_aerosol_particles - - lagrangian_tendency_of_atmosphere_sigma_coordinate + + platform_name - - diameter_of_ambient_aerosol_particles + + platform_id - - effective_radius_of_stratiform_cloud_ice_particles + + platform_pitch - - effective_radius_of_convective_cloud_ice_particles + + tendency_of_specific_humidity_due_to_stratiform_precipitation - - effective_radius_of_stratiform_cloud_graupel_particles + + tendency_of_air_temperature_due_to_stratiform_precipitation - - effective_radius_of_stratiform_cloud_rain_particles + + water_evaporation_amount_from_canopy - - effective_radius_of_convective_cloud_snow_particles - - - - product_of_eastward_wind_and_lagrangian_tendency_of_air_pressure - - - - carbon_mass_flux_into_litter_and_soil_due_to_anthropogenic_land_use_or_land_cover_change - - - - stratiform_cloud_area_fraction - - - - sea_water_velocity_from_direction - - - - thickness_of_stratiform_snowfall_amount - - - - optical_thickness_of_atmosphere_layer_due_to_ambient_aerosol_particles - - - - optical_thickness_of_atmosphere_layer_due_to_ambient_aerosol_particles - - - - lwe_thickness_of_stratiform_snowfall_amount - - - - equivalent_thickness_at_stp_of_atmosphere_ozone_content - - - - atmosphere_optical_thickness_due_to_water_in_ambient_aerosol_particles - - - - atmosphere_optical_thickness_due_to_dust_dry_aerosol_particles - - - - atmosphere_optical_thickness_due_to_dust_ambient_aerosol_particles + + tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_turbulent_deposition - - atmosphere_optical_thickness_due_to_ambient_aerosol_particles + + tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_gravitational_settling - - atmosphere_optical_thickness_due_to_ambient_aerosol_particles + + tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_emission - - atmosphere_net_upward_convective_mass_flux + + atmosphere_mass_content_of_cloud_ice - - mass_fraction_of_mercury_dry_aerosol_particles_in_air + + stratiform_precipitation_amount - - atmosphere_moles_of_hcc140a + + tendency_of_atmosphere_moles_of_nitrous_oxide - - floating_ice_shelf_area_fraction + + tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_dry_deposition - - atmosphere_moles_of_carbon_tetrachloride + + atmosphere_mass_content_of_convective_cloud_condensed_water @@ -32188,144 +32127,12 @@ mole_fraction_of_noy_expressed_as_nitrogen_in_air - - net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_miscellaneous_phytoplankton - - - - mole_fraction_of_inorganic_bromine_in_air - - - - water_vapor_saturation_deficit_in_air - - - - tendency_of_atmosphere_mass_content_of_elemental_carbon_dry_aerosol_particles_due_to_emission_from_agricultural_waste_burning - - - - tendency_of_atmosphere_moles_of_carbon_tetrachloride - - - - tendency_of_atmosphere_moles_of_carbon_monoxide - - - - tendency_of_atmosphere_moles_of_cfc113 - - - - atmosphere_moles_of_cfc113 - - - - tendency_of_atmosphere_moles_of_cfc114 - - - - atmosphere_moles_of_cfc114 - - - - tendency_of_atmosphere_moles_of_cfc115 - - - - atmosphere_moles_of_cfc115 - - - - tendency_of_atmosphere_moles_of_cfc12 - - - - atmosphere_moles_of_cfc12 - - - - tendency_of_atmosphere_moles_of_halon1202 - - - - atmosphere_moles_of_halon1202 - - - - tendency_of_atmosphere_moles_of_halon1211 - - - - atmosphere_moles_of_halon1211 - - - - tendency_of_atmosphere_moles_of_halon1301 - - - - atmosphere_moles_of_halon1301 - - - - tendency_of_atmosphere_moles_of_halon2402 - - - - atmosphere_moles_of_halon2402 - - - - tendency_of_atmosphere_mass_content_of_nitrogen_compounds_expressed_as_nitrogen_due_to_wet_deposition - - - - mole_concentration_of_phytoplankton_expressed_as_nitrogen_in_sea_water - - - - net_primary_mole_productivity_of_biomass_expressed_as_carbon_due_to_nitrate_utilization - - - - net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_picophytoplankton - - - - net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_phytoplankton - - - - net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_diatoms - - - - net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_calcareous_phytoplankton - - - - mole_concentration_of_diatoms_expressed_as_nitrogen_in_sea_water - - - - tendency_of_mole_concentration_of_dissolved_inorganic_phosphorus_in_sea_water_due_to_biological_processes - - - - tendency_of_mole_concentration_of_dissolved_inorganic_silicon_in_sea_water_due_to_biological_processes - - - - tendency_of_atmosphere_mole_concentration_of_carbon_monoxide_due_to_chemical_destruction - - - - volume_extinction_coefficient_in_air_due_to_ambient_aerosol_particles + + tendency_of_atmosphere_moles_of_methane - - atmosphere_mass_content_of_convective_cloud_condensed_water + + rate_of_hydroxyl_radical_destruction_due_to_reaction_with_nmvoc @@ -32400,78 +32207,6 @@ lwe_thickness_of_stratiform_precipitation_amount - - tendency_of_atmosphere_moles_of_methane - - - - rate_of_hydroxyl_radical_destruction_due_to_reaction_with_nmvoc - - - - magnitude_of_sea_ice_displacement - - - - surface_downwelling_radiative_flux_per_unit_wavelength_in_sea_water - - - - surface_downwelling_radiative_flux_per_unit_wavelength_in_air - - - - surface_downwelling_shortwave_flux_in_air_assuming_clear_sky_and_no_aerosol - - - - surface_downwelling_photon_spherical_irradiance_per_unit_wavelength_in_sea_water - - - - surface_downwelling_photon_flux_per_unit_wavelength_in_sea_water - - - - surface_downwelling_longwave_flux_in_air - - - - integral_wrt_time_of_surface_downwelling_shortwave_flux_in_air - - - - integral_wrt_time_of_surface_downwelling_longwave_flux_in_air - - - - downwelling_spherical_irradiance_per_unit_wavelength_in_sea_water - - - - downwelling_radiative_flux_per_unit_wavelength_in_sea_water - - - - downwelling_radiative_flux_per_unit_wavelength_in_air - - - - downwelling_shortwave_flux_in_air_assuming_clear_sky_and_no_aerosol - - - - downwelling_photon_spherical_irradiance_per_unit_wavelength_in_sea_water - - - - downwelling_radiance_per_unit_wavelength_in_air - - - - downwelling_photon_radiance_per_unit_wavelength_in_sea_water - - surface_upwelling_shortwave_flux_in_air_assuming_clear_sky @@ -32528,6 +32263,26 @@ surface_upwelling_radiance_per_unit_wavelength_in_air_reflected_by_sea_water + + surface_water_evaporation_flux + + + + water_evapotranspiration_flux + + + + water_volume_transport_into_sea_water_from_rivers + + + + stratiform_graupel_flux + + + + toa_outgoing_shortwave_flux_assuming_clear_sky_and_no_aerosol + + wood_debris_mass_content_of_carbon @@ -32556,6 +32311,18 @@ volume_scattering_coefficient_of_radiative_flux_in_air_due_to_ambient_aerosol_particles + + platform_yaw + + + + platform_roll + + + + water_vapor_partial_pressure_in_air + + volume_scattering_coefficient_of_radiative_flux_in_air_due_to_dried_aerosol_particles @@ -32572,68 +32339,68 @@ integral_wrt_height_of_product_of_eastward_wind_and_specific_humidity - - platform_yaw + + magnitude_of_sea_ice_displacement - - platform_roll + + surface_downwelling_radiative_flux_per_unit_wavelength_in_sea_water - - water_vapor_partial_pressure_in_air + + surface_downwelling_radiative_flux_per_unit_wavelength_in_air - - platform_name + + surface_downwelling_shortwave_flux_in_air_assuming_clear_sky_and_no_aerosol - - platform_id + + surface_downwelling_photon_spherical_irradiance_per_unit_wavelength_in_sea_water - - platform_pitch + + surface_downwelling_photon_flux_per_unit_wavelength_in_sea_water - - tendency_of_specific_humidity_due_to_stratiform_precipitation + + surface_downwelling_longwave_flux_in_air - - tendency_of_air_temperature_due_to_stratiform_precipitation + + integral_wrt_time_of_surface_downwelling_shortwave_flux_in_air - - water_evaporation_amount_from_canopy + + integral_wrt_time_of_surface_downwelling_longwave_flux_in_air - - tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_turbulent_deposition + + downwelling_spherical_irradiance_per_unit_wavelength_in_sea_water - - tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_gravitational_settling + + downwelling_radiative_flux_per_unit_wavelength_in_sea_water - - tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_emission + + downwelling_radiative_flux_per_unit_wavelength_in_air - - atmosphere_mass_content_of_cloud_ice + + downwelling_shortwave_flux_in_air_assuming_clear_sky_and_no_aerosol - - stratiform_precipitation_amount + + downwelling_photon_spherical_irradiance_per_unit_wavelength_in_sea_water - - tendency_of_atmosphere_moles_of_nitrous_oxide + + downwelling_radiance_per_unit_wavelength_in_air - - tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_dry_deposition + + downwelling_photon_radiance_per_unit_wavelength_in_sea_water @@ -32740,26 +32507,6 @@ lwe_stratiform_precipitation_rate - - surface_water_evaporation_flux - - - - water_evapotranspiration_flux - - - - water_volume_transport_into_sea_water_from_rivers - - - - stratiform_graupel_flux - - - - toa_outgoing_shortwave_flux_assuming_clear_sky_and_no_aerosol - - ocean_y_overturning_mass_streamfunction_due_to_parameterized_eddy_advection @@ -32788,94 +32535,6 @@ tendency_of_sea_water_salinity_due_to_parameterized_eddy_advection - - integral_wrt_time_of_surface_net_downward_shortwave_flux - - - - tendency_of_ocean_eddy_kinetic_energy_content_due_to_parameterized_eddy_advection - - - - sea_water_y_velocity_due_to_parameterized_mesoscale_eddies - - - - ocean_tracer_biharmonic_diffusivity_due_to_parameterized_mesoscale_eddy_advection - - - - eastward_sea_water_velocity_due_to_parameterized_mesoscale_eddies - - - - northward_sea_water_velocity_due_to_parameterized_mesoscale_eddies - - - - ocean_heat_y_transport_due_to_parameterized_eddy_advection - - - - ocean_meridional_overturning_mass_streamfunction_due_to_parameterized_eddy_advection - - - - ocean_mass_y_transport_due_to_advection_and_parameterized_eddy_advection - - - - ocean_mass_x_transport_due_to_advection_and_parameterized_eddy_advection - - - - ocean_heat_x_transport_due_to_parameterized_eddy_advection - - - - northward_ocean_freshwater_transport_due_to_parameterized_eddy_advection - - - - northward_ocean_salt_transport_due_to_parameterized_eddy_advection - - - - integral_wrt_time_of_toa_outgoing_longwave_flux - - - - integral_wrt_time_of_toa_net_downward_shortwave_flux - - - - integral_wrt_time_of_surface_net_downward_longwave_flux - - - - integral_wrt_time_of_surface_downward_sensible_heat_flux - - - - integral_wrt_time_of_surface_downward_latent_heat_flux - - - - integral_wrt_time_of_air_temperature_excess - - - - integral_wrt_time_of_air_temperature_deficit - - - - tendency_of_atmosphere_mass_content_of_ammonium_dry_aerosol_particles_due_to_wet_deposition - - - - tendency_of_atmosphere_mass_content_of_ammonium_dry_aerosol_particles_due_to_dry_deposition - - atmosphere_absorption_optical_thickness_due_to_sulfate_ambient_aerosol_particles @@ -33052,392 +32711,392 @@ surface_geostrophic_sea_water_x_velocity_assuming_mean_sea_level_for_geoid - - air_pressure_at_mean_sea_level + + tendency_of_atmosphere_mass_content_of_pm2p5_sea_salt_dry_aerosol_particles_due_to_emission - - sea_floor_depth_below_mean_sea_level + + tendency_of_atmosphere_mass_content_of_pm2p5_sea_salt_dry_aerosol_particles_due_to_dry_deposition - - ocean_mixed_layer_thickness_defined_by_vertical_tracer_diffusivity_deficit + + tendency_of_sea_surface_height_above_mean_sea_level - - sea_surface_wind_wave_mean_period + + mass_fraction_of_pm10_ambient_aerosol_particles_in_air - - sea_surface_wave_mean_period + + mass_fraction_of_pm10_ambient_aerosol_particles_in_air - - sea_surface_swell_wave_mean_period + + mass_concentration_of_pm10_ambient_aerosol_particles_in_air - - sea_surface_wind_wave_to_direction + + atmosphere_optical_thickness_due_to_pm10_ambient_aerosol_particles - - sea_surface_swell_wave_to_direction + + surface_geostrophic_eastward_sea_water_velocity - - mass_content_of_water_in_soil_layer + + mass_fraction_of_pm2p5_ambient_aerosol_particles_in_air - - mass_content_of_water_in_soil + + mass_fraction_of_pm2p5_ambient_aerosol_particles_in_air - - sea_surface_wind_wave_significant_height + + mass_concentration_of_pm2p5_ambient_aerosol_particles_in_air - - sea_surface_swell_wave_significant_height + + atmosphere_optical_thickness_due_to_pm2p5_ambient_aerosol_particles - - tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_expressed_as_sulfur_due_to_turbulent_deposition + + mass_fraction_of_pm1_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_expressed_as_sulfur_due_to_turbulent_deposition + + mass_fraction_of_pm1_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_due_to_emission + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_wet_deposition - - atmosphere_optical_thickness_due_to_particulate_organic_matter_ambient_aerosol_particles + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_wet_deposition - - mass_concentration_of_nitric_acid_trihydrate_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_gravitational_settling - - atmosphere_mass_content_of_water_in_ambient_aerosol_particles + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_gravitational_settling - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_residential_and_commercial_combustion + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_turbulent_deposition - - tendency_of_atmosphere_mass_content_of_mercury_dry_aerosol_particles_due_to_wet_deposition + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_turbulent_deposition - - tendency_of_atmosphere_mass_content_of_mercury_dry_aerosol_particles_due_to_dry_deposition + + mass_concentration_of_pm1_ambient_aerosol_particles_in_air - - mass_fraction_of_nitrate_dry_aerosol_particles_in_air + + atmosphere_optical_thickness_due_to_pm1_ambient_aerosol_particles - - mass_concentration_of_sulfate_dry_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_dry_deposition - - mass_fraction_of_water_in_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_dry_deposition - - mass_fraction_of_secondary_particulate_organic_matter_dry_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_pm2p5_sea_salt_dry_aerosol_particles_due_to_wet_deposition - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_industrial_processes_and_combustion + + tendency_of_atmosphere_mass_content_of_pm10_sea_salt_dry_aerosol_particles_due_to_wet_deposition - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_energy_production_and_distribution + + tendency_of_atmosphere_mass_content_of_pm10_sea_salt_dry_aerosol_particles_due_to_dry_deposition - - mass_concentration_of_sulfate_ambient_aerosol_particles_in_air + + mass_fraction_of_sea_salt_dry_aerosol_particles_in_air - - mass_concentration_of_sulfate_ambient_aerosol_particles_in_air + + mass_fraction_of_sea_salt_dry_aerosol_particles_in_air - - mass_concentration_of_dust_dry_aerosol_particles_in_air + + mass_concentration_of_sea_salt_dry_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles_due_to_emission + + mass_concentration_of_sea_salt_dry_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles_due_to_dry_deposition + + atmosphere_optical_thickness_due_to_sea_salt_ambient_aerosol_particles - - mass_fraction_of_primary_particulate_organic_matter_dry_aerosol_particles_in_air + + atmosphere_optical_thickness_due_to_sea_salt_ambient_aerosol_particles - - mass_fraction_of_particulate_organic_matter_dry_aerosol_particles_in_air + + atmosphere_mass_content_of_sea_salt_dry_aerosol_particles - - number_concentration_of_coarse_mode_ambient_aerosol_particles_in_air + + atmosphere_mass_content_of_sea_salt_dry_aerosol_particles - - sea_surface_wave_significant_height + + air_pressure_at_mean_sea_level - - tendency_of_atmosphere_moles_of_nitric_acid_trihydrate_ambient_aerosol_particles + + sea_floor_depth_below_mean_sea_level - - tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_due_to_dry_deposition + + ocean_mixed_layer_thickness_defined_by_vertical_tracer_diffusivity_deficit - - tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_wet_deposition + + sea_surface_wind_wave_mean_period - - number_concentration_of_nucleation_mode_ambient_aerosol_particles_in_air + + sea_surface_wave_mean_period - - number_concentration_of_ambient_aerosol_particles_in_air + + sea_surface_swell_wave_mean_period - - mole_fraction_of_nitric_acid_trihydrate_ambient_aerosol_particles_in_air + + sea_surface_wind_wave_to_direction - - mass_fraction_of_dust_dry_aerosol_particles_in_air + + sea_surface_swell_wave_to_direction - - mass_concentration_of_water_in_ambient_aerosol_particles_in_air + + mass_content_of_water_in_soil_layer - - mass_concentration_of_nitrate_dry_aerosol_particles_in_air + + mass_content_of_water_in_soil - - mass_concentration_of_particulate_organic_matter_dry_aerosol_particles_in_air + + sea_surface_wind_wave_significant_height - - mass_concentration_of_ammonium_dry_aerosol_particles_in_air + + sea_surface_swell_wave_significant_height - - atmosphere_mass_content_of_sulfate_ambient_aerosol_particles + + integral_wrt_time_of_surface_net_downward_shortwave_flux - - atmosphere_mass_content_of_sulfate_ambient_aerosol_particles + + tendency_of_ocean_eddy_kinetic_energy_content_due_to_parameterized_eddy_advection - - atmosphere_mass_content_of_dust_dry_aerosol_particles + + sea_water_y_velocity_due_to_parameterized_mesoscale_eddies - - atmosphere_absorption_optical_thickness_due_to_ambient_aerosol_particles + + ocean_tracer_biharmonic_diffusivity_due_to_parameterized_mesoscale_eddy_advection - - atmosphere_mass_content_of_sulfate_dry_aerosol_particles + + eastward_sea_water_velocity_due_to_parameterized_mesoscale_eddies - - tendency_of_atmosphere_mass_content_of_water_vapor_due_to_turbulence + + northward_sea_water_velocity_due_to_parameterized_mesoscale_eddies - - surface_upward_mole_flux_of_carbon_dioxide + + ocean_heat_y_transport_due_to_parameterized_eddy_advection - - surface_downward_mole_flux_of_carbon_dioxide + + ocean_meridional_overturning_mass_streamfunction_due_to_parameterized_eddy_advection - - atmosphere_mass_content_of_cloud_condensed_water + + ocean_mass_y_transport_due_to_advection_and_parameterized_eddy_advection - - northward_water_vapor_flux_in_air + + ocean_mass_x_transport_due_to_advection_and_parameterized_eddy_advection - - lwe_stratiform_snowfall_rate + + ocean_heat_x_transport_due_to_parameterized_eddy_advection - - stratiform_snowfall_amount + + northward_ocean_freshwater_transport_due_to_parameterized_eddy_advection - - stratiform_rainfall_rate + + northward_ocean_salt_transport_due_to_parameterized_eddy_advection - - stratiform_rainfall_flux + + integral_wrt_time_of_toa_outgoing_longwave_flux - - stratiform_rainfall_amount + + integral_wrt_time_of_toa_net_downward_shortwave_flux - - tendency_of_atmosphere_mass_content_of_pm2p5_sea_salt_dry_aerosol_particles_due_to_emission + + integral_wrt_time_of_surface_net_downward_longwave_flux - - tendency_of_atmosphere_mass_content_of_pm2p5_sea_salt_dry_aerosol_particles_due_to_dry_deposition + + integral_wrt_time_of_surface_downward_sensible_heat_flux - - tendency_of_sea_surface_height_above_mean_sea_level + + integral_wrt_time_of_surface_downward_latent_heat_flux - - mass_fraction_of_pm10_ambient_aerosol_particles_in_air + + integral_wrt_time_of_air_temperature_excess - - mass_fraction_of_pm10_ambient_aerosol_particles_in_air + + integral_wrt_time_of_air_temperature_deficit - - mass_concentration_of_pm10_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_ammonium_dry_aerosol_particles_due_to_wet_deposition - - atmosphere_optical_thickness_due_to_pm10_ambient_aerosol_particles + + tendency_of_atmosphere_mass_content_of_ammonium_dry_aerosol_particles_due_to_dry_deposition - - surface_geostrophic_eastward_sea_water_velocity + + tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_expressed_as_sulfur_due_to_turbulent_deposition - - mass_fraction_of_pm2p5_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_expressed_as_sulfur_due_to_turbulent_deposition - - mass_fraction_of_pm2p5_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_due_to_emission - - mass_concentration_of_pm2p5_ambient_aerosol_particles_in_air + + atmosphere_optical_thickness_due_to_particulate_organic_matter_ambient_aerosol_particles - - atmosphere_optical_thickness_due_to_pm2p5_ambient_aerosol_particles + + mass_concentration_of_nitric_acid_trihydrate_ambient_aerosol_particles_in_air - - mass_fraction_of_pm1_ambient_aerosol_particles_in_air + + atmosphere_mass_content_of_water_in_ambient_aerosol_particles - - mass_fraction_of_pm1_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_residential_and_commercial_combustion - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_wet_deposition + + tendency_of_atmosphere_mass_content_of_mercury_dry_aerosol_particles_due_to_wet_deposition - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_wet_deposition + + tendency_of_atmosphere_mass_content_of_mercury_dry_aerosol_particles_due_to_dry_deposition - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_gravitational_settling + + mass_fraction_of_nitrate_dry_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_gravitational_settling + + mass_concentration_of_sulfate_dry_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_turbulent_deposition + + mass_fraction_of_water_in_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_turbulent_deposition + + mass_fraction_of_secondary_particulate_organic_matter_dry_aerosol_particles_in_air - - mass_concentration_of_pm1_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_industrial_processes_and_combustion - - atmosphere_optical_thickness_due_to_pm1_ambient_aerosol_particles + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_energy_production_and_distribution - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_dry_deposition + + mass_concentration_of_sulfate_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_dry_deposition + + mass_concentration_of_sulfate_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_pm2p5_sea_salt_dry_aerosol_particles_due_to_wet_deposition + + mass_concentration_of_dust_dry_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_pm10_sea_salt_dry_aerosol_particles_due_to_wet_deposition + + tendency_of_atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles_due_to_emission - - tendency_of_atmosphere_mass_content_of_pm10_sea_salt_dry_aerosol_particles_due_to_dry_deposition + + tendency_of_atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles_due_to_dry_deposition - - mass_fraction_of_sea_salt_dry_aerosol_particles_in_air + + mass_fraction_of_primary_particulate_organic_matter_dry_aerosol_particles_in_air - - mass_fraction_of_sea_salt_dry_aerosol_particles_in_air + + mass_fraction_of_particulate_organic_matter_dry_aerosol_particles_in_air - - mass_concentration_of_sea_salt_dry_aerosol_particles_in_air + + number_concentration_of_coarse_mode_ambient_aerosol_particles_in_air - - mass_concentration_of_sea_salt_dry_aerosol_particles_in_air + + mass_concentration_of_primary_particulate_organic_matter_dry_aerosol_particles_in_air - - atmosphere_optical_thickness_due_to_sea_salt_ambient_aerosol_particles + + atmosphere_mass_content_of_ammonium_dry_aerosol_particles - - atmosphere_optical_thickness_due_to_sea_salt_ambient_aerosol_particles + + stratiform_rainfall_rate - - atmosphere_mass_content_of_sea_salt_dry_aerosol_particles + + stratiform_rainfall_flux - - atmosphere_mass_content_of_sea_salt_dry_aerosol_particles + + stratiform_rainfall_amount @@ -33480,6 +33139,22 @@ upward_eastward_momentum_flux_in_air_due_to_nonorographic_westward_gravity_waves + + mass_fraction_of_ozone_in_air + + + + mass_fraction_of_convective_cloud_condensed_water_in_air + + + + sea_surface_swell_wave_period + + + + surface_drag_coefficient_in_air + + specific_gravitational_potential_energy @@ -33500,14 +33175,6 @@ isotropic_longwave_radiance_in_air - - mass_concentration_of_primary_particulate_organic_matter_dry_aerosol_particles_in_air - - - - atmosphere_mass_content_of_ammonium_dry_aerosol_particles - - stratiform_snowfall_flux @@ -33516,120 +33183,108 @@ thickness_of_stratiform_rainfall_amount - - sea_surface_wind_wave_period - - - - omnidirectional_spherical_irradiance_per_unit_wavelength_in_sea_water - - - - tendency_of_middle_atmosphere_moles_of_molecular_hydrogen - - - - tendency_of_middle_atmosphere_moles_of_methyl_chloride + + sea_surface_wave_significant_height - - tendency_of_middle_atmosphere_moles_of_methane + + tendency_of_atmosphere_moles_of_nitric_acid_trihydrate_ambient_aerosol_particles - - sea_water_y_velocity + + tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_due_to_dry_deposition - - sea_water_x_velocity + + tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_wet_deposition - - mole_fraction_of_hypochlorous_acid_in_air + + number_concentration_of_nucleation_mode_ambient_aerosol_particles_in_air - - tendency_of_troposphere_moles_of_molecular_hydrogen + + number_concentration_of_ambient_aerosol_particles_in_air - - tendency_of_troposphere_moles_of_methyl_chloride + + mole_fraction_of_nitric_acid_trihydrate_ambient_aerosol_particles_in_air - - mass_content_of_water_vapor_in_atmosphere_layer + + mass_fraction_of_dust_dry_aerosol_particles_in_air - - mass_content_of_water_in_atmosphere_layer + + mass_concentration_of_water_in_ambient_aerosol_particles_in_air - - tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_turbulence + + mass_concentration_of_nitrate_dry_aerosol_particles_in_air - - tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_deep_convection + + mass_concentration_of_particulate_organic_matter_dry_aerosol_particles_in_air - - tendency_of_troposphere_moles_of_methyl_bromide + + mass_concentration_of_ammonium_dry_aerosol_particles_in_air - - tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_convection + + atmosphere_mass_content_of_sulfate_ambient_aerosol_particles - - tendency_of_atmosphere_mass_content_of_water_vapor_due_to_shallow_convection + + atmosphere_mass_content_of_sulfate_ambient_aerosol_particles - - radiation_wavelength + + atmosphere_mass_content_of_dust_dry_aerosol_particles - - tendency_of_troposphere_moles_of_methane + + atmosphere_absorption_optical_thickness_due_to_ambient_aerosol_particles - - tendency_of_atmosphere_mass_content_of_water_due_to_advection + + atmosphere_mass_content_of_sulfate_dry_aerosol_particles - - mole_fraction_of_chlorine_monoxide_in_air + + tendency_of_atmosphere_mass_content_of_water_vapor_due_to_turbulence - - mole_fraction_of_chlorine_dioxide_in_air + + surface_upward_mole_flux_of_carbon_dioxide - - mass_fraction_of_ozone_in_air + + surface_downward_mole_flux_of_carbon_dioxide - - mass_fraction_of_convective_cloud_condensed_water_in_air + + atmosphere_mass_content_of_cloud_condensed_water - - sea_surface_swell_wave_period + + northward_water_vapor_flux_in_air - - surface_drag_coefficient_in_air + + lwe_stratiform_snowfall_rate - - mass_content_of_cloud_condensed_water_in_atmosphere_layer + + stratiform_snowfall_amount - - mole_concentration_of_organic_detritus_expressed_as_silicon_in_sea_water + + sea_surface_wind_wave_period - - mole_concentration_of_organic_detritus_expressed_as_nitrogen_in_sea_water + + omnidirectional_spherical_irradiance_per_unit_wavelength_in_sea_water @@ -33692,6 +33347,98 @@ tendency_of_atmosphere_moles_of_methyl_chloride + + tendency_of_middle_atmosphere_moles_of_molecular_hydrogen + + + + tendency_of_middle_atmosphere_moles_of_methyl_chloride + + + + tendency_of_middle_atmosphere_moles_of_methane + + + + sea_water_y_velocity + + + + sea_water_x_velocity + + + + mole_fraction_of_hypochlorous_acid_in_air + + + + tendency_of_troposphere_moles_of_molecular_hydrogen + + + + tendency_of_troposphere_moles_of_methyl_chloride + + + + mass_content_of_water_vapor_in_atmosphere_layer + + + + mass_content_of_water_in_atmosphere_layer + + + + tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_turbulence + + + + tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_deep_convection + + + + tendency_of_troposphere_moles_of_methyl_bromide + + + + tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_convection + + + + tendency_of_atmosphere_mass_content_of_water_vapor_due_to_shallow_convection + + + + radiation_wavelength + + + + tendency_of_troposphere_moles_of_methane + + + + tendency_of_atmosphere_mass_content_of_water_due_to_advection + + + + mole_fraction_of_chlorine_monoxide_in_air + + + + mole_fraction_of_chlorine_dioxide_in_air + + + + mass_content_of_cloud_condensed_water_in_atmosphere_layer + + + + mole_concentration_of_organic_detritus_expressed_as_silicon_in_sea_water + + + + mole_concentration_of_organic_detritus_expressed_as_nitrogen_in_sea_water + + surface_drag_coefficient_for_momentum_in_air diff --git a/lib/iris/_merge.py b/lib/iris/_merge.py index bc12080523..6758e9f55d 100644 --- a/lib/iris/_merge.py +++ b/lib/iris/_merge.py @@ -1809,8 +1809,7 @@ def key_func(coord): # Order the coordinates by hints, axis, and definition. for coord in sorted(coords, key=key_func): - dims = tuple(cube.coord_dims(coord)) - if not dims and coord.shape == (1,): + if not cube.coord_dims(coord) and coord.shape == (1,): # Extract the scalar coordinate data and metadata. scalar_defns.append(coord.metadata) # Because we know there's a single Cell in the @@ -1835,11 +1834,11 @@ def key_func(coord): # Extract the vector coordinate and metadata. if id(coord) in cube_aux_coord_ids: vector_aux_coords_and_dims.append( - _CoordAndDims(coord, dims) + _CoordAndDims(coord, tuple(cube.coord_dims(coord))) ) else: vector_dim_coords_and_dims.append( - _CoordAndDims(coord, dims) + _CoordAndDims(coord, tuple(cube.coord_dims(coord))) ) factory_defns = [] diff --git a/lib/iris/_representation/cube_summary.py b/lib/iris/_representation/cube_summary.py index 1e78a92fd1..68f86832f5 100644 --- a/lib/iris/_representation/cube_summary.py +++ b/lib/iris/_representation/cube_summary.py @@ -207,7 +207,7 @@ def __init__(self, title, attributes): self.values = [] self.contents = [] for name, value in sorted(attributes.items()): - value = value_repr(value, quote_strings=True) + value = value_repr(value) value = iris.util.clip_string(value) self.names.append(name) self.values.append(value) diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index b1a9e1d259..dc8ad78080 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -1394,7 +1394,7 @@ def _lazy_rms(array, axis, **kwargs): # all. Thus trying to use this aggregator with weights will currently # raise an error in dask due to the unexpected keyword `weights`, # rather than silently returning the wrong answer. - return da.sqrt(da.mean(array**2, axis=axis, **kwargs)) + return da.sqrt(da.mean(array ** 2, axis=axis, **kwargs)) @_build_dask_mdtol_function @@ -2170,20 +2170,15 @@ def _compute_groupby_coords(self): def _compute_shared_coords(self): """Create the new shared coordinates given the group slices.""" - groupby_indices = [] groupby_bounds = [] - # Iterate over the ordered dictionary in order to construct a list of - # tuple group indices, and a list of the respective bounds of those - # indices. + # Iterate over the ordered dictionary in order to construct + # a list of tuple group boundary indexes. for key_slice in self._slices_by_key.values(): if isinstance(key_slice, tuple): - indices = key_slice + groupby_bounds.append((key_slice[0], key_slice[-1])) else: - indices = tuple(range(*key_slice.indices(self._stop))) - - groupby_indices.append(indices) - groupby_bounds.append((indices[0], indices[-1])) + groupby_bounds.append((key_slice.start, key_slice.stop - 1)) # Create new shared bounded coordinates. for coord, dim in self._shared_coords: @@ -2202,9 +2197,15 @@ def _compute_shared_coords(self): new_shape += shape[:-1] work_arr = work_arr.reshape(work_shape) - for indices in groupby_indices: + for key_slice in self._slices_by_key.values(): + if isinstance(key_slice, slice): + indices = key_slice.indices( + coord.points.shape[dim] + ) + key_slice = range(*indices) + for arr in work_arr: - new_points.append("|".join(arr.take(indices))) + new_points.append("|".join(arr.take(key_slice))) # Reinstate flattened dimensions. Aggregated dim now leads. new_points = np.array(new_points).reshape(new_shape) @@ -2219,54 +2220,48 @@ def _compute_shared_coords(self): raise ValueError(msg) else: new_bounds = [] - if coord.has_bounds(): - # Derive new coord's bounds from bounds. - item = coord.bounds - maxmin_axis = (dim, -1) - first_choices = coord.bounds.take(0, -1) - last_choices = coord.bounds.take(1, -1) - else: - # Derive new coord's bounds from points. - item = coord.points - maxmin_axis = dim - first_choices = last_choices = coord.points - - # Check whether item is monotonic along the dimension of interest. - deltas = np.diff(item, 1, dim) - monotonic = np.all(deltas >= 0) or np.all(deltas <= 0) # Construct list of coordinate group boundary pairs. - if monotonic: - # Use first and last bound or point for new bounds. - for start, stop in groupby_bounds: + for start, stop in groupby_bounds: + if coord.has_bounds(): + # Collapse group bounds into bounds. if ( getattr(coord, "circular", False) - and (stop + 1) == self._stop + and (stop + 1) == coord.shape[dim] ): new_bounds.append( [ - first_choices.take(start, dim), - first_choices.take(0, dim) + coord.bounds.take(start, dim).take(0, -1), + coord.bounds.take(0, dim).take(0, -1) + coord.units.modulus, ] ) else: new_bounds.append( [ - first_choices.take(start, dim), - last_choices.take(stop, dim), + coord.bounds.take(start, dim).take(0, -1), + coord.bounds.take(stop, dim).take(1, -1), + ] + ) + else: + # Collapse group points into bounds. + if getattr(coord, "circular", False) and ( + stop + 1 + ) == len(coord.points): + new_bounds.append( + [ + coord.points.take(start, dim), + coord.points.take(0, dim) + + coord.units.modulus, + ] + ) + else: + new_bounds.append( + [ + coord.points.take(start, dim), + coord.points.take(stop, dim), ] ) - else: - # Use min and max bound or point for new bounds. - for indices in groupby_indices: - item_slice = item.take(indices, dim) - new_bounds.append( - [ - item_slice.min(axis=maxmin_axis), - item_slice.max(axis=maxmin_axis), - ] - ) # Bounds needs to be an array with the length 2 start-stop # dimension last, and the aggregated dimension back in its @@ -2691,6 +2686,14 @@ class UnstructuredNearest: .. Note:: Currently only supports regridding, not interpolation. + .. Note:: + This scheme performs essentially the same job as + :class:`iris.experimental.regrid.ProjectedUnstructuredNearest`. + That scheme is faster, but only works well on data in a limited + region of the globe, covered by a specified projection. + This approach is more rigorously correct and can be applied to global + datasets. + """ # Note: the argument requirements are simply those of the underlying @@ -2761,9 +2764,6 @@ class PointInCell: This class describes the point-in-cell regridding scheme for use typically with :meth:`iris.cube.Cube.regrid()`. - Each result datapoint is an average over all source points that fall inside - that (bounded) target cell. - The PointInCell regridder can regrid data from a source grid of any dimensionality and in any coordinate system. The location of each source point is specified by X and Y coordinates @@ -2781,12 +2781,8 @@ class PointInCell: def __init__(self, weights=None): """ - Point-in-cell regridding scheme suitable for regridding from a source - cube with X and Y coordinates all on the same dimensions, to a target - cube with bounded X and Y coordinates on separate X and Y dimensions. - - Each result datapoint is an average over all source points that fall - inside that (bounded) target cell. + Point-in-cell regridding scheme suitable for regridding over one + or more orthogonal coordinates. Optional Args: diff --git a/lib/iris/analysis/_area_weighted.py b/lib/iris/analysis/_area_weighted.py index 8381185e58..ae162f6c53 100644 --- a/lib/iris/analysis/_area_weighted.py +++ b/lib/iris/analysis/_area_weighted.py @@ -3,18 +3,8 @@ # 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. -import functools -import cf_units -import numpy as np -import numpy.ma as ma - -from iris._lazy_data import map_complete_blocks from iris.analysis._interpolation import get_xy_dim_coords, snapshot_grid -from iris.analysis._regrid import RectilinearRegridder -import iris.analysis.cartography -import iris.coord_systems -from iris.util import _meshgrid class AreaWeightedRegridder: @@ -53,6 +43,10 @@ def __init__(self, src_grid_cube, target_grid_cube, mdtol=1): the same coordinate system. """ + from iris.experimental.regrid import ( + _regrid_area_weighted_rectilinear_src_and_grid__prepare, + ) + # Snapshot the state of the source cube to ensure that the regridder is # impervious to external changes to the original cubes. self._src_grid = snapshot_grid(src_grid_cube) @@ -109,6 +103,10 @@ def __call__(self, cube): in the horizontal dimensions will be combined before regridding. """ + from iris.experimental.regrid import ( + _regrid_area_weighted_rectilinear_src_and_grid__perform, + ) + src_x, src_y = get_xy_dim_coords(cube) if (src_x, src_y) != self._src_grid: raise ValueError( @@ -132,1007 +130,3 @@ def __call__(self, cube): return _regrid_area_weighted_rectilinear_src_and_grid__perform( cube, _regrid_info, mdtol=self._mdtol ) - - -# -# Support routines, all originally in iris.experimental.regrid -# - - -def _get_xy_coords(cube): - """ - Return the x and y coordinates from a cube. - - This function will preferentially return a pair of dimension - coordinates (if there are more than one potential x or y dimension - coordinates a ValueError will be raised). If the cube does not have - a pair of x and y dimension coordinates it will return 1D auxiliary - coordinates (including scalars). If there is not one and only one set - of x and y auxiliary coordinates a ValueError will be raised. - - Having identified the x and y coordinates, the function checks that they - have equal coordinate systems and that they do not occupy the same - dimension on the cube. - - Args: - - * cube: - An instance of :class:`iris.cube.Cube`. - - Returns: - A tuple containing the cube's x and y coordinates. - - """ - # Look for a suitable dimension coords first. - x_coords = cube.coords(axis="x", dim_coords=True) - if not x_coords: - # If there is no x coord in dim_coords look for scalars or - # monotonic coords in aux_coords. - x_coords = [ - coord - for coord in cube.coords(axis="x", dim_coords=False) - if coord.ndim == 1 and coord.is_monotonic() - ] - if len(x_coords) != 1: - raise ValueError( - "Cube {!r} must contain a single 1D x " - "coordinate.".format(cube.name()) - ) - x_coord = x_coords[0] - - # Look for a suitable dimension coords first. - y_coords = cube.coords(axis="y", dim_coords=True) - if not y_coords: - # If there is no y coord in dim_coords look for scalars or - # monotonic coords in aux_coords. - y_coords = [ - coord - for coord in cube.coords(axis="y", dim_coords=False) - if coord.ndim == 1 and coord.is_monotonic() - ] - if len(y_coords) != 1: - raise ValueError( - "Cube {!r} must contain a single 1D y " - "coordinate.".format(cube.name()) - ) - y_coord = y_coords[0] - - if x_coord.coord_system != y_coord.coord_system: - raise ValueError( - "The cube's x ({!r}) and y ({!r}) " - "coordinates must have the same coordinate " - "system.".format(x_coord.name(), y_coord.name()) - ) - - # The x and y coordinates must describe different dimensions - # or be scalar coords. - x_dims = cube.coord_dims(x_coord) - x_dim = None - if x_dims: - x_dim = x_dims[0] - - y_dims = cube.coord_dims(y_coord) - y_dim = None - if y_dims: - y_dim = y_dims[0] - - if x_dim is not None and y_dim == x_dim: - raise ValueError( - "The cube's x and y coords must not describe the " - "same data dimension." - ) - - return x_coord, y_coord - - -def _within_bounds(src_bounds, tgt_bounds, orderswap=False): - """ - Determine which target bounds lie within the extremes of the source bounds. - - Args: - - * src_bounds (ndarray): - An (n, 2) shaped array of monotonic contiguous source bounds. - * tgt_bounds (ndarray): - An (n, 2) shaped array corresponding to the target bounds. - - Kwargs: - - * orderswap (bool): - A Boolean indicating whether the target bounds are in descending order - (True). Defaults to False. - - Returns: - Boolean ndarray, indicating whether each target bound is within the - extremes of the source bounds. - - """ - min_bound = np.min(src_bounds) - 1e-14 - max_bound = np.max(src_bounds) + 1e-14 - - # Swap upper-lower is necessary. - if orderswap is True: - upper, lower = tgt_bounds.T - else: - lower, upper = tgt_bounds.T - - return ((lower <= max_bound) * (lower >= min_bound)) * ( - (upper <= max_bound) * (upper >= min_bound) - ) - - -def _cropped_bounds(bounds, lower, upper): - """ - Return a new bounds array and corresponding slice object (or indices) of - the original data array, resulting from cropping the provided bounds - between the specified lower and upper values. The bounds at the - extremities will be truncated so that they start and end with lower and - upper. - - This function will return an empty NumPy array and slice if there is no - overlap between the region covered by bounds and the region from lower to - upper. - - If lower > upper the resulting bounds may not be contiguous and the - indices object will be a tuple of indices rather than a slice object. - - Args: - - * bounds: - An (n, 2) shaped array of monotonic contiguous bounds. - * lower: - Lower bound at which to crop the bounds array. - * upper: - Upper bound at which to crop the bounds array. - - Returns: - A tuple of the new bounds array and the corresponding slice object or - indices from the zeroth axis of the original array. - - """ - reversed_flag = False - # Ensure order is increasing. - if bounds[0, 0] > bounds[-1, 0]: - # Reverse bounds - bounds = bounds[::-1, ::-1] - reversed_flag = True - - # Number of bounds. - n = bounds.shape[0] - - if lower <= upper: - if lower > bounds[-1, 1] or upper < bounds[0, 0]: - new_bounds = bounds[0:0] - indices = slice(0, 0) - else: - # A single region lower->upper. - if lower < bounds[0, 0]: - # Region extends below bounds so use first lower bound. - lindex = 0 - lower = bounds[0, 0] - else: - # Index of last lower bound less than or equal to lower. - lindex = np.nonzero(bounds[:, 0] <= lower)[0][-1] - if upper > bounds[-1, 1]: - # Region extends above bounds so use last upper bound. - uindex = n - 1 - upper = bounds[-1, 1] - else: - # Index of first upper bound greater than or equal to - # upper. - uindex = np.nonzero(bounds[:, 1] >= upper)[0][0] - # Extract the bounds in our region defined by lower->upper. - new_bounds = np.copy(bounds[lindex : (uindex + 1), :]) - # Replace first and last values with specified bounds. - new_bounds[0, 0] = lower - new_bounds[-1, 1] = upper - if reversed_flag: - indices = slice(n - (uindex + 1), n - lindex) - else: - indices = slice(lindex, uindex + 1) - else: - # Two regions [0]->upper, lower->[-1] - # [0]->upper - if upper < bounds[0, 0]: - # Region outside src bounds. - new_bounds_left = bounds[0:0] - indices_left = tuple() - slice_left = slice(0, 0) - else: - if upper > bounds[-1, 1]: - # Whole of bounds. - uindex = n - 1 - upper = bounds[-1, 1] - else: - # Index of first upper bound greater than or equal to upper. - uindex = np.nonzero(bounds[:, 1] >= upper)[0][0] - # Extract the bounds in our region defined by [0]->upper. - new_bounds_left = np.copy(bounds[0 : (uindex + 1), :]) - # Replace last value with specified bound. - new_bounds_left[-1, 1] = upper - if reversed_flag: - indices_left = tuple(range(n - (uindex + 1), n)) - slice_left = slice(n - (uindex + 1), n) - else: - indices_left = tuple(range(0, uindex + 1)) - slice_left = slice(0, uindex + 1) - # lower->[-1] - if lower > bounds[-1, 1]: - # Region is outside src bounds. - new_bounds_right = bounds[0:0] - indices_right = tuple() - slice_right = slice(0, 0) - else: - if lower < bounds[0, 0]: - # Whole of bounds. - lindex = 0 - lower = bounds[0, 0] - else: - # Index of last lower bound less than or equal to lower. - lindex = np.nonzero(bounds[:, 0] <= lower)[0][-1] - # Extract the bounds in our region defined by lower->[-1]. - new_bounds_right = np.copy(bounds[lindex:, :]) - # Replace first value with specified bound. - new_bounds_right[0, 0] = lower - if reversed_flag: - indices_right = tuple(range(0, n - lindex)) - slice_right = slice(0, n - lindex) - else: - indices_right = tuple(range(lindex, n)) - slice_right = slice(lindex, None) - - if reversed_flag: - # Flip everything around. - indices_left, indices_right = indices_right, indices_left - slice_left, slice_right = slice_right, slice_left - - # Combine regions. - new_bounds = np.concatenate((new_bounds_left, new_bounds_right)) - # Use slices if possible, but if we have two regions use indices. - if indices_left and indices_right: - indices = indices_left + indices_right - elif indices_left: - indices = slice_left - elif indices_right: - indices = slice_right - else: - indices = slice(0, 0) - - if reversed_flag: - new_bounds = new_bounds[::-1, ::-1] - - return new_bounds, indices - - -def _cartesian_area(y_bounds, x_bounds): - """ - Return an array of the areas of each cell given two arrays - of cartesian bounds. - - Args: - - * y_bounds: - An (n, 2) shaped NumPy array. - * x_bounds: - An (m, 2) shaped NumPy array. - - Returns: - An (n, m) shaped Numpy array of areas. - - """ - heights = y_bounds[:, 1] - y_bounds[:, 0] - widths = x_bounds[:, 1] - x_bounds[:, 0] - return np.abs(np.outer(heights, widths)) - - -def _spherical_area(y_bounds, x_bounds, radius=1.0): - """ - Return an array of the areas of each cell on a sphere - given two arrays of latitude and longitude bounds in radians. - - Args: - - * y_bounds: - An (n, 2) shaped NumPy array of latitide bounds in radians. - * x_bounds: - An (m, 2) shaped NumPy array of longitude bounds in radians. - * radius: - Radius of the sphere. Default is 1.0. - - Returns: - An (n, m) shaped Numpy array of areas. - - """ - return iris.analysis.cartography._quadrant_area(y_bounds, x_bounds, radius) - - -def _get_bounds_in_units(coord, units, dtype): - """Return a copy of coord's bounds in the specified units and dtype.""" - # The bounds are cast to dtype before conversion to prevent issues when - # mixing float32 and float64 types. - return coord.units.convert(coord.bounds.astype(dtype), units).astype(dtype) - - -def _weighted_mean_with_mdtol(data, weights, axis=None, mdtol=0): - """ - Return the weighted mean of an array over the specified axis - using the provided weights (if any) and a permitted fraction of - masked data. - - Args: - - * data (array-like): - Data to be averaged. - - * weights (array-like): - An array of the same shape as the data that specifies the contribution - of each corresponding data element to the calculated mean. - - Kwargs: - - * axis (int or tuple of ints): - Axis along which the mean is computed. The default is to compute - the mean of the flattened array. - - * mdtol (float): - Tolerance of missing data. The value returned in each element of the - returned array will be masked if the fraction of masked data exceeds - mdtol. This fraction is weighted by the `weights` array if one is - provided. mdtol=0 means no missing data is tolerated - while mdtol=1 will mean the resulting element will be masked if and - only if all the contributing elements of data are masked. - Defaults to 0. - - Returns: - Numpy array (possibly masked) or scalar. - - """ - if ma.is_masked(data): - res, unmasked_weights_sum = ma.average( - data, weights=weights, axis=axis, returned=True - ) - if mdtol < 1: - weights_sum = weights.sum(axis=axis) - frac_masked = 1 - np.true_divide(unmasked_weights_sum, weights_sum) - mask_pt = frac_masked > mdtol - if np.any(mask_pt) and not isinstance(res, ma.core.MaskedConstant): - if np.isscalar(res): - res = ma.masked - elif ma.isMaskedArray(res): - res.mask |= mask_pt - else: - res = ma.masked_array(res, mask=mask_pt) - else: - res = np.average(data, weights=weights, axis=axis) - return res - - -def _regrid_area_weighted_array( - src_data, x_dim, y_dim, weights_info, index_info, mdtol=0 -): - """ - Regrid the given data from its source grid to a new grid using - an area weighted mean to determine the resulting data values. - - .. note:: - - Elements in the returned array that lie either partially - or entirely outside of the extent of the source grid will - be masked irrespective of the value of mdtol. - - Args: - - * src_data: - An N-dimensional NumPy array. - * x_dim: - The X dimension within `src_data`. - * y_dim: - The Y dimension within `src_data`. - * weights_info: - The area weights information to be used for area-weighted - regridding. - - Kwargs: - - * mdtol: - Tolerance of missing data. The value returned in each element of the - returned array will be masked if the fraction of missing data exceeds - mdtol. This fraction is calculated based on the area of masked cells - within each target cell. mdtol=0 means no missing data is tolerated - while mdtol=1 will mean the resulting element will be masked if and - only if all the overlapping elements of the source grid are masked. - Defaults to 0. - - Returns: - The regridded data as an N-dimensional NumPy array. The lengths - of the X and Y dimensions will now match those of the target - grid. - - """ - ( - blank_weights, - src_area_weights, - new_data_mask_basis, - ) = weights_info - - ( - result_x_extent, - result_y_extent, - square_data_indices_y, - square_data_indices_x, - src_area_datas_required, - ) = index_info - - # Ensure we have x_dim and y_dim. - x_dim_orig = x_dim - y_dim_orig = y_dim - if y_dim is None: - src_data = np.expand_dims(src_data, axis=src_data.ndim) - y_dim = src_data.ndim - 1 - if x_dim is None: - src_data = np.expand_dims(src_data, axis=src_data.ndim) - x_dim = src_data.ndim - 1 - # Move y_dim and x_dim to last dimensions - if not x_dim == src_data.ndim - 1: - src_data = np.moveaxis(src_data, x_dim, -1) - if not y_dim == src_data.ndim - 2: - if x_dim < y_dim: - # note: y_dim was shifted along by one position when - # x_dim was moved to the last dimension - src_data = np.moveaxis(src_data, y_dim - 1, -2) - elif x_dim > y_dim: - src_data = np.moveaxis(src_data, y_dim, -2) - x_dim = src_data.ndim - 1 - y_dim = src_data.ndim - 2 - - # Create empty "pre-averaging" data array that will enable the - # src_data data coresponding to a given target grid point, - # to be stacked per point. - # Note that dtype is not preserved and that the array mask - # allows for regions that do not overlap. - new_shape = list(src_data.shape) - new_shape[x_dim] = result_x_extent - new_shape[y_dim] = result_y_extent - - # Use input cube dtype or convert values to the smallest possible float - # dtype when necessary. - dtype = np.promote_types(src_data.dtype, np.float16) - - # Axes of data over which the weighted mean is calculated. - axis = (y_dim, x_dim) - - # Use previously established indices - - src_area_datas_square = src_data[ - ..., square_data_indices_y, square_data_indices_x - ] - - _, src_area_datas_required = np.broadcast_arrays( - src_area_datas_square, src_area_datas_required - ) - - src_area_datas = np.where( - src_area_datas_required, src_area_datas_square, 0 - ) - - # Flag to indicate whether the original data was a masked array. - src_masked = src_data.mask.any() if ma.isMaskedArray(src_data) else False - if src_masked: - src_area_masks_square = src_data.mask[ - ..., square_data_indices_y, square_data_indices_x - ] - src_area_masks = np.where( - src_area_datas_required, src_area_masks_square, True - ) - - else: - # If the weights were originally blank, set the weights to all 1 to - # avoid divide by 0 error and set the new data mask for making the - # values 0 - src_area_weights = np.where(blank_weights, 1, src_area_weights) - - new_data_mask = np.broadcast_to(new_data_mask_basis, new_shape) - - # Broadcast the weights array to allow numpy's ma.average - # to be called. - # Assign new shape to raise error on copy. - src_area_weights.shape = src_area_datas.shape[-3:] - # Broadcast weights to match shape of data. - _, src_area_weights = np.broadcast_arrays(src_area_datas, src_area_weights) - - # Mask the data points - if src_masked: - src_area_datas = np.ma.array(src_area_datas, mask=src_area_masks) - - # Calculate weighted mean taking into account missing data. - new_data = _weighted_mean_with_mdtol( - src_area_datas, weights=src_area_weights, axis=axis, mdtol=mdtol - ) - new_data = new_data.reshape(new_shape) - if src_masked: - new_data_mask = new_data.mask - - # Mask the data if originally masked or if the result has masked points - if ma.isMaskedArray(src_data): - new_data = ma.array( - new_data, - mask=new_data_mask, - fill_value=src_data.fill_value, - dtype=dtype, - ) - elif new_data_mask.any(): - new_data = ma.array(new_data, mask=new_data_mask, dtype=dtype) - else: - new_data = new_data.astype(dtype) - - # Restore data to original form - if x_dim_orig is None and y_dim_orig is None: - new_data = np.squeeze(new_data, axis=x_dim) - new_data = np.squeeze(new_data, axis=y_dim) - elif y_dim_orig is None: - new_data = np.squeeze(new_data, axis=y_dim) - new_data = np.moveaxis(new_data, -1, x_dim_orig) - elif x_dim_orig is None: - new_data = np.squeeze(new_data, axis=x_dim) - new_data = np.moveaxis(new_data, -1, y_dim_orig) - elif x_dim_orig < y_dim_orig: - # move the x_dim back first, so that the y_dim will - # then be moved to its original position - new_data = np.moveaxis(new_data, -1, x_dim_orig) - new_data = np.moveaxis(new_data, -1, y_dim_orig) - else: - # move the y_dim back first, so that the x_dim will - # then be moved to its original position - new_data = np.moveaxis(new_data, -2, y_dim_orig) - new_data = np.moveaxis(new_data, -1, x_dim_orig) - - return new_data - - -def _regrid_area_weighted_rectilinear_src_and_grid__prepare( - src_cube, grid_cube -): - """ - First (setup) part of 'regrid_area_weighted_rectilinear_src_and_grid'. - - Check inputs and calculate related info. The 'regrid info' returned - can be re-used over many 2d slices. - - """ - # Get the 1d monotonic (or scalar) src and grid coordinates. - src_x, src_y = _get_xy_coords(src_cube) - grid_x, grid_y = _get_xy_coords(grid_cube) - - # Condition 1: All x and y coordinates must have contiguous bounds to - # define areas. - if ( - not src_x.is_contiguous() - or not src_y.is_contiguous() - or not grid_x.is_contiguous() - or not grid_y.is_contiguous() - ): - raise ValueError( - "The horizontal grid coordinates of both the source " - "and grid cubes must have contiguous bounds." - ) - - # Condition 2: Everything must have the same coordinate system. - src_cs = src_x.coord_system - grid_cs = grid_x.coord_system - if src_cs != grid_cs: - raise ValueError( - "The horizontal grid coordinates of both the source " - "and grid cubes must have the same coordinate " - "system." - ) - - # Condition 3: cannot create vector coords from scalars. - src_x_dims = src_cube.coord_dims(src_x) - src_x_dim = None - if src_x_dims: - src_x_dim = src_x_dims[0] - src_y_dims = src_cube.coord_dims(src_y) - src_y_dim = None - if src_y_dims: - src_y_dim = src_y_dims[0] - if ( - src_x_dim is None - and grid_x.shape[0] != 1 - or src_y_dim is None - and grid_y.shape[0] != 1 - ): - raise ValueError( - "The horizontal grid coordinates of source cube " - "includes scalar coordinates, but the new grid does " - "not. The new grid must not require additional data " - "dimensions to be created." - ) - - # Determine whether to calculate flat or spherical areas. - # Don't only rely on coord system as it may be None. - spherical = ( - isinstance( - src_cs, - (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS), - ) - or src_x.units == "degrees" - or src_x.units == "radians" - ) - - # Get src and grid bounds in the same units. - x_units = cf_units.Unit("radians") if spherical else src_x.units - y_units = cf_units.Unit("radians") if spherical else src_y.units - - # Operate in highest precision. - src_dtype = np.promote_types(src_x.bounds.dtype, src_y.bounds.dtype) - grid_dtype = np.promote_types(grid_x.bounds.dtype, grid_y.bounds.dtype) - dtype = np.promote_types(src_dtype, grid_dtype) - - src_x_bounds = _get_bounds_in_units(src_x, x_units, dtype) - src_y_bounds = _get_bounds_in_units(src_y, y_units, dtype) - grid_x_bounds = _get_bounds_in_units(grid_x, x_units, dtype) - grid_y_bounds = _get_bounds_in_units(grid_y, y_units, dtype) - - # Create 2d meshgrids as required by _create_cube func. - meshgrid_x, meshgrid_y = _meshgrid(grid_x.points, grid_y.points) - - # Determine whether target grid bounds are decreasing. This must - # be determined prior to wrap_lons being called. - grid_x_decreasing = grid_x_bounds[-1, 0] < grid_x_bounds[0, 0] - grid_y_decreasing = grid_y_bounds[-1, 0] < grid_y_bounds[0, 0] - - # Wrapping of longitudes. - if spherical: - base = np.min(src_x_bounds) - modulus = x_units.modulus - # Only wrap if necessary to avoid introducing floating - # point errors. - if np.min(grid_x_bounds) < base or np.max(grid_x_bounds) > ( - base + modulus - ): - grid_x_bounds = iris.analysis.cartography.wrap_lons( - grid_x_bounds, base, modulus - ) - - # Determine whether the src_x coord has periodic boundary conditions. - circular = getattr(src_x, "circular", False) - - # Use simple cartesian area function or one that takes into - # account the curved surface if coord system is spherical. - if spherical: - area_func = _spherical_area - else: - area_func = _cartesian_area - - def _calculate_regrid_area_weighted_weights( - src_x_bounds, - src_y_bounds, - grid_x_bounds, - grid_y_bounds, - grid_x_decreasing, - grid_y_decreasing, - area_func, - circular=False, - ): - """ - Compute the area weights used for area-weighted regridding. - Args: - * src_x_bounds: - A NumPy array of bounds along the X axis defining the source grid. - * src_y_bounds: - A NumPy array of bounds along the Y axis defining the source grid. - * grid_x_bounds: - A NumPy array of bounds along the X axis defining the new grid. - * grid_y_bounds: - A NumPy array of bounds along the Y axis defining the new grid. - * grid_x_decreasing: - Boolean indicating whether the X coordinate of the new grid is - in descending order. - * grid_y_decreasing: - Boolean indicating whether the Y coordinate of the new grid is - in descending order. - * area_func: - A function that returns an (p, q) array of weights given an (p, 2) - shaped array of Y bounds and an (q, 2) shaped array of X bounds. - Kwargs: - * circular: - A boolean indicating whether the `src_x_bounds` are periodic. - Default is False. - Returns: - The area weights to be used for area-weighted regridding. - """ - # Determine which grid bounds are within src extent. - y_within_bounds = _within_bounds( - src_y_bounds, grid_y_bounds, grid_y_decreasing - ) - x_within_bounds = _within_bounds( - src_x_bounds, grid_x_bounds, grid_x_decreasing - ) - - # Cache which src_bounds are within grid bounds - cached_x_bounds = [] - cached_x_indices = [] - max_x_indices = 0 - for (x_0, x_1) in grid_x_bounds: - if grid_x_decreasing: - x_0, x_1 = x_1, x_0 - x_bounds, x_indices = _cropped_bounds(src_x_bounds, x_0, x_1) - cached_x_bounds.append(x_bounds) - cached_x_indices.append(x_indices) - # Keep record of the largest slice - if isinstance(x_indices, slice): - x_indices_size = np.sum(x_indices.stop - x_indices.start) - else: # is tuple of indices - x_indices_size = len(x_indices) - if x_indices_size > max_x_indices: - max_x_indices = x_indices_size - - # Cache which y src_bounds areas and weights are within grid bounds - cached_y_indices = [] - cached_weights = [] - max_y_indices = 0 - for j, (y_0, y_1) in enumerate(grid_y_bounds): - # Reverse lower and upper if dest grid is decreasing. - if grid_y_decreasing: - y_0, y_1 = y_1, y_0 - y_bounds, y_indices = _cropped_bounds(src_y_bounds, y_0, y_1) - cached_y_indices.append(y_indices) - # Keep record of the largest slice - if isinstance(y_indices, slice): - y_indices_size = np.sum(y_indices.stop - y_indices.start) - else: # is tuple of indices - y_indices_size = len(y_indices) - if y_indices_size > max_y_indices: - max_y_indices = y_indices_size - - weights_i = [] - for i, (x_0, x_1) in enumerate(grid_x_bounds): - # Reverse lower and upper if dest grid is decreasing. - if grid_x_decreasing: - x_0, x_1 = x_1, x_0 - x_bounds = cached_x_bounds[i] - x_indices = cached_x_indices[i] - - # Determine whether element i, j overlaps with src and hence - # an area weight should be computed. - # If x_0 > x_1 then we want [0]->x_1 and x_0->[0] + mod in the case - # of wrapped longitudes. However if the src grid is not global - # (i.e. circular) this new cell would include a region outside of - # the extent of the src grid and thus the weight is therefore - # invalid. - outside_extent = x_0 > x_1 and not circular - if ( - outside_extent - or not y_within_bounds[j] - or not x_within_bounds[i] - ): - weights = False - else: - # Calculate weights based on areas of cropped bounds. - if isinstance(x_indices, tuple) and isinstance( - y_indices, tuple - ): - raise RuntimeError( - "Cannot handle split bounds " "in both x and y." - ) - weights = area_func(y_bounds, x_bounds) - weights_i.append(weights) - cached_weights.append(weights_i) - return ( - tuple(cached_x_indices), - tuple(cached_y_indices), - max_x_indices, - max_y_indices, - tuple(cached_weights), - ) - - ( - cached_x_indices, - cached_y_indices, - max_x_indices, - max_y_indices, - cached_weights, - ) = _calculate_regrid_area_weighted_weights( - src_x_bounds, - src_y_bounds, - grid_x_bounds, - grid_y_bounds, - grid_x_decreasing, - grid_y_decreasing, - area_func, - circular, - ) - - # Go further, calculating the full weights array that we'll need in the - # perform step and the indices we'll need to extract from the cube we're - # regridding (src_data) - - result_y_extent = len(grid_y_bounds) - result_x_extent = len(grid_x_bounds) - - # Total number of points - num_target_pts = result_y_extent * result_x_extent - - # Create empty array to hold weights - src_area_weights = np.zeros( - list((max_y_indices, max_x_indices, num_target_pts)) - ) - - # Built for the case where the source cube isn't masked - blank_weights = np.zeros((num_target_pts,)) - new_data_mask_basis = np.full( - (len(cached_y_indices), len(cached_x_indices)), False, dtype=np.bool_ - ) - - # To permit fancy indexing, we need to store our data in an array whose - # first two dimensions represent the indices needed for the target cell. - # Since target cells can require a different number of indices, the size of - # these dimensions should be the maximum of this number. - # This means we need to track whether the data in - # that array is actually required and build those squared-off arrays - # TODO: Consider if a proper mask would be better - src_area_datas_required = np.full( - (max_y_indices, max_x_indices, num_target_pts), False - ) - square_data_indices_y = np.zeros( - (max_y_indices, max_x_indices, num_target_pts), dtype=int - ) - square_data_indices_x = np.zeros( - (max_y_indices, max_x_indices, num_target_pts), dtype=int - ) - - # Stack the weights for each target point and build the indices we'll need - # to extract the src_area_data - target_pt_ji = -1 - for j, y_indices in enumerate(cached_y_indices): - for i, x_indices in enumerate(cached_x_indices): - target_pt_ji += 1 - # Determine whether to mask element i, j based on whether - # there are valid weights. - weights = cached_weights[j][i] - if weights is False: - # Prepare for the src_data not being masked by storing the - # information that will let us fill the data with zeros and - # weights as one. The weighted average result will be the same, - # but we avoid dividing by zero. - blank_weights[target_pt_ji] = True - new_data_mask_basis[j, i] = True - else: - # Establish which indices are actually in y_indices and x_indices - if isinstance(y_indices, slice): - y_indices = list( - range( - y_indices.start, - y_indices.stop, - y_indices.step or 1, - ) - ) - else: - y_indices = list(y_indices) - - if isinstance(x_indices, slice): - x_indices = list( - range( - x_indices.start, - x_indices.stop, - x_indices.step or 1, - ) - ) - else: - x_indices = list(x_indices) - - # For the weights, we just need the lengths of these as we're - # dropping them into a pre-made array - - len_y = len(y_indices) - len_x = len(x_indices) - - src_area_weights[0:len_y, 0:len_x, target_pt_ji] = weights - - # To build the indices for the source cube, we need equal - # shaped array so we pad with 0s and record the need to mask - # them in src_area_datas_required - padded_y_indices = y_indices + [0] * (max_y_indices - len_y) - padded_x_indices = x_indices + [0] * (max_x_indices - len_x) - - square_data_indices_y[..., target_pt_ji] = np.array( - padded_y_indices - )[:, np.newaxis] - square_data_indices_x[..., target_pt_ji] = padded_x_indices - - src_area_datas_required[0:len_y, 0:len_x, target_pt_ji] = True - - # Package up the return data - - weights_info = ( - blank_weights, - src_area_weights, - new_data_mask_basis, - ) - - index_info = ( - result_x_extent, - result_y_extent, - square_data_indices_y, - square_data_indices_x, - src_area_datas_required, - ) - - # Now return it - - return ( - src_x, - src_y, - src_x_dim, - src_y_dim, - grid_x, - grid_y, - meshgrid_x, - meshgrid_y, - weights_info, - index_info, - ) - - -def _regrid_area_weighted_rectilinear_src_and_grid__perform( - src_cube, regrid_info, mdtol -): - """ - Second (regrid) part of 'regrid_area_weighted_rectilinear_src_and_grid'. - - Perform the prepared regrid calculation on a single 2d cube. - - """ - ( - src_x, - src_y, - src_x_dim, - src_y_dim, - grid_x, - grid_y, - meshgrid_x, - meshgrid_y, - weights_info, - index_info, - ) = regrid_info - - # Calculate new data array for regridded cube. - regrid = functools.partial( - _regrid_area_weighted_array, - x_dim=src_x_dim, - y_dim=src_y_dim, - weights_info=weights_info, - index_info=index_info, - mdtol=mdtol, - ) - - new_data = map_complete_blocks( - src_cube, regrid, (src_y_dim, src_x_dim), meshgrid_x.shape - ) - - # Wrap up the data as a Cube. - regrid_callback = RectilinearRegridder._regrid - new_cube = RectilinearRegridder._create_cube( - new_data, - src_cube, - src_x_dim, - src_y_dim, - src_x, - src_y, - grid_x, - grid_y, - meshgrid_x, - meshgrid_y, - regrid_callback, - ) - - # Slice out any length 1 dimensions. - indices = [slice(None, None)] * new_data.ndim - if src_x_dim is not None and new_cube.shape[src_x_dim] == 1: - indices[src_x_dim] = 0 - if src_y_dim is not None and new_cube.shape[src_y_dim] == 1: - indices[src_y_dim] = 0 - if 0 in indices: - new_cube = new_cube[tuple(indices)] - - return new_cube diff --git a/lib/iris/analysis/_grid_angles.py b/lib/iris/analysis/_grid_angles.py index 0b52f54568..127aec7c1e 100644 --- a/lib/iris/analysis/_grid_angles.py +++ b/lib/iris/analysis/_grid_angles.py @@ -120,7 +120,7 @@ def _angle(p, q, r): mid_lons = np.deg2rad(q[0]) pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) - pr_norm = np.sqrt(np.sum(pr**2, axis=0)) + pr_norm = np.sqrt(np.sum(pr ** 2, axis=0)) pr_top = pr[1] * np.cos(mid_lons) - pr[0] * np.sin(mid_lons) index = pr_norm == 0 diff --git a/lib/iris/analysis/_scipy_interpolate.py b/lib/iris/analysis/_scipy_interpolate.py index fc64249729..c6b33c56a4 100644 --- a/lib/iris/analysis/_scipy_interpolate.py +++ b/lib/iris/analysis/_scipy_interpolate.py @@ -229,7 +229,7 @@ def compute_interp_weights(self, xi, method=None): xi_shape, method, indices, norm_distances, out_of_bounds = prepared # Allocate arrays for describing the sparse matrix. - n_src_values_per_result_value = 2**ndim + n_src_values_per_result_value = 2 ** ndim n_result_values = len(indices[0]) n_non_zero = n_result_values * n_src_values_per_result_value weights = np.ones(n_non_zero, dtype=norm_distances[0].dtype) diff --git a/lib/iris/analysis/calculus.py b/lib/iris/analysis/calculus.py index 4630f47967..409782f256 100644 --- a/lib/iris/analysis/calculus.py +++ b/lib/iris/analysis/calculus.py @@ -629,10 +629,14 @@ def curl(i_cube, j_cube, k_cube=None): # (d/dtheta (i_cube * sin(lat)) - d_j_cube_dphi) # phi_cmpt = 1/r * ( d/dr (r * j_cube) - d_k_cube_dtheta) # theta_cmpt = 1/r * ( 1/cos(lat) * d_k_cube_dphi - d/dr (r * i_cube) - if y_coord.name() not in [ - "latitude", - "grid_latitude", - ] or x_coord.name() not in ["longitude", "grid_longitude"]: + if ( + y_coord.name() + not in [ + "latitude", + "grid_latitude", + ] + or x_coord.name() not in ["longitude", "grid_longitude"] + ): raise ValueError( "Expecting latitude as the y coord and " "longitude as the x coord for spherical curl." diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index f704468e33..373487af53 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -335,7 +335,7 @@ def _quadrant_area(radian_lat_bounds, radian_lon_bounds, radius_of_earth): raise ValueError("Bounds must be [n,2] array") # fill in a new array of areas - radius_sqr = radius_of_earth**2 + radius_sqr = radius_of_earth ** 2 radian_lat_64 = radian_lat_bounds.astype(np.float64) radian_lon_64 = radian_lon_bounds.astype(np.float64) @@ -1010,8 +1010,8 @@ def _transform_distance_vectors_tolerance_mask( # Squared magnitudes should be equal to one within acceptable tolerance. # A value of atol=2e-3 is used, which corresponds to a change in magnitude # of approximately 0.1%. - sqmag_1_0 = u_one_t**2 + v_zero_t**2 - sqmag_0_1 = u_zero_t**2 + v_one_t**2 + sqmag_1_0 = u_one_t ** 2 + v_zero_t ** 2 + sqmag_0_1 = u_zero_t ** 2 + v_one_t ** 2 mask = np.logical_not( np.logical_and( np.isclose(sqmag_1_0, ones, atol=2e-3), diff --git a/lib/iris/analysis/maths.py b/lib/iris/analysis/maths.py index 1cbc90cc60..571a66b756 100644 --- a/lib/iris/analysis/maths.py +++ b/lib/iris/analysis/maths.py @@ -19,7 +19,6 @@ import numpy as np from numpy import ma -from iris._deprecation import warn_deprecated import iris.analysis from iris.common import SERVICES, Resolve from iris.common.lenient import _lenient_client @@ -139,35 +138,10 @@ def intersection_of_cubes(cube, other_cube): An instance of :class:`iris.cube.Cube`. Returns: - A pair of :class:`iris.cube.Cube` instances in a tuple corresponding to - the original cubes restricted to their intersection. - - .. deprecated:: 3.2.0 - - Instead use :meth:`iris.cube.CubeList.extract_overlapping`. For example, - rather than calling - - .. code:: - - cube1, cube2 = intersection_of_cubes(cube1, cube2) - - replace with - - .. code:: - - cubes = CubeList([cube1, cube2]) - coords = ["latitude", "longitude"] # Replace with relevant coords - intersections = cubes.extract_overlapping(coords) - cube1, cube2 = (intersections[0], intersections[1]) + A pair of :class:`iris.cube.Cube` instances in a tuple corresponding + to the original cubes restricted to their intersection. """ - wmsg = ( - "iris.analysis.maths.intersection_of_cubes has been deprecated and will " - "be removed, please use iris.cube.CubeList.extract_overlapping " - "instead. See intersection_of_cubes docstring for more information." - ) - warn_deprecated(wmsg) - # Take references of the original cubes (which will be copied when # slicing later). new_cube_self = cube @@ -540,7 +514,7 @@ def power(data, out=None): return _math_op_common( cube, power, - cube.units**exponent, + cube.units ** exponent, new_dtype=new_dtype, in_place=in_place, ) diff --git a/lib/iris/analysis/stats.py b/lib/iris/analysis/stats.py index 711e3c5bfb..89dde1818b 100644 --- a/lib/iris/analysis/stats.py +++ b/lib/iris/analysis/stats.py @@ -168,10 +168,10 @@ def _ones_like(cube): covar = (s1 * s2).collapsed( corr_coords, iris.analysis.SUM, weights=weights_1, mdtol=mdtol ) - var_1 = (s1**2).collapsed( + var_1 = (s1 ** 2).collapsed( corr_coords, iris.analysis.SUM, weights=weights_1 ) - var_2 = (s2**2).collapsed( + var_2 = (s2 ** 2).collapsed( corr_coords, iris.analysis.SUM, weights=weights_2 ) diff --git a/lib/iris/common/metadata.py b/lib/iris/common/metadata.py index cb5f53f5f4..29ae910e38 100644 --- a/lib/iris/common/metadata.py +++ b/lib/iris/common/metadata.py @@ -204,7 +204,7 @@ def func(field): lambda field: field not in ( "circular", - "location_axis", + "src_dim", "node_dimension", "edge_dimension", "face_dimension", diff --git a/lib/iris/common/resolve.py b/lib/iris/common/resolve.py index 12db64cafe..6eb79a65f9 100644 --- a/lib/iris/common/resolve.py +++ b/lib/iris/common/resolve.py @@ -112,10 +112,10 @@ class Resolve: Cell methods: mean time (6 hour) Attributes: - Conventions 'CF-1.5' - Model scenario 'A1B' + Conventions CF-1.5 + Model scenario A1B STASH m01s03i236 - source 'Data from Met Office Unified Model 6.05' + source Data from Met Office Unified Model 6.05 >>> print(cube2) air_temperature / (K) (longitude: 49; latitude: 37) @@ -130,10 +130,10 @@ class Resolve: Cell methods: mean time (6 hour) Attributes: - Conventions 'CF-1.5' - Model scenario 'E1' + Conventions CF-1.5 + Model scenario E1 STASH m01s03i236 - source 'Data from Met Office Unified Model 6.05' + source Data from Met Office Unified Model 6.05 >>> print(data.shape) (240, 37, 49) @@ -153,9 +153,9 @@ class Resolve: Cell methods: mean time (6 hour) Attributes: - Conventions 'CF-1.5' + Conventions CF-1.5 STASH m01s03i236 - source 'Data from Met Office Unified Model 6.05' + source Data from Met Office Unified Model 6.05 Secondly, creating an *empty* ``resolver`` instance, that may be called *multiple* times with *different* :class:`~iris.cube.Cube` operands and *different* ``data``, @@ -2413,10 +2413,10 @@ def mapped(self): Cell methods: mean time (6 hour) Attributes: - Conventions 'CF-1.5' - Model scenario 'A1B' + Conventions CF-1.5 + Model scenario A1B STASH m01s03i236 - source 'Data from Met Office Unified Model 6.05' + source Data from Met Office Unified Model 6.05 >>> print(cube2) air_temperature / (K) (longitude: 49; latitude: 37) Dimension coordinates: @@ -2430,10 +2430,10 @@ def mapped(self): Cell methods: mean time (6 hour) Attributes: - Conventions 'CF-1.5' - Model scenario 'E1' + Conventions CF-1.5 + Model scenario E1 STASH m01s03i236 - source 'Data from Met Office Unified Model 6.05' + source Data from Met Office Unified Model 6.05 >>> Resolve().mapped is None True >>> resolver = Resolve(cube1, cube2) @@ -2481,10 +2481,10 @@ def shape(self): Cell methods: mean time (6 hour) Attributes: - Conventions 'CF-1.5' - Model scenario 'A1B' + Conventions CF-1.5 + Model scenario A1B STASH m01s03i236 - source 'Data from Met Office Unified Model 6.05' + source Data from Met Office Unified Model 6.05 >>> print(cube2) air_temperature / (K) (longitude: 49; latitude: 37) Dimension coordinates: @@ -2498,10 +2498,10 @@ def shape(self): Cell methods: mean time (6 hour) Attributes: - Conventions 'CF-1.5' - Model scenario 'E1' + Conventions CF-1.5 + Model scenario E1 STASH m01s03i236 - source 'Data from Met Office Unified Model 6.05' + source Data from Met Office Unified Model 6.05 >>> Resolve().shape is None True >>> Resolve(cube1, cube2).shape diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index 2f875bb159..300f49014a 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -297,10 +297,7 @@ def as_cartopy_crs(self): return ccrs.Geodetic(self.as_cartopy_globe()) def as_cartopy_projection(self): - return ccrs.PlateCarree( - central_longitude=self.longitude_of_prime_meridian, - globe=self.as_cartopy_globe(), - ) + return ccrs.PlateCarree() def as_cartopy_globe(self): # Explicitly set `ellipse` to None as a workaround for diff --git a/lib/iris/coords.py b/lib/iris/coords.py index b236d407da..db193d0046 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -10,7 +10,7 @@ from abc import ABCMeta, abstractmethod from collections import namedtuple -from collections.abc import Container, Iterator +from collections.abc import Iterator import copy from itertools import chain, zip_longest import operator @@ -57,10 +57,6 @@ class _DimensionalMetadata(CFVariableMixin, metaclass=ABCMeta): _MODE_RDIV: "/", } - # Used by printout methods : __str__ and __repr__ - # Overridden in subclasses : Coord->'points', Connectivity->'indices' - _values_array_name = "data" - @abstractmethod def __init__( self, @@ -272,332 +268,78 @@ def _has_lazy_values(self): """ return self._values_dm.has_lazy_data() - def summary( - self, - shorten=False, - max_values=None, - edgeitems=2, - linewidth=None, - precision=None, - convert_dates=True, - _section_indices=None, - ): - r""" - Make a printable text summary. - - Parameters - ---------- - shorten : bool, default = False - If True, produce an abbreviated one-line summary. - If False, produce a multi-line summary, with embedded newlines. - max_values : int or None, default = None - If more than this many data values, print truncated data arrays - instead of full contents. - If 0, print only the shape. - The default is 5 if :attr:`shorten`\ =True, or 15 otherwise. - This overrides ``numpy.get_printoptions['threshold']``\ . - linewidth : int or None, default = None - Character-width controlling line splitting of array outputs. - If unset, defaults to ``numpy.get_printoptions['linewidth']``\ . - edgeitems : int = 2 - Controls truncated array output. - Overrides ``numpy.getprintoptions['edgeitems']``\ . - precision : int or None, default = None - Controls number decimal formatting. - When :attr:`shorten`\ =True this is defaults to 3, in which case it - overrides ``numpy.get_printoptions()['precision']``\ . - convert_dates : bool, default = True - If the units has a calendar, then print array values as date - strings instead of the actual numbers. - - Returns - ------- - result : str - Output text, with embedded newlines when :attr:`shorten`\ =False. - + def _repr_other_metadata(self): + fmt = "" + if self.long_name: + fmt = ", long_name={self.long_name!r}" + if self.var_name: + fmt += ", var_name={self.var_name!r}" + if len(self.attributes) > 0: + fmt += ", attributes={self.attributes}" + result = fmt.format(self=self) + return result - .. note:: - Arrays are formatted using :meth:`numpy.array2string`. Some aspects - of the array formatting are controllable in the usual way, via - :meth:`numpy.printoptions`, but others are overridden as detailed - above. - Control of those aspects is still available, but only via the call - arguments. - - """ - # NOTE: the *private* key "_section_indices" can be set to a dict, to - # return details of which (line, character) each particular section of - # the output text begins at. - # Currently only used by MeshCoord.summary(), which needs this info to - # modify the result string, for idiosyncratic reasons. - - def array_summary(data, n_max, n_edge, linewidth, precision): - # Return a text summary of an array. - # Take account of strings, dates and masked data. - result = "" - formatter = None - if convert_dates and self.units.is_time_reference(): - # Account for dates, if enabled. - # N.B. a time unit with a long time interval ("months" - # or "years") cannot be converted to a date using - # `num2date`, so gracefully fall back to printing - # values as numbers. - if not self.units.is_long_time_interval(): - # Otherwise ... replace all with strings. - if ma.is_masked(data): - mask = data.mask - else: - mask = None - data = np.array(self.units.num2date(data)) - data = data.astype(str) - # Masked datapoints do not survive num2date. - if mask is not None: - data = np.ma.masked_array(data, mask) - - if ma.is_masked(data): - # Masks are not handled by np.array2string, whereas - # MaskedArray.__str__ is using a private method to convert to - # objects. - # Our preferred solution is to convert to strings *and* fill - # with '--'. This is not ideal because numbers will not align - # with a common numeric format, but there is no *public* logic - # in numpy to arrange that, so let's not overcomplicate. - # It happens that array2string *also* does not use a common - # format (width) for strings, but we fix that below... - data = data.astype(str).filled("--") - - if data.dtype.kind == "U": - # Strings : N.B. includes all missing data - # find the longest. - length = max(len(str(x)) for x in data.flatten()) - # Pre-apply a common formatting width. - formatter = {"all": lambda x: str(x).ljust(length)} - - result = np.array2string( - data, - separator=", ", - edgeitems=n_edge, - threshold=n_max, - max_line_width=linewidth, - formatter=formatter, - precision=precision, - ) + def _str_dates(self, dates_as_numbers): + date_obj_array = self.units.num2date(dates_as_numbers) + kwargs = {"separator": ", ", "prefix": " "} + return np.core.arrayprint.array2string( + date_obj_array, formatter={"all": str}, **kwargs + ) - return result - - units_str = str(self.units) - if self.units.calendar and not shorten: - units_str += f", {self.units.calendar} calendar" - title_str = f"{self.name()} / ({units_str})" - cls_str = type(self).__name__ - shape_str = str(self.shape) - - # Implement conditional defaults for control args. - if max_values is None: - max_values = 5 if shorten else 15 - precision = 3 if shorten else None - n_indent = 4 - indent = " " * n_indent - newline_indent = "\n" + indent - if linewidth is not None: - given_array_width = linewidth - else: - given_array_width = np.get_printoptions()["linewidth"] - using_array_width = given_array_width - n_indent * 2 - # Make a printout of the main data array (or maybe not, if lazy). - if self._has_lazy_values(): - data_str = "" - elif max_values == 0: - data_str = "[...]" - else: - data_str = array_summary( - self._values, - n_max=max_values, - n_edge=edgeitems, - linewidth=using_array_width, - precision=precision, + def __str__(self): + # Note: this method includes bounds handling code, but it only runs + # within Coord type instances, as only these allow bounds to be set. + if self.units.is_time_reference(): + fmt = ( + "{cls}({values}{bounds}" + ", standard_name={self.standard_name!r}" + ", calendar={self.units.calendar!r}{other_metadata})" ) - - # The output under construction, divided into lines for convenience. - output_lines = [""] - - def add_output(text, section=None): - # Append output text and record locations of named 'sections' - if section and _section_indices is not None: - # defined a named 'section', recording the current line number - # and character position as its start position - i_line = len(output_lines) - 1 - i_char = len(output_lines[-1]) - _section_indices[section] = (i_line, i_char) - # Split the text-to-add into lines - lines = text.split("\n") - # Add initial text (before first '\n') to the current line - output_lines[-1] += lines[0] - # Add subsequent lines as additional output lines - for line in lines[1:]: - output_lines.append(line) # Add new lines - - if shorten: - add_output(f"<{cls_str}: ") - add_output(f"{title_str} ", section="title") - - if data_str != "": - # Flatten to a single line, reducing repeated spaces. - def flatten_array_str(array_str): - array_str = array_str.replace("\n", " ") - array_str = array_str.replace("\t", " ") - while " " in array_str: - array_str = array_str.replace(" ", " ") - return array_str - - data_str = flatten_array_str(data_str) - # Adjust maximum-width to allow for the title width in the - # repr form. - current_line_len = len(output_lines[-1]) - using_array_width = given_array_width - current_line_len - # Work out whether to include a summary of the data values - if len(data_str) > using_array_width: - # Make one more attempt, printing just the *first* point, - # as this is useful for dates. - data_str = data_str = array_summary( - self._values[:1], - n_max=max_values, - n_edge=edgeitems, - linewidth=using_array_width, - precision=precision, - ) - data_str = flatten_array_str(data_str) - data_str = data_str[:-1] + ", ...]" - if len(data_str) > using_array_width: - # Data summary is still too long : replace with array - # "placeholder" representation. - data_str = "[...]" - - if self.has_bounds(): - data_str += "+bounds" - - if self.shape != (1,): - # Anything non-scalar : show shape as well. - data_str += f" shape{shape_str}" - - # single-line output in 'shorten' mode - add_output(f"{data_str}>", section="data") - - else: - # Long (multi-line) output format. - add_output(f"{cls_str} : ") - add_output(f"{title_str}", section="title") - - def reindent_data_string(text, n_indent): - lines = [line for line in text.split("\n")] - indent = " " * (n_indent - 1) # allow 1 for the initial '[' - # Indent all but the *first* line. - line_1, rest_lines = lines[0], lines[1:] - rest_lines = ["\n" + indent + line for line in rest_lines] - result = line_1 + "".join(rest_lines) - return result - - data_array_str = reindent_data_string(data_str, 2 * n_indent) - - # NOTE: actual section name is variable here : data/points/indices - data_text = f"{self._values_array_name}: " - if "\n" in data_array_str: - # Put initial '[' here, and the rest on subsequent lines - data_text += "[" + newline_indent + indent + data_array_str[1:] + if self.units.is_long_time_interval(): + # A time unit with a long time interval ("months" or "years") + # cannot be converted to a date using `num2date` so gracefully + # fall back to printing points as numbers, not datetimes. + values = self._values else: - # All on one line - data_text += data_array_str - - # N.B. indent section and record section start after that - add_output(newline_indent) - add_output(data_text, section="data") - + values = self._str_dates(self._values) + bounds = "" if self.has_bounds(): - # Add a bounds section : basically just like the 'data'. - if self._bounds_dm.has_lazy_data(): - bounds_array_str = "" - elif max_values == 0: - bounds_array_str = "[...]" + if self.units.is_long_time_interval(): + bounds_vals = self.bounds else: - bounds_array_str = array_summary( - self._bounds_dm.data, - n_max=max_values, - n_edge=edgeitems, - linewidth=using_array_width, - precision=precision, - ) - bounds_array_str = reindent_data_string( - bounds_array_str, 2 * n_indent - ) - - bounds_text = "bounds: " - if "\n" in bounds_array_str: - # Put initial '[' here, and the rest on subsequent lines - bounds_text += ( - "[" + newline_indent + indent + bounds_array_str[1:] - ) - else: - # All on one line - bounds_text += bounds_array_str - - # N.B. indent section and record section start after that - add_output(newline_indent) - add_output(bounds_text, section="bounds") - - if self.has_bounds(): - shape_str += f" bounds{self._bounds_dm.shape}" - - # Add shape section (always) - add_output(newline_indent) - add_output(f"shape: {shape_str}", section="shape") - - # Add dtype section (always) - add_output(newline_indent) - add_output(f"dtype: {self.dtype}", section="dtype") - - for name in self._metadata_manager._fields: - if name == "units": - # This was already included in the header line - continue - val = getattr(self, name, None) - if isinstance(val, Container): - # Don't print empty containers, like attributes={} - show = bool(val) - else: - # Don't print properties when not present, or set to None, - # or False. - # This works OK as long as we are happy to treat all - # boolean properties as 'off' when False : Which happens to - # work for all those defined so far. - show = val is not None and val is not False - if show: - if name == "attributes": - # Use a multi-line form for this. - add_output(newline_indent) - add_output("attributes:", section="attributes") - max_attname_len = max(len(attr) for attr in val.keys()) - for attrname, attrval in val.items(): - attrname = attrname.ljust(max_attname_len) - if isinstance(attrval, str): - # quote strings - attrval = repr(attrval) - # and abbreviate really long ones - attrval = iris.util.clip_string(attrval) - attr_string = f"{attrname} {attrval}" - add_output(newline_indent + indent + attr_string) - else: - # add a one-line section for this property - # (aka metadata field) - add_output(newline_indent) - add_output(f"{name}: {val!r}", section=name) - - return "\n".join(output_lines) + bounds_vals = self._str_dates(self.bounds) + bounds = ", bounds={vals}".format(vals=bounds_vals) + result = fmt.format( + self=self, + cls=type(self).__name__, + values=values, + bounds=bounds, + other_metadata=self._repr_other_metadata(), + ) + else: + result = repr(self) - def __str__(self): - return self.summary() + return result def __repr__(self): - return self.summary(shorten=True) + # Note: this method includes bounds handling code, but it only runs + # within Coord type instances, as only these allow bounds to be set. + fmt = ( + "{cls}({self._values!r}{bounds}" + ", standard_name={self.standard_name!r}, units={self.units!r}" + "{other_metadata})" + ) + bounds = "" + # if coordinate, handle the bounds + if self.has_bounds(): + bounds = ", bounds=" + repr(self.bounds) + result = fmt.format( + self=self, + cls=type(self).__name__, + bounds=bounds, + other_metadata=self._repr_other_metadata(), + ) + return result def __eq__(self, other): # Note: this method includes bounds handling code, but it only runs @@ -1119,6 +861,23 @@ def measure(self, measure): raise ValueError(emsg) self._metadata_manager.measure = measure + def __str__(self): + result = repr(self) + return result + + def __repr__(self): + fmt = ( + "{cls}({self.data!r}, " + "measure={self.measure!r}, standard_name={self.standard_name!r}, " + "units={self.units!r}{other_metadata})" + ) + result = fmt.format( + self=self, + cls=type(self).__name__, + other_metadata=self._repr_other_metadata(), + ) + return result + def cube_dims(self, cube): """ Return the cube dimensions of this CellMeasure. @@ -1544,8 +1303,6 @@ class Coord(_DimensionalMetadata): """ - _values_array_name = "points" - @abstractmethod def __init__( self, @@ -1846,6 +1603,14 @@ def has_lazy_bounds(self): result = self._bounds_dm.has_lazy_data() return result + def _repr_other_metadata(self): + result = super()._repr_other_metadata() + if self.coord_system: + result += ", coord_system={}".format(self.coord_system) + if self.climatological: + result += ", climatological={}".format(self.climatological) + return result + # Must supply __hash__ as Python 3 does not enable it if __eq__ is defined. # NOTE: Violates "objects which compare equal must have the same hash". # We ought to remove this, as equality of two coords can *change*, so they @@ -2747,6 +2512,12 @@ def collapsed(self, dims_to_collapse=None): coord.circular = False return coord + def _repr_other_metadata(self): + result = Coord._repr_other_metadata(self) + if self.circular: + result += ", circular=%r" % self.circular + return result + def _new_points_requirements(self, points): """ Confirm that a new set of coord points adheres to the requirements for diff --git a/lib/iris/cube.py b/lib/iris/cube.py index b456bd9663..90acc021bc 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -745,7 +745,7 @@ class Cube(CFVariableMixin): mean over years time Attributes: STASH m01s16i203 - source 'Data from Met Office Unified Model' + source Data from Met Office Unified Model See the :doc:`user guide` for more information. @@ -1051,7 +1051,9 @@ def convert_units(self, unit): old_unit = self.units new_unit = unit - pointwise_convert = partial(old_unit.convert, other=new_unit) + # Define a delayed conversion operation (i.e. a callback). + def pointwise_convert(values): + return old_unit.convert(values, new_unit) new_data = _lazy.lazy_elementwise( self.lazy_data(), pointwise_convert @@ -1451,53 +1453,39 @@ def coord_dims(self, coord): The (name of the) coord to look for. """ - name_provided = False - if isinstance(coord, str): - # Forced to look-up the coordinate if we only have the name. - coord = self.coord(coord) - name_provided = True - - coord_id = id(coord) - - # Dimension of dimension coordinate by object id - dims_by_id = {id(c): (d,) for c, d in self._dim_coords_and_dims} - # Check for id match - faster than equality check - match = dims_by_id.get(coord_id) - - if match is None: - # Dimension/s of auxiliary coordinate by object id - aux_dims_by_id = {id(c): d for c, d in self._aux_coords_and_dims} - # Check for id match - faster than equality - match = aux_dims_by_id.get(coord_id) - if match is None: - dims_by_id.update(aux_dims_by_id) - - if match is None and not name_provided: - # We may have an equivalent coordinate but not the actual - # cube coordinate instance - so forced to perform coordinate - # lookup to attempt to retrieve it - coord = self.coord(coord) - # Check for id match - faster than equality - match = dims_by_id.get(id(coord)) - - # Search derived aux coordinates - if match is None: + + coord = self.coord(coord) + + # Search for existing coordinate (object) on the cube, faster lookup + # than equality - makes no functional difference. + matches = [ + (dim,) + for coord_, dim in self._dim_coords_and_dims + if coord_ is coord + ] + if not matches: + matches = [ + dims + for coord_, dims in self._aux_coords_and_dims + if coord_ is coord + ] + + # Search derived aux coords + if not matches: target_metadata = coord.metadata - def matcher(factory): + def match(factory): return factory.metadata == target_metadata - factories = filter(matcher, self._aux_factories) + factories = filter(match, self._aux_factories) matches = [ factory.derived_dims(self.coord_dims) for factory in factories ] - if matches: - match = matches[0] - if match is None: + if not matches: raise iris.exceptions.CoordinateNotFoundError(coord.name()) - return match + return matches[0] def cell_measure_dims(self, cell_measure): """ @@ -3650,7 +3638,7 @@ def collapsed(self, coords, aggregator, **kwargs): mean month, year mean longitude Attributes: - Conventions 'CF-1.5' + Conventions CF-1.5 STASH m01s00i024 @@ -3885,7 +3873,7 @@ def aggregated_by(self, coords, aggregator, **kwargs): mean month, year mean year Attributes: - Conventions 'CF-1.5' + Conventions CF-1.5 STASH m01s00i024 """ @@ -4090,8 +4078,8 @@ def rolling_window(self, coord, aggregator, window, **kwargs): Attributes: STASH m01s00i024 source \ -'Data from Met Office Unified Model' - um_version '7.6' +Data from Met Office Unified Model + um_version 7.6 >>> print(air_press.rolling_window('time', iris.analysis.MEAN, 3)) @@ -4116,8 +4104,8 @@ def rolling_window(self, coord, aggregator, window, **kwargs): Attributes: STASH m01s00i024 source \ -'Data from Met Office Unified Model' - um_version '7.6' +Data from Met Office Unified Model + um_version 7.6 Notice that the forecast_period dimension now represents the 4 possible windows of size 3 from the original cube. @@ -4248,7 +4236,7 @@ def interpolate(self, sample_points, scheme, collapse_scalar=True): dates or times may optionally be supplied as datetime.datetime or cftime.datetime instances. * scheme: - An instance of the type of interpolation to use to interpolate from this + The type of interpolation to use to interpolate from this :class:`~iris.cube.Cube` to the given sample points. The interpolation schemes currently available in Iris are: @@ -4277,11 +4265,8 @@ def interpolate(self, sample_points, scheme, collapse_scalar=True): air_potential_temperature / (K) \ (time: 3; model_level_number: 7; grid_latitude: 204; grid_longitude: 187) >>> print(cube.coord('time')) - DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) - points: [2009-11-19 10:00:00, 2009-11-19 11:00:00, 2009-11-19 12:00:00] - shape: (3,) - dtype: float64 - standard_name: 'time' + DimCoord([2009-11-19 10:00:00, 2009-11-19 11:00:00, \ +2009-11-19 12:00:00], standard_name='time', calendar='gregorian') >>> print(cube.coord('time').points) [349618. 349619. 349620.] >>> samples = [('time', 349618.5)] @@ -4290,11 +4275,8 @@ def interpolate(self, sample_points, scheme, collapse_scalar=True): air_potential_temperature / (K) \ (model_level_number: 7; grid_latitude: 204; grid_longitude: 187) >>> print(result.coord('time')) - DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) - points: [2009-11-19 10:30:00] - shape: (1,) - dtype: float64 - standard_name: 'time' + DimCoord([2009-11-19 10:30:00], standard_name='time', \ +calendar='gregorian') >>> print(result.coord('time').points) [349618.5] >>> # For datetime-like coordinates, we can also use @@ -4305,11 +4287,8 @@ def interpolate(self, sample_points, scheme, collapse_scalar=True): air_potential_temperature / (K) \ (model_level_number: 7; grid_latitude: 204; grid_longitude: 187) >>> print(result2.coord('time')) - DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) - points: [2009-11-19 10:30:00] - shape: (1,) - dtype: float64 - standard_name: 'time' + DimCoord([2009-11-19 10:30:00], standard_name='time', \ +calendar='gregorian') >>> print(result2.coord('time').points) [349618.5] >>> print(result == result2) @@ -4330,7 +4309,7 @@ def regrid(self, grid, scheme): * grid: A :class:`~iris.cube.Cube` that defines the target grid. * scheme: - An instance of the type of regridding to use to regrid this cube onto the + The type of regridding to use to regrid this cube onto the target grid. The regridding schemes in Iris currently include: * :class:`iris.analysis.Linear`\*, diff --git a/lib/iris/experimental/equalise_cubes.py b/lib/iris/experimental/equalise_cubes.py new file mode 100644 index 0000000000..8be7175067 --- /dev/null +++ b/lib/iris/experimental/equalise_cubes.py @@ -0,0 +1,30 @@ +# 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. +""" +Experimental cube-adjusting functions to assist merge operations. + +""" + + +def equalise_attributes(cubes): + """ + Delete cube attributes that are not identical over all cubes in a group. + + .. warning:: + + This function is now **disabled**. + + The functionality has been moved to + :func:`iris.util.equalise_attributes`. + + """ + old = "iris.experimental.equalise_cubes.equalise_attributes" + new = "iris.util.equalise_attributes" + emsg = ( + f'The function "{old}" has been moved.\n' + f'Please replace "{old}()" with "{new}()".' + ) + raise Exception(emsg) diff --git a/lib/iris/experimental/raster.py b/lib/iris/experimental/raster.py index 7c95293469..83fd761973 100644 --- a/lib/iris/experimental/raster.py +++ b/lib/iris/experimental/raster.py @@ -20,17 +20,8 @@ from osgeo import gdal, osr import iris -from iris._deprecation import warn_deprecated import iris.coord_systems -wmsg = ( - "iris.experimental.raster is deprecated since version 3.2, and will be " - "removed in a future release. If you make use of this functionality, " - "please contact the Iris Developers to discuss how to retain it (which may " - "involve reversing the deprecation)." -) -warn_deprecated(wmsg) - _GDAL_DATATYPES = { "i2": gdal.GDT_Int16, "i4": gdal.GDT_Int32, @@ -105,14 +96,6 @@ def export_geotiff(cube, fname): """ Writes cube data to raster file format as a PixelIsArea GeoTiff image. - .. deprecated:: 3.2.0 - - This method is scheduled to be removed in a future release, and no - replacement is currently planned. - If you make use of this functionality, please contact the Iris - Developers to discuss how to retain it (which could include reversing - the deprecation). - Args: * cube (Cube): The 2D regularly gridded cube slice to be exported. The cube must have regular, contiguous bounds. @@ -124,13 +107,6 @@ def export_geotiff(cube, fname): http://www.remotesensing.org/geotiff/spec/geotiff2.5.html#2.5.2.2 """ - wmsg = ( - "iris.experimental.raster.export_geotiff has been deprecated, and will " - "be removed in a future release. Please consult the docstring for " - "details." - ) - warn_deprecated(wmsg) - if cube.ndim != 2: raise ValueError("The cube must be two dimensional.") diff --git a/lib/iris/experimental/regrid.py b/lib/iris/experimental/regrid.py index 7c5d8e99cc..9a02fbd3b1 100644 --- a/lib/iris/experimental/regrid.py +++ b/lib/iris/experimental/regrid.py @@ -6,37 +6,26 @@ """ Regridding functions. -.. note:: - - .. deprecated:: 3.2.0 - - This package will be removed in a future release. - The PointInCell class has now moved to :class:`iris.analysis.PointInCell`. - All the other content will be withdrawn. - - If you still use any of this, please contact the Iris Developers to - discuss how to replace it or to retain it. - """ + import copy import functools import warnings import cartopy.crs as ccrs +import cf_units import numpy as np +import numpy.ma as ma import scipy.interpolate -from iris._deprecation import warn_deprecated -from iris.analysis._area_weighted import ( - _regrid_area_weighted_rectilinear_src_and_grid__perform, - _regrid_area_weighted_rectilinear_src_and_grid__prepare, -) +from iris._lazy_data import map_complete_blocks from iris.analysis._interpolation import ( get_xy_coords, get_xy_dim_coords, snapshot_grid, ) from iris.analysis._regrid import ( + RectilinearRegridder, _regrid_weighted_curvilinear_to_rectilinear__perform, _regrid_weighted_curvilinear_to_rectilinear__prepare, ) @@ -45,38 +34,564 @@ import iris.cube from iris.util import _meshgrid -wmsg = ( - "The 'iris.experimental.regrid' package is deprecated since version 3.2, " - "and will be removed in a future release. The PointInCell class has now " - "moved into iris.analysis. All its other content will be withdrawn. " - "If you still use any of this, please contact the Iris Developers to " - "discuss how to replace it or to retain it (reverse the deprecation)." -) -warn_deprecated(wmsg) +def _get_xy_coords(cube): + """ + Return the x and y coordinates from a cube. -def regrid_area_weighted_rectilinear_src_and_grid( - src_cube, grid_cube, mdtol=0 + This function will preferentially return a pair of dimension + coordinates (if there are more than one potential x or y dimension + coordinates a ValueError will be raised). If the cube does not have + a pair of x and y dimension coordinates it will return 1D auxiliary + coordinates (including scalars). If there is not one and only one set + of x and y auxiliary coordinates a ValueError will be raised. + + Having identified the x and y coordinates, the function checks that they + have equal coordinate systems and that they do not occupy the same + dimension on the cube. + + Args: + + * cube: + An instance of :class:`iris.cube.Cube`. + + Returns: + A tuple containing the cube's x and y coordinates. + + """ + # Look for a suitable dimension coords first. + x_coords = cube.coords(axis="x", dim_coords=True) + if not x_coords: + # If there is no x coord in dim_coords look for scalars or + # monotonic coords in aux_coords. + x_coords = [ + coord + for coord in cube.coords(axis="x", dim_coords=False) + if coord.ndim == 1 and coord.is_monotonic() + ] + if len(x_coords) != 1: + raise ValueError( + "Cube {!r} must contain a single 1D x " + "coordinate.".format(cube.name()) + ) + x_coord = x_coords[0] + + # Look for a suitable dimension coords first. + y_coords = cube.coords(axis="y", dim_coords=True) + if not y_coords: + # If there is no y coord in dim_coords look for scalars or + # monotonic coords in aux_coords. + y_coords = [ + coord + for coord in cube.coords(axis="y", dim_coords=False) + if coord.ndim == 1 and coord.is_monotonic() + ] + if len(y_coords) != 1: + raise ValueError( + "Cube {!r} must contain a single 1D y " + "coordinate.".format(cube.name()) + ) + y_coord = y_coords[0] + + if x_coord.coord_system != y_coord.coord_system: + raise ValueError( + "The cube's x ({!r}) and y ({!r}) " + "coordinates must have the same coordinate " + "system.".format(x_coord.name(), y_coord.name()) + ) + + # The x and y coordinates must describe different dimensions + # or be scalar coords. + x_dims = cube.coord_dims(x_coord) + x_dim = None + if x_dims: + x_dim = x_dims[0] + + y_dims = cube.coord_dims(y_coord) + y_dim = None + if y_dims: + y_dim = y_dims[0] + + if x_dim is not None and y_dim == x_dim: + raise ValueError( + "The cube's x and y coords must not describe the " + "same data dimension." + ) + + return x_coord, y_coord + + +def _within_bounds(src_bounds, tgt_bounds, orderswap=False): + """ + Determine which target bounds lie within the extremes of the source bounds. + + Args: + + * src_bounds (ndarray): + An (n, 2) shaped array of monotonic contiguous source bounds. + * tgt_bounds (ndarray): + An (n, 2) shaped array corresponding to the target bounds. + + Kwargs: + + * orderswap (bool): + A Boolean indicating whether the target bounds are in descending order + (True). Defaults to False. + + Returns: + Boolean ndarray, indicating whether each target bound is within the + extremes of the source bounds. + + """ + min_bound = np.min(src_bounds) - 1e-14 + max_bound = np.max(src_bounds) + 1e-14 + + # Swap upper-lower is necessary. + if orderswap is True: + upper, lower = tgt_bounds.T + else: + lower, upper = tgt_bounds.T + + return ((lower <= max_bound) * (lower >= min_bound)) * ( + (upper <= max_bound) * (upper >= min_bound) + ) + + +def _cropped_bounds(bounds, lower, upper): + """ + Return a new bounds array and corresponding slice object (or indices) of + the original data array, resulting from cropping the provided bounds + between the specified lower and upper values. The bounds at the + extremities will be truncated so that they start and end with lower and + upper. + + This function will return an empty NumPy array and slice if there is no + overlap between the region covered by bounds and the region from lower to + upper. + + If lower > upper the resulting bounds may not be contiguous and the + indices object will be a tuple of indices rather than a slice object. + + Args: + + * bounds: + An (n, 2) shaped array of monotonic contiguous bounds. + * lower: + Lower bound at which to crop the bounds array. + * upper: + Upper bound at which to crop the bounds array. + + Returns: + A tuple of the new bounds array and the corresponding slice object or + indices from the zeroth axis of the original array. + + """ + reversed_flag = False + # Ensure order is increasing. + if bounds[0, 0] > bounds[-1, 0]: + # Reverse bounds + bounds = bounds[::-1, ::-1] + reversed_flag = True + + # Number of bounds. + n = bounds.shape[0] + + if lower <= upper: + if lower > bounds[-1, 1] or upper < bounds[0, 0]: + new_bounds = bounds[0:0] + indices = slice(0, 0) + else: + # A single region lower->upper. + if lower < bounds[0, 0]: + # Region extends below bounds so use first lower bound. + lindex = 0 + lower = bounds[0, 0] + else: + # Index of last lower bound less than or equal to lower. + lindex = np.nonzero(bounds[:, 0] <= lower)[0][-1] + if upper > bounds[-1, 1]: + # Region extends above bounds so use last upper bound. + uindex = n - 1 + upper = bounds[-1, 1] + else: + # Index of first upper bound greater than or equal to + # upper. + uindex = np.nonzero(bounds[:, 1] >= upper)[0][0] + # Extract the bounds in our region defined by lower->upper. + new_bounds = np.copy(bounds[lindex : (uindex + 1), :]) + # Replace first and last values with specified bounds. + new_bounds[0, 0] = lower + new_bounds[-1, 1] = upper + if reversed_flag: + indices = slice(n - (uindex + 1), n - lindex) + else: + indices = slice(lindex, uindex + 1) + else: + # Two regions [0]->upper, lower->[-1] + # [0]->upper + if upper < bounds[0, 0]: + # Region outside src bounds. + new_bounds_left = bounds[0:0] + indices_left = tuple() + slice_left = slice(0, 0) + else: + if upper > bounds[-1, 1]: + # Whole of bounds. + uindex = n - 1 + upper = bounds[-1, 1] + else: + # Index of first upper bound greater than or equal to upper. + uindex = np.nonzero(bounds[:, 1] >= upper)[0][0] + # Extract the bounds in our region defined by [0]->upper. + new_bounds_left = np.copy(bounds[0 : (uindex + 1), :]) + # Replace last value with specified bound. + new_bounds_left[-1, 1] = upper + if reversed_flag: + indices_left = tuple(range(n - (uindex + 1), n)) + slice_left = slice(n - (uindex + 1), n) + else: + indices_left = tuple(range(0, uindex + 1)) + slice_left = slice(0, uindex + 1) + # lower->[-1] + if lower > bounds[-1, 1]: + # Region is outside src bounds. + new_bounds_right = bounds[0:0] + indices_right = tuple() + slice_right = slice(0, 0) + else: + if lower < bounds[0, 0]: + # Whole of bounds. + lindex = 0 + lower = bounds[0, 0] + else: + # Index of last lower bound less than or equal to lower. + lindex = np.nonzero(bounds[:, 0] <= lower)[0][-1] + # Extract the bounds in our region defined by lower->[-1]. + new_bounds_right = np.copy(bounds[lindex:, :]) + # Replace first value with specified bound. + new_bounds_right[0, 0] = lower + if reversed_flag: + indices_right = tuple(range(0, n - lindex)) + slice_right = slice(0, n - lindex) + else: + indices_right = tuple(range(lindex, n)) + slice_right = slice(lindex, None) + + if reversed_flag: + # Flip everything around. + indices_left, indices_right = indices_right, indices_left + slice_left, slice_right = slice_right, slice_left + + # Combine regions. + new_bounds = np.concatenate((new_bounds_left, new_bounds_right)) + # Use slices if possible, but if we have two regions use indices. + if indices_left and indices_right: + indices = indices_left + indices_right + elif indices_left: + indices = slice_left + elif indices_right: + indices = slice_right + else: + indices = slice(0, 0) + + if reversed_flag: + new_bounds = new_bounds[::-1, ::-1] + + return new_bounds, indices + + +def _cartesian_area(y_bounds, x_bounds): + """ + Return an array of the areas of each cell given two arrays + of cartesian bounds. + + Args: + + * y_bounds: + An (n, 2) shaped NumPy array. + * x_bounds: + An (m, 2) shaped NumPy array. + + Returns: + An (n, m) shaped Numpy array of areas. + + """ + heights = y_bounds[:, 1] - y_bounds[:, 0] + widths = x_bounds[:, 1] - x_bounds[:, 0] + return np.abs(np.outer(heights, widths)) + + +def _spherical_area(y_bounds, x_bounds, radius=1.0): + """ + Return an array of the areas of each cell on a sphere + given two arrays of latitude and longitude bounds in radians. + + Args: + + * y_bounds: + An (n, 2) shaped NumPy array of latitide bounds in radians. + * x_bounds: + An (m, 2) shaped NumPy array of longitude bounds in radians. + * radius: + Radius of the sphere. Default is 1.0. + + Returns: + An (n, m) shaped Numpy array of areas. + + """ + return iris.analysis.cartography._quadrant_area(y_bounds, x_bounds, radius) + + +def _get_bounds_in_units(coord, units, dtype): + """Return a copy of coord's bounds in the specified units and dtype.""" + # The bounds are cast to dtype before conversion to prevent issues when + # mixing float32 and float64 types. + return coord.units.convert(coord.bounds.astype(dtype), units).astype(dtype) + + +def _weighted_mean_with_mdtol(data, weights, axis=None, mdtol=0): + """ + Return the weighted mean of an array over the specified axis + using the provided weights (if any) and a permitted fraction of + masked data. + + Args: + + * data (array-like): + Data to be averaged. + + * weights (array-like): + An array of the same shape as the data that specifies the contribution + of each corresponding data element to the calculated mean. + + Kwargs: + + * axis (int or tuple of ints): + Axis along which the mean is computed. The default is to compute + the mean of the flattened array. + + * mdtol (float): + Tolerance of missing data. The value returned in each element of the + returned array will be masked if the fraction of masked data exceeds + mdtol. This fraction is weighted by the `weights` array if one is + provided. mdtol=0 means no missing data is tolerated + while mdtol=1 will mean the resulting element will be masked if and + only if all the contributing elements of data are masked. + Defaults to 0. + + Returns: + Numpy array (possibly masked) or scalar. + + """ + if ma.is_masked(data): + res, unmasked_weights_sum = ma.average( + data, weights=weights, axis=axis, returned=True + ) + if mdtol < 1: + weights_sum = weights.sum(axis=axis) + frac_masked = 1 - np.true_divide(unmasked_weights_sum, weights_sum) + mask_pt = frac_masked > mdtol + if np.any(mask_pt) and not isinstance(res, ma.core.MaskedConstant): + if np.isscalar(res): + res = ma.masked + elif ma.isMaskedArray(res): + res.mask |= mask_pt + else: + res = ma.masked_array(res, mask=mask_pt) + else: + res = np.average(data, weights=weights, axis=axis) + return res + + +def _regrid_area_weighted_array( + src_data, x_dim, y_dim, weights_info, index_info, mdtol=0 ): """ - Return a new cube with data values calculated using the area weighted - mean of data values from src_grid regridded onto the horizontal grid of - grid_cube. + Regrid the given data from its source grid to a new grid using + an area weighted mean to determine the resulting data values. .. note:: - .. deprecated:: 3.2.0 + Elements in the returned array that lie either partially + or entirely outside of the extent of the source grid will + be masked irrespective of the value of mdtol. - This function is scheduled to be removed in a future release. - Please use :meth:`iris.cube.Cube.regrid` with the - :class:`iris.analysis.AreaWeighted` scheme instead : this is an exact - replacement. + Args: - For example : + * src_data: + An N-dimensional NumPy array. + * x_dim: + The X dimension within `src_data`. + * y_dim: + The Y dimension within `src_data`. + * weights_info: + The area weights information to be used for area-weighted + regridding. - .. code:: + Kwargs: - result = src_cube.regrid(grid_cube, AreaWeighted()) + * mdtol: + Tolerance of missing data. The value returned in each element of the + returned array will be masked if the fraction of missing data exceeds + mdtol. This fraction is calculated based on the area of masked cells + within each target cell. mdtol=0 means no missing data is tolerated + while mdtol=1 will mean the resulting element will be masked if and + only if all the overlapping elements of the source grid are masked. + Defaults to 0. + + Returns: + The regridded data as an N-dimensional NumPy array. The lengths + of the X and Y dimensions will now match those of the target + grid. + + """ + ( + blank_weights, + src_area_weights, + new_data_mask_basis, + ) = weights_info + + ( + result_x_extent, + result_y_extent, + square_data_indices_y, + square_data_indices_x, + src_area_datas_required, + ) = index_info + + # Ensure we have x_dim and y_dim. + x_dim_orig = x_dim + y_dim_orig = y_dim + if y_dim is None: + src_data = np.expand_dims(src_data, axis=src_data.ndim) + y_dim = src_data.ndim - 1 + if x_dim is None: + src_data = np.expand_dims(src_data, axis=src_data.ndim) + x_dim = src_data.ndim - 1 + # Move y_dim and x_dim to last dimensions + if not x_dim == src_data.ndim - 1: + src_data = np.moveaxis(src_data, x_dim, -1) + if not y_dim == src_data.ndim - 2: + if x_dim < y_dim: + # note: y_dim was shifted along by one position when + # x_dim was moved to the last dimension + src_data = np.moveaxis(src_data, y_dim - 1, -2) + elif x_dim > y_dim: + src_data = np.moveaxis(src_data, y_dim, -2) + x_dim = src_data.ndim - 1 + y_dim = src_data.ndim - 2 + + # Create empty "pre-averaging" data array that will enable the + # src_data data coresponding to a given target grid point, + # to be stacked per point. + # Note that dtype is not preserved and that the array mask + # allows for regions that do not overlap. + new_shape = list(src_data.shape) + new_shape[x_dim] = result_x_extent + new_shape[y_dim] = result_y_extent + + # Use input cube dtype or convert values to the smallest possible float + # dtype when necessary. + dtype = np.promote_types(src_data.dtype, np.float16) + + # Axes of data over which the weighted mean is calculated. + axis = (y_dim, x_dim) + + # Use previously established indices + + src_area_datas_square = src_data[ + ..., square_data_indices_y, square_data_indices_x + ] + + _, src_area_datas_required = np.broadcast_arrays( + src_area_datas_square, src_area_datas_required + ) + + src_area_datas = np.where( + src_area_datas_required, src_area_datas_square, 0 + ) + + # Flag to indicate whether the original data was a masked array. + src_masked = src_data.mask.any() if ma.isMaskedArray(src_data) else False + if src_masked: + src_area_masks_square = src_data.mask[ + ..., square_data_indices_y, square_data_indices_x + ] + src_area_masks = np.where( + src_area_datas_required, src_area_masks_square, True + ) + + else: + # If the weights were originally blank, set the weights to all 1 to + # avoid divide by 0 error and set the new data mask for making the + # values 0 + src_area_weights = np.where(blank_weights, 1, src_area_weights) + + new_data_mask = np.broadcast_to(new_data_mask_basis, new_shape) + + # Broadcast the weights array to allow numpy's ma.average + # to be called. + # Assign new shape to raise error on copy. + src_area_weights.shape = src_area_datas.shape[-3:] + # Broadcast weights to match shape of data. + _, src_area_weights = np.broadcast_arrays(src_area_datas, src_area_weights) + + # Mask the data points + if src_masked: + src_area_datas = np.ma.array(src_area_datas, mask=src_area_masks) + + # Calculate weighted mean taking into account missing data. + new_data = _weighted_mean_with_mdtol( + src_area_datas, weights=src_area_weights, axis=axis, mdtol=mdtol + ) + new_data = new_data.reshape(new_shape) + if src_masked: + new_data_mask = new_data.mask + + # Mask the data if originally masked or if the result has masked points + if ma.isMaskedArray(src_data): + new_data = ma.array( + new_data, + mask=new_data_mask, + fill_value=src_data.fill_value, + dtype=dtype, + ) + elif new_data_mask.any(): + new_data = ma.array(new_data, mask=new_data_mask, dtype=dtype) + else: + new_data = new_data.astype(dtype) + + # Restore data to original form + if x_dim_orig is None and y_dim_orig is None: + new_data = np.squeeze(new_data, axis=x_dim) + new_data = np.squeeze(new_data, axis=y_dim) + elif y_dim_orig is None: + new_data = np.squeeze(new_data, axis=y_dim) + new_data = np.moveaxis(new_data, -1, x_dim_orig) + elif x_dim_orig is None: + new_data = np.squeeze(new_data, axis=x_dim) + new_data = np.moveaxis(new_data, -1, y_dim_orig) + elif x_dim_orig < y_dim_orig: + # move the x_dim back first, so that the y_dim will + # then be moved to its original position + new_data = np.moveaxis(new_data, -1, x_dim_orig) + new_data = np.moveaxis(new_data, -1, y_dim_orig) + else: + # move the y_dim back first, so that the x_dim will + # then be moved to its original position + new_data = np.moveaxis(new_data, -2, y_dim_orig) + new_data = np.moveaxis(new_data, -1, x_dim_orig) + + return new_data + + +def regrid_area_weighted_rectilinear_src_and_grid( + src_cube, grid_cube, mdtol=0 +): + """ + Return a new cube with data values calculated using the area weighted + mean of data values from src_grid regridded onto the horizontal grid of + grid_cube. This function requires that the horizontal grids of both cubes are rectilinear (i.e. expressed in terms of two orthogonal 1D coordinates) @@ -114,15 +629,6 @@ def regrid_area_weighted_rectilinear_src_and_grid( A new :class:`iris.cube.Cube` instance. """ - wmsg = ( - "The function " - "'iris.experimental.regrid." - "regrid_area_weighted_rectilinear_src_and_grid' " - "has been deprecated, and will be removed in a future release. " - "Please consult the docstring for details." - ) - warn_deprecated(wmsg) - regrid_info = _regrid_area_weighted_rectilinear_src_and_grid__prepare( src_cube, grid_cube ) @@ -132,26 +638,460 @@ def regrid_area_weighted_rectilinear_src_and_grid( return result -def regrid_weighted_curvilinear_to_rectilinear(src_cube, weights, grid_cube): - r""" - Return a new cube with the data values calculated using the weighted - mean of data values from :data:`src_cube` and the weights from - :data:`weights` regridded onto the horizontal grid of :data:`grid_cube`. +def _regrid_area_weighted_rectilinear_src_and_grid__prepare( + src_cube, grid_cube +): + """ + First (setup) part of 'regrid_area_weighted_rectilinear_src_and_grid'. + + Check inputs and calculate related info. The 'regrid info' returned + can be re-used over many 2d slices. + + """ + # Get the 1d monotonic (or scalar) src and grid coordinates. + src_x, src_y = _get_xy_coords(src_cube) + grid_x, grid_y = _get_xy_coords(grid_cube) + + # Condition 1: All x and y coordinates must have contiguous bounds to + # define areas. + if ( + not src_x.is_contiguous() + or not src_y.is_contiguous() + or not grid_x.is_contiguous() + or not grid_y.is_contiguous() + ): + raise ValueError( + "The horizontal grid coordinates of both the source " + "and grid cubes must have contiguous bounds." + ) + + # Condition 2: Everything must have the same coordinate system. + src_cs = src_x.coord_system + grid_cs = grid_x.coord_system + if src_cs != grid_cs: + raise ValueError( + "The horizontal grid coordinates of both the source " + "and grid cubes must have the same coordinate " + "system." + ) + + # Condition 3: cannot create vector coords from scalars. + src_x_dims = src_cube.coord_dims(src_x) + src_x_dim = None + if src_x_dims: + src_x_dim = src_x_dims[0] + src_y_dims = src_cube.coord_dims(src_y) + src_y_dim = None + if src_y_dims: + src_y_dim = src_y_dims[0] + if ( + src_x_dim is None + and grid_x.shape[0] != 1 + or src_y_dim is None + and grid_y.shape[0] != 1 + ): + raise ValueError( + "The horizontal grid coordinates of source cube " + "includes scalar coordinates, but the new grid does " + "not. The new grid must not require additional data " + "dimensions to be created." + ) + + # Determine whether to calculate flat or spherical areas. + # Don't only rely on coord system as it may be None. + spherical = ( + isinstance( + src_cs, + (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS), + ) + or src_x.units == "degrees" + or src_x.units == "radians" + ) + + # Get src and grid bounds in the same units. + x_units = cf_units.Unit("radians") if spherical else src_x.units + y_units = cf_units.Unit("radians") if spherical else src_y.units + + # Operate in highest precision. + src_dtype = np.promote_types(src_x.bounds.dtype, src_y.bounds.dtype) + grid_dtype = np.promote_types(grid_x.bounds.dtype, grid_y.bounds.dtype) + dtype = np.promote_types(src_dtype, grid_dtype) + + src_x_bounds = _get_bounds_in_units(src_x, x_units, dtype) + src_y_bounds = _get_bounds_in_units(src_y, y_units, dtype) + grid_x_bounds = _get_bounds_in_units(grid_x, x_units, dtype) + grid_y_bounds = _get_bounds_in_units(grid_y, y_units, dtype) + + # Create 2d meshgrids as required by _create_cube func. + meshgrid_x, meshgrid_y = _meshgrid(grid_x.points, grid_y.points) + + # Determine whether target grid bounds are decreasing. This must + # be determined prior to wrap_lons being called. + grid_x_decreasing = grid_x_bounds[-1, 0] < grid_x_bounds[0, 0] + grid_y_decreasing = grid_y_bounds[-1, 0] < grid_y_bounds[0, 0] + + # Wrapping of longitudes. + if spherical: + base = np.min(src_x_bounds) + modulus = x_units.modulus + # Only wrap if necessary to avoid introducing floating + # point errors. + if np.min(grid_x_bounds) < base or np.max(grid_x_bounds) > ( + base + modulus + ): + grid_x_bounds = iris.analysis.cartography.wrap_lons( + grid_x_bounds, base, modulus + ) + + # Determine whether the src_x coord has periodic boundary conditions. + circular = getattr(src_x, "circular", False) + + # Use simple cartesian area function or one that takes into + # account the curved surface if coord system is spherical. + if spherical: + area_func = _spherical_area + else: + area_func = _cartesian_area + + def _calculate_regrid_area_weighted_weights( + src_x_bounds, + src_y_bounds, + grid_x_bounds, + grid_y_bounds, + grid_x_decreasing, + grid_y_decreasing, + area_func, + circular=False, + ): + """ + Compute the area weights used for area-weighted regridding. + Args: + * src_x_bounds: + A NumPy array of bounds along the X axis defining the source grid. + * src_y_bounds: + A NumPy array of bounds along the Y axis defining the source grid. + * grid_x_bounds: + A NumPy array of bounds along the X axis defining the new grid. + * grid_y_bounds: + A NumPy array of bounds along the Y axis defining the new grid. + * grid_x_decreasing: + Boolean indicating whether the X coordinate of the new grid is + in descending order. + * grid_y_decreasing: + Boolean indicating whether the Y coordinate of the new grid is + in descending order. + * area_func: + A function that returns an (p, q) array of weights given an (p, 2) + shaped array of Y bounds and an (q, 2) shaped array of X bounds. + Kwargs: + * circular: + A boolean indicating whether the `src_x_bounds` are periodic. + Default is False. + Returns: + The area weights to be used for area-weighted regridding. + """ + # Determine which grid bounds are within src extent. + y_within_bounds = _within_bounds( + src_y_bounds, grid_y_bounds, grid_y_decreasing + ) + x_within_bounds = _within_bounds( + src_x_bounds, grid_x_bounds, grid_x_decreasing + ) + + # Cache which src_bounds are within grid bounds + cached_x_bounds = [] + cached_x_indices = [] + max_x_indices = 0 + for (x_0, x_1) in grid_x_bounds: + if grid_x_decreasing: + x_0, x_1 = x_1, x_0 + x_bounds, x_indices = _cropped_bounds(src_x_bounds, x_0, x_1) + cached_x_bounds.append(x_bounds) + cached_x_indices.append(x_indices) + # Keep record of the largest slice + if isinstance(x_indices, slice): + x_indices_size = np.sum(x_indices.stop - x_indices.start) + else: # is tuple of indices + x_indices_size = len(x_indices) + if x_indices_size > max_x_indices: + max_x_indices = x_indices_size + + # Cache which y src_bounds areas and weights are within grid bounds + cached_y_indices = [] + cached_weights = [] + max_y_indices = 0 + for j, (y_0, y_1) in enumerate(grid_y_bounds): + # Reverse lower and upper if dest grid is decreasing. + if grid_y_decreasing: + y_0, y_1 = y_1, y_0 + y_bounds, y_indices = _cropped_bounds(src_y_bounds, y_0, y_1) + cached_y_indices.append(y_indices) + # Keep record of the largest slice + if isinstance(y_indices, slice): + y_indices_size = np.sum(y_indices.stop - y_indices.start) + else: # is tuple of indices + y_indices_size = len(y_indices) + if y_indices_size > max_y_indices: + max_y_indices = y_indices_size + + weights_i = [] + for i, (x_0, x_1) in enumerate(grid_x_bounds): + # Reverse lower and upper if dest grid is decreasing. + if grid_x_decreasing: + x_0, x_1 = x_1, x_0 + x_bounds = cached_x_bounds[i] + x_indices = cached_x_indices[i] + + # Determine whether element i, j overlaps with src and hence + # an area weight should be computed. + # If x_0 > x_1 then we want [0]->x_1 and x_0->[0] + mod in the case + # of wrapped longitudes. However if the src grid is not global + # (i.e. circular) this new cell would include a region outside of + # the extent of the src grid and thus the weight is therefore + # invalid. + outside_extent = x_0 > x_1 and not circular + if ( + outside_extent + or not y_within_bounds[j] + or not x_within_bounds[i] + ): + weights = False + else: + # Calculate weights based on areas of cropped bounds. + if isinstance(x_indices, tuple) and isinstance( + y_indices, tuple + ): + raise RuntimeError( + "Cannot handle split bounds " "in both x and y." + ) + weights = area_func(y_bounds, x_bounds) + weights_i.append(weights) + cached_weights.append(weights_i) + return ( + tuple(cached_x_indices), + tuple(cached_y_indices), + max_x_indices, + max_y_indices, + tuple(cached_weights), + ) + + ( + cached_x_indices, + cached_y_indices, + max_x_indices, + max_y_indices, + cached_weights, + ) = _calculate_regrid_area_weighted_weights( + src_x_bounds, + src_y_bounds, + grid_x_bounds, + grid_y_bounds, + grid_x_decreasing, + grid_y_decreasing, + area_func, + circular, + ) + + # Go further, calculating the full weights array that we'll need in the + # perform step and the indices we'll need to extract from the cube we're + # regridding (src_data) + + result_y_extent = len(grid_y_bounds) + result_x_extent = len(grid_x_bounds) + + # Total number of points + num_target_pts = result_y_extent * result_x_extent + + # Create empty array to hold weights + src_area_weights = np.zeros( + list((max_y_indices, max_x_indices, num_target_pts)) + ) + + # Built for the case where the source cube isn't masked + blank_weights = np.zeros((num_target_pts,)) + new_data_mask_basis = np.full( + (len(cached_y_indices), len(cached_x_indices)), False, dtype=np.bool_ + ) + + # To permit fancy indexing, we need to store our data in an array whose + # first two dimensions represent the indices needed for the target cell. + # Since target cells can require a different number of indices, the size of + # these dimensions should be the maximum of this number. + # This means we need to track whether the data in + # that array is actually required and build those squared-off arrays + # TODO: Consider if a proper mask would be better + src_area_datas_required = np.full( + (max_y_indices, max_x_indices, num_target_pts), False + ) + square_data_indices_y = np.zeros( + (max_y_indices, max_x_indices, num_target_pts), dtype=int + ) + square_data_indices_x = np.zeros( + (max_y_indices, max_x_indices, num_target_pts), dtype=int + ) - .. note :: + # Stack the weights for each target point and build the indices we'll need + # to extract the src_area_data + target_pt_ji = -1 + for j, y_indices in enumerate(cached_y_indices): + for i, x_indices in enumerate(cached_x_indices): + target_pt_ji += 1 + # Determine whether to mask element i, j based on whether + # there are valid weights. + weights = cached_weights[j][i] + if weights is False: + # Prepare for the src_data not being masked by storing the + # information that will let us fill the data with zeros and + # weights as one. The weighted average result will be the same, + # but we avoid dividing by zero. + blank_weights[target_pt_ji] = True + new_data_mask_basis[j, i] = True + else: + # Establish which indices are actually in y_indices and x_indices + if isinstance(y_indices, slice): + y_indices = list( + range( + y_indices.start, + y_indices.stop, + y_indices.step or 1, + ) + ) + else: + y_indices = list(y_indices) + + if isinstance(x_indices, slice): + x_indices = list( + range( + x_indices.start, + x_indices.stop, + x_indices.step or 1, + ) + ) + else: + x_indices = list(x_indices) + + # For the weights, we just need the lengths of these as we're + # dropping them into a pre-made array + + len_y = len(y_indices) + len_x = len(x_indices) + + src_area_weights[0:len_y, 0:len_x, target_pt_ji] = weights + + # To build the indices for the source cube, we need equal + # shaped array so we pad with 0s and record the need to mask + # them in src_area_datas_required + padded_y_indices = y_indices + [0] * (max_y_indices - len_y) + padded_x_indices = x_indices + [0] * (max_x_indices - len_x) + + square_data_indices_y[..., target_pt_ji] = np.array( + padded_y_indices + )[:, np.newaxis] + square_data_indices_x[..., target_pt_ji] = padded_x_indices - .. deprecated:: 3.2.0 + src_area_datas_required[0:len_y, 0:len_x, target_pt_ji] = True - This function is scheduled to be removed in a future release. - Please use :meth:`iris.cube.Cube.regrid` with the - :class:`iris.analysis.PointInCell` scheme instead : this is an exact - replacement. + # Package up the return data - For example : + weights_info = ( + blank_weights, + src_area_weights, + new_data_mask_basis, + ) + + index_info = ( + result_x_extent, + result_y_extent, + square_data_indices_y, + square_data_indices_x, + src_area_datas_required, + ) - .. code:: + # Now return it + + return ( + src_x, + src_y, + src_x_dim, + src_y_dim, + grid_x, + grid_y, + meshgrid_x, + meshgrid_y, + weights_info, + index_info, + ) - result = src_cube.regrid(grid_cube, PointInCell()) + +def _regrid_area_weighted_rectilinear_src_and_grid__perform( + src_cube, regrid_info, mdtol +): + """ + Second (regrid) part of 'regrid_area_weighted_rectilinear_src_and_grid'. + + Perform the prepared regrid calculation on a single 2d cube. + + """ + ( + src_x, + src_y, + src_x_dim, + src_y_dim, + grid_x, + grid_y, + meshgrid_x, + meshgrid_y, + weights_info, + index_info, + ) = regrid_info + + # Calculate new data array for regridded cube. + regrid = functools.partial( + _regrid_area_weighted_array, + x_dim=src_x_dim, + y_dim=src_y_dim, + weights_info=weights_info, + index_info=index_info, + mdtol=mdtol, + ) + + new_data = map_complete_blocks( + src_cube, regrid, (src_y_dim, src_x_dim), meshgrid_x.shape + ) + + # Wrap up the data as a Cube. + regrid_callback = RectilinearRegridder._regrid + new_cube = RectilinearRegridder._create_cube( + new_data, + src_cube, + src_x_dim, + src_y_dim, + src_x, + src_y, + grid_x, + grid_y, + meshgrid_x, + meshgrid_y, + regrid_callback, + ) + + # Slice out any length 1 dimensions. + indices = [slice(None, None)] * new_data.ndim + if src_x_dim is not None and new_cube.shape[src_x_dim] == 1: + indices[src_x_dim] = 0 + if src_y_dim is not None and new_cube.shape[src_y_dim] == 1: + indices[src_y_dim] = 0 + if 0 in indices: + new_cube = new_cube[tuple(indices)] + + return new_cube + + +def regrid_weighted_curvilinear_to_rectilinear(src_cube, weights, grid_cube): + r""" + Return a new cube with the data values calculated using the weighted + mean of data values from :data:`src_cube` and the weights from + :data:`weights` regridded onto the horizontal grid of :data:`grid_cube`. This function requires that the :data:`src_cube` has a horizontal grid defined by a pair of X- and Y-axis coordinates which are mapped over the @@ -194,14 +1134,6 @@ def regrid_weighted_curvilinear_to_rectilinear(src_cube, weights, grid_cube): A :class:`iris.cube.Cube` instance. """ - wmsg = ( - "The function " - "'iris.experimental.regrid." - "regrid_weighted_curvilinear_to_rectilinear' " - "has been deprecated, and will be removed in a future release. " - "Please consult the docstring for details." - ) - warn_deprecated(wmsg) regrid_info = _regrid_weighted_curvilinear_to_rectilinear__prepare( src_cube, weights, grid_cube ) @@ -640,16 +1572,6 @@ def __init__(self, projection=None): Linear regridding scheme that uses scipy.interpolate.griddata on projected unstructured data. - .. note:: - - .. deprecated:: 3.2.0 - - This class is scheduled to be removed in a future release, and no - replacement is currently planned. - If you make use of this functionality, please contact the Iris - Developers to discuss how to retain it (which could include - reversing the deprecation). - Optional Args: * projection: `cartopy.crs instance` @@ -659,12 +1581,6 @@ def __init__(self, projection=None): """ self.projection = projection - wmsg = ( - "The class iris.experimental.regrid.ProjectedUnstructuredLinear " - "has been deprecated, and will be removed in a future release. " - "Please consult the docstring for details." - ) - warn_deprecated(wmsg) def regridder(self, src_cube, target_grid): """ @@ -723,17 +1639,6 @@ def __init__(self, projection=None): Nearest regridding scheme that uses scipy.interpolate.griddata on projected unstructured data. - .. note:: - - .. deprecated:: 3.2.0 - - This class is scheduled to be removed in a future release, and no - exact replacement is currently planned. - Please use :class:`iris.analysis.UnstructuredNearest` instead, if - possible. If you have a need for this exact functionality, please - contact the Iris Developers to discuss how to retain it (which - could include reversing the deprecation). - Optional Args: * projection: `cartopy.crs instance` @@ -743,13 +1648,6 @@ def __init__(self, projection=None): """ self.projection = projection - wmsg = ( - "iris.experimental.regrid.ProjectedUnstructuredNearest has been " - "deprecated, and will be removed in a future release. " - "Please use 'iris.analysis.UnstructuredNearest' instead, where " - "possible. Consult the docstring for details." - ) - warn_deprecated(wmsg) def regridder(self, src_cube, target_grid): """ diff --git a/lib/iris/experimental/regrid_conservative.py b/lib/iris/experimental/regrid_conservative.py index bfa048ddf0..421bd86c93 100644 --- a/lib/iris/experimental/regrid_conservative.py +++ b/lib/iris/experimental/regrid_conservative.py @@ -6,35 +6,16 @@ """ Support for conservative regridding via ESMPy. -.. note:: - - .. deprecated:: 3.2.0 - - This package will be removed in a future release. - Please use - `iris-esmf-regrid `_ - instead. - """ import cartopy.crs as ccrs import numpy as np import iris -from iris._deprecation import warn_deprecated from iris.analysis._interpolation import get_xy_dim_coords from iris.analysis._regrid import RectilinearRegridder from iris.util import _meshgrid -wmsg = ( - "The 'iris.experimental.regrid_conservative' package is deprecated since " - "version 3.2, and will be removed in a future release. Please use " - "iris-emsf-regrid instead. " - "See https://github.com/SciTools-incubator/iris-esmf-regrid." -) -warn_deprecated(wmsg) - - #: A static Cartopy Geodetic() instance for transforming to true-lat-lons. _CRS_TRUELATLON = ccrs.Geodetic() @@ -150,22 +131,6 @@ def regrid_conservative_via_esmpy(source_cube, grid_cube): """ Perform a conservative regridding with ESMPy. - .. note :: - - .. deprecated:: 3.2.0 - - This function is scheduled to be removed in a future release. - Please use - `iris-esmf-regrid `_ - instead. - - For example : - - .. code:: - - from emsf_regrid.schemes import ESMFAreaWeighted - result = src_cube.regrid(grid_cube, ESMFAreaWeighted()) - Regrids the data of a source cube onto a new grid defined by a destination cube. @@ -204,15 +169,6 @@ def regrid_conservative_via_esmpy(source_cube, grid_cube): To alter this, make a prior call to ESMF.Manager(). """ - wmsg = ( - "The function " - "'iris.experimental.regrid_conservative." - "regrid_weighted_curvilinear_to_rectilinear' " - "has been deprecated, and will be removed in a future release. " - "Please consult the docstring for details." - ) - warn_deprecated(wmsg) - # Lazy import so we can build the docs with no ESMF. import ESMF diff --git a/lib/iris/experimental/ugrid/load.py b/lib/iris/experimental/ugrid/load.py index 6c802e00d4..5e42f2e606 100644 --- a/lib/iris/experimental/ugrid/load.py +++ b/lib/iris/experimental/ugrid/load.py @@ -280,7 +280,7 @@ def _build_aux_coord(coord_var, file_path): return coord, axis -def _build_connectivity(connectivity_var, file_path, element_dims): +def _build_connectivity(connectivity_var, file_path, location_dims): """ Construct a :class:`~iris.experimental.ugrid.mesh.Connectivity` from a given :class:`~iris.experimental.ugrid.cf.CFUGridConnectivityVariable`, @@ -301,10 +301,10 @@ def _build_connectivity(connectivity_var, file_path, element_dims): dim_names = connectivity_var.dimensions # Connectivity arrays must have two dimensions. assert len(dim_names) == 2 - if dim_names[1] in element_dims: - location_axis = 1 + if dim_names[1] in location_dims: + src_dim = 1 else: - location_axis = 0 + src_dim = 0 standard_name, long_name, var_name = get_names( connectivity_var, None, attributes @@ -319,7 +319,7 @@ def _build_connectivity(connectivity_var, file_path, element_dims): units=attr_units, attributes=attributes, start_index=start_index, - location_axis=location_axis, + src_dim=src_dim, ) return connectivity, dim_names[0] @@ -423,20 +423,20 @@ def _build_mesh(cf, mesh_var, file_path): raise ValueError(message) # Used for detecting transposed connectivities. - element_dims = (edge_dimension, face_dimension) + location_dims = (edge_dimension, face_dimension) connectivity_args = [] for connectivity_var in mesh_var.cf_group.connectivities.values(): connectivity, first_dim_name = _build_connectivity( - connectivity_var, file_path, element_dims + connectivity_var, file_path, location_dims ) assert connectivity.var_name == getattr(mesh_var, connectivity.cf_role) connectivity_args.append(connectivity) # If the mesh_var has not supplied the dimension name, it is safe to # fall back on the connectivity's first dimension's name. - if edge_dimension is None and connectivity.location == "edge": + if edge_dimension is None and connectivity.src_location == "edge": edge_dimension = first_dim_name - if face_dimension is None and connectivity.location == "face": + if face_dimension is None and connectivity.src_location == "face": face_dimension = first_dim_name standard_name, long_name, var_name = get_names(mesh_var, None, attributes) @@ -480,12 +480,12 @@ def _build_mesh_coords(mesh, cf_var): """ # TODO: integrate with standard saving API when no longer 'experimental'. # Identify the cube's mesh dimension, for attaching MeshCoords. - element_dimensions = { + locations_dimensions = { "node": mesh.node_dimension, "edge": mesh.edge_dimension, "face": mesh.face_dimension, } - mesh_dim_name = element_dimensions[cf_var.location] + mesh_dim_name = locations_dimensions[cf_var.location] # (Only expecting 1 mesh dimension per cf_var). mesh_dim = cf_var.dimensions.index(mesh_dim_name) diff --git a/lib/iris/experimental/ugrid/mesh.py b/lib/iris/experimental/ugrid/mesh.py index 974a563046..0f2bfd844c 100644 --- a/lib/iris/experimental/ugrid/mesh.py +++ b/lib/iris/experimental/ugrid/mesh.py @@ -12,10 +12,9 @@ """ from abc import ABC, abstractmethod from collections import namedtuple -from collections.abc import Container +import re from typing import Iterable -from cf_units import Unit from dask import array as da import numpy as np @@ -25,11 +24,10 @@ metadata_filter, metadata_manager_factory, ) -from ...common.metadata import BaseMetadata from ...config import get_logger from ...coords import AuxCoord, _DimensionalMetadata from ...exceptions import ConnectivityNotFoundError, CoordinateNotFoundError -from ...util import array_equal, clip_string, guess_coord_axis +from ...util import array_equal, guess_coord_axis from .metadata import ConnectivityMetadata, MeshCoordMetadata, MeshMetadata # Configure the logger. @@ -94,8 +92,8 @@ class Connectivity(_DimensionalMetadata): """ A CF-UGRID topology connectivity, describing the topological relationship - between two types of mesh element. One or more connectivities make up a - CF-UGRID topology - a constituent of a CF-UGRID mesh. + between two lists of dimensional locations. One or more connectivities + make up a CF-UGRID topology - a constituent of a CF-UGRID mesh. See: https://ugrid-conventions.github.io/ugrid-conventions @@ -124,7 +122,7 @@ def __init__( units=None, attributes=None, start_index=0, - location_axis=0, + src_dim=0, ): """ Constructs a single connectivity. @@ -132,23 +130,17 @@ def __init__( Args: * indices (numpy.ndarray or numpy.ma.core.MaskedArray or dask.array.Array): - 2D array giving the topological connection relationship between - :attr:`location` elements and :attr:`connected` elements. - The :attr:`location_axis` dimension indexes over the - :attr:`location` dimension of the mesh - i.e. its length matches - the total number of :attr:`location` elements in the mesh. The - :attr:`connected_axis` dimension can be any length, corresponding - to the highest number of :attr:`connected` elements connected to a - :attr:`location` element. The array values are indices into the - :attr:`connected` dimension of the mesh. If the number of - :attr:`connected` elements varies between :attr:`location` - elements: use a :class:`numpy.ma.core.MaskedArray` and mask the - :attr:`location` elements' unused index 'slots'. Use a - :class:`dask.array.Array` to keep indices 'lazy'. + The index values describing a topological relationship. Constructed + of 2 dimensions - the list of locations, and within each location: + the indices of the 'target locations' it relates to. + Use a :class:`numpy.ma.core.MaskedArray` if :attr:`src_location` + lengths vary - mask unused index 'slots' within each + :attr:`src_location`. Use a :class:`dask.array.Array` to keep + indices 'lazy'. * cf_role (str): Denotes the topological relationship that this connectivity - describes. Made up of this array's :attr:`location`, and the - :attr:`connected` element type that is indexed by the array. + describes. Made up of this array's locations, and the indexed + 'target location' within each location. See :attr:`UGRID_CF_ROLES` for valid arguments. Kwargs: @@ -172,14 +164,14 @@ def __init__( Either ``0`` or ``1``. Default is ``0``. Denotes whether :attr:`indices` uses 0-based or 1-based indexing (allows support for Fortran and legacy NetCDF files). - * location_axis (int): - Either ``0`` or ``1``. Default is ``0``. Denotes which axis - of :attr:`indices` varies over the :attr:`location` elements (the - alternate axis therefore varying over :attr:`connected` elements). - (This parameter allows support for fastest varying index being + * src_dim (int): + Either ``0`` or ``1``. Default is ``0``. Denotes which dimension + of :attr:`indices` varies over the :attr:`src_location`\\ s (the + alternate dimension therefore varying within individual + :attr:`src_location`\\ s). (This parameter allows support for fastest varying index being either first or last). E.g. for ``face_node_connectivity``, for 10 faces: - ``indices.shape[location_axis] == 10``. + ``indices.shape[src_dim] = 10``. """ @@ -196,15 +188,15 @@ def validate_arg_vs_list(arg_name, arg, valid_list): validate_arg_vs_list("start_index", start_index, [0, 1]) # indices array will be 2-dimensional, so must be either 0 or 1. - validate_arg_vs_list("location_axis", location_axis, [0, 1]) + validate_arg_vs_list("src_dim", src_dim, [0, 1]) validate_arg_vs_list("cf_role", cf_role, Connectivity.UGRID_CF_ROLES) self._metadata_manager.start_index = start_index - self._metadata_manager.location_axis = location_axis + self._metadata_manager.src_dim = src_dim self._metadata_manager.cf_role = cf_role - self._connected_axis = 1 - location_axis - self._location, self._connected = cf_role.split("_")[:2] + self._tgt_dim = 1 - src_dim + self._src_location, self._tgt_location = cf_role.split("_")[:2] super().__init__( values=indices, @@ -215,6 +207,58 @@ def validate_arg_vs_list(arg_name, arg, valid_list): attributes=attributes, ) + def __repr__(self): + def kwargs_filter(k, v): + result = False + if k != "cf_role": + if v is not None: + result = True + if ( + not isinstance(v, str) + and isinstance(v, Iterable) + and not v + ): + result = False + elif k == "units" and v == "unknown": + result = False + return result + + def array2repr(array): + if self.has_lazy_indices(): + result = repr(array) + else: + with np.printoptions( + threshold=NP_PRINTOPTIONS_THRESHOLD, + edgeitems=NP_PRINTOPTIONS_EDGEITEMS, + ): + result = re.sub("\n *", " ", repr(array)) + return result + + # positional arguments + args = ", ".join( + [ + f"{array2repr(self.core_indices())}", + f"cf_role={self.cf_role!r}", + ] + ) + + # optional arguments (metadata) + kwargs = ", ".join( + [ + f"{k}={v!r}" + for k, v in self.metadata._asdict().items() + if kwargs_filter(k, v) + ] + ) + + return f"{self.__class__.__name__}({', '.join([args, kwargs])})" + + def __str__(self): + args = ", ".join( + [f"cf_role={self.cf_role!r}", f"start_index={self.start_index!r}"] + ) + return f"{self.__class__.__name__}({args})" + @property def _values(self): # Overridden just to allow .setter override. @@ -239,25 +283,25 @@ def cf_role(self): return self._metadata_manager.cf_role @property - def location(self): + def src_location(self): """ Derived from the connectivity's :attr:`cf_role` - the first part, e.g. - ``face`` in ``face_node_connectivity``. Refers to the elements that - vary along the :attr:`location_axis` of the connectivity's - :attr:`indices` array. + ``face`` in ``face_node_connectivity``. Refers to the locations + listed by the :attr:`src_dim` of the connectivity's :attr:`indices` + array. """ - return self._location + return self._src_location @property - def connected(self): + def tgt_location(self): """ Derived from the connectivity's :attr:`cf_role` - the second part, e.g. - ``node`` in ``face_node_connectivity``. Refers to the elements indexed + ``node`` in ``face_node_connectivity``. Refers to the locations indexed by the values in the connectivity's :attr:`indices` array. """ - return self._connected + return self._tgt_location @property def start_index(self): @@ -272,48 +316,47 @@ def start_index(self): return self._metadata_manager.start_index @property - def location_axis(self): + def src_dim(self): """ - The axis of the connectivity's :attr:`indices` array that varies - over the connectivity's :attr:`location` elements. Either ``0`` or ``1``. + The dimension of the connectivity's :attr:`indices` array that varies + over the connectivity's :attr:`src_location`\\ s. Either ``0`` or ``1``. **Read-only** - validity of :attr:`indices` is dependent on - :attr:`location_axis`. Use :meth:`transpose` to create a new, transposed - :class:`Connectivity` if a different :attr:`location_axis` is needed. + :attr:`src_dim`. Use :meth:`transpose` to create a new, transposed + :class:`Connectivity` if a different :attr:`src_dim` is needed. """ - return self._metadata_manager.location_axis + return self._metadata_manager.src_dim @property - def connected_axis(self): + def tgt_dim(self): """ - Derived as the alternate value of :attr:`location_axis` - each must - equal either ``0`` or ``1``. The axis of the connectivity's - :attr:`indices` array that varies over the :attr:`connected` elements - associated with each :attr:`location` element. + Derived as the alternate value of :attr:`src_dim` - each must equal + either ``0`` or ``1``. + The dimension of the connectivity's :attr:`indices` array that varies + within the connectivity's individual :attr:`src_location`\\ s. """ - return self._connected_axis + return self._tgt_dim @property def indices(self): """ The index values describing the topological relationship of the connectivity, as a NumPy array. Masked points indicate a - :attr:`location` element with fewer :attr:`connected` elements than - other :attr:`location` elements described in this array - unused index - 'slots' are masked. + :attr:`src_location` shorter than the longest :attr:`src_location` + described in this array - unused index 'slots' are masked. **Read-only** - index values are only meaningful when combined with an appropriate :attr:`cf_role`, :attr:`start_index` and - :attr:`location_axis`. A new :class:`Connectivity` must therefore be + :attr:`src_dim`. A new :class:`Connectivity` must therefore be defined if different indices are needed. """ return self._values - def indices_by_location(self, indices=None): + def indices_by_src(self, indices=None): """ - Return a view of the indices array with :attr:`location_axis` **always** as - the first axis - transposed if necessary. Can optionally pass in an + Return a view of the indices array with :attr:`src_dim` **always** as + the first index - transposed if necessary. Can optionally pass in an identically shaped array on which to perform this operation (e.g. the output from :meth:`core_indices` or :meth:`lazy_indices`). @@ -325,7 +368,7 @@ def indices_by_location(self, indices=None): Returns: A view of the indices array, transposed - if necessary - to put - :attr:`location_axis` first. + :attr:`src_dim` first. """ if indices is None: @@ -337,20 +380,20 @@ def indices_by_location(self, indices=None): f"got shape={indices.shape} ." ) - if self.location_axis == 0: + if self.src_dim == 0: result = indices - elif self.location_axis == 1: + elif self.src_dim == 1: result = indices.transpose() else: - raise ValueError("Invalid location_axis.") + raise ValueError("Invalid src_dim.") return result def _validate_indices(self, indices, shapes_only=False): # Use shapes_only=True for a lower resource, less thorough validation # of indices by just inspecting the array shape instead of inspecting - # individual masks. So will not catch individual location elements - # having unacceptably low numbers of associated connected elements. + # individual masks. So will not catch individual src_locations being + # unacceptably small. def indices_error(message): raise ValueError("Invalid indices provided. " + message) @@ -379,43 +422,43 @@ def indices_error(message): len_req_fail = False if shapes_only: - location_shape = indices_shape[self.connected_axis] + src_shape = indices_shape[self.tgt_dim] # Wrap as lazy to allow use of the same operations below # regardless of shapes_only. - location_lengths = _lazy.as_lazy_data(np.asarray(location_shape)) + src_lengths = _lazy.as_lazy_data(np.asarray(src_shape)) else: # Wouldn't be safe to use during __init__ validation, since - # lazy_location_lengths requires self.indices to exist. Safe here since + # lazy_src_lengths requires self.indices to exist. Safe here since # shapes_only==False is only called manually, i.e. after # initialisation. - location_lengths = self.lazy_location_lengths() - if self.location in ("edge", "boundary"): - if (location_lengths != 2).any().compute(): + src_lengths = self.lazy_src_lengths() + if self.src_location in ("edge", "boundary"): + if (src_lengths != 2).any().compute(): len_req_fail = "len=2" else: - if self.location == "face": + if self.src_location == "face": min_size = 3 - elif self.location == "volume": - if self.connected == "edge": + elif self.src_location == "volume": + if self.tgt_location == "edge": min_size = 6 else: min_size = 4 else: raise NotImplementedError - if (location_lengths < min_size).any().compute(): + if (src_lengths < min_size).any().compute(): len_req_fail = f"len>={min_size}" if len_req_fail: indices_error( - f"Not all {self.location}s meet requirement: {len_req_fail} - " + f"Not all src_locations meet requirement: {len_req_fail} - " f"needed to describe '{self.cf_role}' ." ) def validate_indices(self): """ Perform a thorough validity check of this connectivity's - :attr:`indices`. Includes checking the number of :attr:`connected` - elements associated with each :attr:`location` element (specified using - masks on the :attr:`indices` array) against the :attr:`cf_role`. + :attr:`indices`. Includes checking the sizes of individual + :attr:`src_location`\\ s (specified using masks on the + :attr:`indices` array) against the :attr:`cf_role`. Raises a ``ValueError`` if any problems are encountered, otherwise passes silently. @@ -433,8 +476,8 @@ def __eq__(self, other): if isinstance(other, Connectivity): # Account for the fact that other could be the transposed equivalent # of self, which we consider 'safe' since the recommended - # interaction with the indices array is via indices_by_location, which - # corrects for this difference. (To enable this, location_axis does + # interaction with the indices array is via indices_by_src, which + # corrects for this difference. (To enable this, src_dim does # not participate in ConnectivityMetadata to ConnectivityMetadata # equivalence). if hasattr(other, "metadata"): @@ -443,22 +486,22 @@ def __eq__(self, other): if eq: eq = ( self.shape == other.shape - and self.location_axis == other.location_axis + and self.src_dim == other.src_dim ) or ( self.shape == other.shape[::-1] - and self.location_axis == other.connected_axis + and self.src_dim == other.tgt_dim ) if eq: eq = array_equal( - self.indices_by_location(self.core_indices()), - other.indices_by_location(other.core_indices()), + self.indices_by_src(self.core_indices()), + other.indices_by_src(other.core_indices()), ) return eq def transpose(self): """ Create a new :class:`Connectivity`, identical to this one but with the - :attr:`indices` array transposed and the :attr:`location_axis` value flipped. + :attr:`indices` array transposed and the :attr:`src_dim` value flipped. Returns: A new :class:`Connectivity` that is the transposed equivalent of @@ -474,7 +517,7 @@ def transpose(self): units=self.units, attributes=self.attributes, start_index=self.start_index, - location_axis=self.connected_axis, + src_dim=self.tgt_dim, ) return new_connectivity @@ -517,11 +560,11 @@ def has_lazy_indices(self): """ return super()._has_lazy_values() - def lazy_location_lengths(self): + def lazy_src_lengths(self): """ - Return a lazy array representing the number of :attr:`connected` - elements associated with each of the connectivity's :attr:`location` - elements, accounting for masks if present. + Return a lazy array representing the lengths of each + :attr:`src_location` in the :attr:`src_dim` of the connectivity's + :attr:`indices` array, accounting for masks if present. Accessing this method will never cause the :attr:`indices` values to be loaded. Similarly, calling methods on, or indexing, the returned Array @@ -531,28 +574,26 @@ def lazy_location_lengths(self): :attr:`indices` have already been loaded. Returns: - A lazy array, representing the number of :attr:`connected` - elements associated with each :attr:`location` element. + A lazy array, representing the lengths of each :attr:`src_location`. """ - location_mask_counts = da.sum( - da.ma.getmaskarray(self.indices), axis=self.connected_axis + src_mask_counts = da.sum( + da.ma.getmaskarray(self.indices), axis=self.tgt_dim ) - max_location_size = self.indices.shape[self.connected_axis] - return max_location_size - location_mask_counts + max_src_size = self.indices.shape[self.tgt_dim] + return max_src_size - src_mask_counts - def location_lengths(self): + def src_lengths(self): """ - Return a NumPy array representing the number of :attr:`connected` - elements associated with each of the connectivity's :attr:`location` - elements, accounting for masks if present. + Return a NumPy array representing the lengths of each + :attr:`src_location` in the :attr:`src_dim` of the connectivity's + :attr:`indices` array, accounting for masks if present. Returns: - A NumPy array, representing the number of :attr:`connected` - elements associated with each :attr:`location` element. + A NumPy array, representing the lengths of each :attr:`src_location`. """ - return self.lazy_location_lengths().compute() + return self.lazy_src_lengths().compute() def cube_dims(self, cube): """Not available on :class:`Connectivity`.""" @@ -565,7 +606,7 @@ def xml_element(self, doc): element.setAttribute("cf_role", self.cf_role) element.setAttribute("start_index", self.start_index) - element.setAttribute("location_axis", self.location_axis) + element.setAttribute("src_dim", self.src_dim) return element @@ -591,8 +632,8 @@ class Mesh(CFVariableMixin): AXES = ("x", "y") #: Valid range of values for ``topology_dimension``. TOPOLOGY_DIMENSIONS = (1, 2) - #: Valid mesh elements. - ELEMENTS = ("edge", "node", "face") + #: Valid mesh locations. + LOCATIONS = ("edge", "node", "face") def __init__( self, @@ -643,12 +684,12 @@ def __init__( self.attributes = attributes # based on the topology_dimension, create the appropriate coordinate manager - def normalise(element, axis): + def normalise(location, axis): result = str(axis).lower() if result not in self.AXES: - emsg = f"Invalid axis specified for {element} coordinate {coord.name()!r}, got {axis!r}." + emsg = f"Invalid axis specified for {location} coordinate {coord.name()!r}, got {axis!r}." raise ValueError(emsg) - return f"{element}_{result}" + return f"{location}_{result}" if not isinstance(node_coords_and_axes, Iterable): node_coords_and_axes = [node_coords_and_axes] @@ -934,143 +975,77 @@ def __ne__(self, other): result = not result return result - def summary(self, shorten=False): - """ - Return a string representation of the Mesh. - - Parameters - ---------- - shorten : bool, default = False - If True, produce a oneline string form of the form . - If False, produce a multi-line detailed print output. - - Returns - ------- - result : str - - """ - if shorten: - result = self._summary_oneline() - else: - result = self._summary_multiline() - return result - def __repr__(self): - return self.summary(shorten=True) - - def __str__(self): - return self.summary(shorten=False) - - def _summary_oneline(self): - # We use the repr output to produce short one-line identity summary, - # similar to the object.__str__ output "". - # This form also used in other str() constructions, like MeshCoord. - # By contrast, __str__ (below) produces a readable multi-line printout. - mesh_name = self.name() - if mesh_name in (None, "", "unknown"): - mesh_name = None - if mesh_name: - # Use a more human-readable form - mesh_string = f"" - else: - # Mimic the generic object.__str__ style. - mesh_id = id(self) - mesh_string = f"" + def to_coord_and_axis(members): + def axis(member): + return member.split("_")[1] + + result = [ + f"({coord!s}, {axis(member)!r})" + for member, coord in members._asdict().items() + if coord is not None + ] + result = f"[{', '.join(result)}]" if result else None + return result - return mesh_string + node_coords_and_axes = to_coord_and_axis(self.node_coords) + connectivities = [ + str(connectivity) + for connectivity in self.all_connectivities + if connectivity is not None + ] - def _summary_multiline(self): - # Produce a readable multi-line summary of the Mesh content. - lines = [] - n_indent = 4 - indent_str = " " * n_indent + if len(connectivities) == 1: + connectivities = connectivities[0] + else: + connectivities = f"[{', '.join(connectivities)}]" - def line(text, i_indent=0): - indent = indent_str * i_indent - lines.append(f"{indent}{text}") + # positional arguments + args = [ + f"topology_dimension={self.topology_dimension!r}", + f"node_coords_and_axes={node_coords_and_axes}", + f"connectivities={connectivities}", + ] - line(f"Mesh : '{self.name()}'") - line(f"topology_dimension: {self.topology_dimension}", 1) - for element in ("node", "edge", "face"): - if element == "node": - element_exists = True - else: - main_conn_name = f"{element}_node_connectivity" - main_conn = getattr(self, main_conn_name, None) - element_exists = main_conn is not None - if element_exists: - # Include a section for this element - line(element, 1) - # Print element dimension - dim_name = f"{element}_dimension" - dim = getattr(self, dim_name) - line(f"{dim_name}: '{dim}'", 2) - # Print defining connectivity (except node) - if element != "node": - main_conn_string = main_conn.summary( - shorten=True, linewidth=0 - ) - line(f"{main_conn_name}: {main_conn_string}", 2) - # Print coords - include_key = f"include_{element}s" - coords = self.coords(**{include_key: True}) - if coords: - line(f"{element} coordinates", 2) - for coord in coords: - coord_string = coord.summary(shorten=True, linewidth=0) - line(coord_string, 3) - - # Having dealt with essential info, now add any optional connectivities - # N.B. includes boundaries: as optional connectivity, not an "element" - optional_conn_names = ( - "boundary_connectivity", - "face_face_connectivity", - "face_edge_connectivity", - "edge_face_connectivity", + # optional argument + edge_coords_and_axes = to_coord_and_axis(self.edge_coords) + if edge_coords_and_axes: + args.append(f"edge_coords_and_axes={edge_coords_and_axes}") + + # optional argument + if self.topology_dimension > 1: + face_coords_and_axes = to_coord_and_axis(self.face_coords) + if face_coords_and_axes: + args.append(f"face_coords_and_axes={face_coords_and_axes}") + + def kwargs_filter(k, v): + result = False + if k != "topology_dimension": + if not ( + self.topology_dimension == 1 and k == "face_dimension" + ): + if v is not None: + result = True + if ( + not isinstance(v, str) + and isinstance(v, Iterable) + and not v + ): + result = False + elif k == "units" and v == "unknown": + result = False + return result + + # optional arguments (metadata) + args.extend( + [ + f"{k}={v!r}" + for k, v in self.metadata._asdict().items() + if kwargs_filter(k, v) + ] ) - optional_conns = [ - getattr(self, name, None) for name in optional_conn_names - ] - optional_conns = { - name: conn - for conn, name in zip(optional_conns, optional_conn_names) - if conn is not None - } - if optional_conns: - line("optional connectivities", 1) - for name, conn in optional_conns.items(): - conn_string = conn.summary(shorten=True, linewidth=0) - line(f"{name}: {conn_string}", 2) - - # Output the detail properties, basically those from CFVariableMixin - for name in BaseMetadata._members: - val = getattr(self, name, None) - if val is not None: - if name == "units": - show = val.origin != Unit(None) - elif isinstance(val, Container): - show = bool(val) - else: - show = val is not None - if show: - if name == "attributes": - # Use a multi-line form for this. - line("attributes:", 1) - max_attname_len = max(len(attr) for attr in val.keys()) - for attrname, attrval in val.items(): - attrname = attrname.ljust(max_attname_len) - if isinstance(attrval, str): - # quote strings - attrval = repr(attrval) - # and abbreviate really long ones - attrval = clip_string(attrval) - attr_string = f"{attrname} {attrval}" - line(attr_string, 2) - else: - line(f"{name}: {val!r}", 1) - - result = "\n".join(lines) - return result + + return f"{self.__class__.__name__}({', '.join(args)})" def __setstate__(self, state): metadata_manager, coord_manager, connectivity_manager = state @@ -1400,17 +1375,17 @@ def connectivities( :class:`~iris.experimental.ugrid.mesh.Connectivity`. * contains_node (bool): - Contains the ``node`` element as part of the + Contains the ``node`` location as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. * contains_edge (bool): - Contains the ``edge`` element as part of the + Contains the ``edge`` location as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. * contains_face (bool): - Contains the ``face`` element as part of the + Contains the ``face`` location as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. @@ -1501,17 +1476,17 @@ def connectivity( :class:`~iris.experimental.ugrid.mesh.Connectivity`. * contains_node (bool): - Contains the ``node`` element as part of the + Contains the ``node`` location as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. * contains_edge (bool): - Contains the ``edge`` element as part of the + Contains the ``edge`` location as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. * contains_face (bool): - Contains the ``face`` element as part of the + Contains the ``face`` location as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. @@ -1768,17 +1743,17 @@ def remove_connectivities( :class:`~iris.experimental.ugrid.mesh.Connectivity`. * contains_node (bool): - Contains the ``node`` element as part of the + Contains the ``node`` location as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched for potential removal. * contains_edge (bool): - Contains the ``edge`` element as part of the + Contains the ``edge`` location as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched for potential removal. * contains_face (bool): - Contains the ``face`` element as part of the + Contains the ``face`` location as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched for potential removal. @@ -2114,12 +2089,12 @@ def _remove(self, **kwargs): return result - def _setter(self, element, axis, coord, shape): + def _setter(self, location, axis, coord, shape): axis = axis.lower() - member = f"{element}_{axis}" + member = f"{location}_{axis}" # enforce the UGRID minimum coordinate requirement - if element == "node" and coord is None: + if location == "node" and coord is None: emsg = ( f"{member!r} is a required coordinate, cannot set to 'None'." ) @@ -2146,22 +2121,22 @@ def _setter(self, element, axis, coord, shape): self._members[member] = coord - def _shape(self, element): - coord = getattr(self, f"{element}_x") + def _shape(self, location): + coord = getattr(self, f"{location}_x") shape = coord.shape if coord is not None else None if shape is None: - coord = getattr(self, f"{element}_y") + coord = getattr(self, f"{location}_y") if coord is not None: shape = coord.shape return shape @property def _edge_shape(self): - return self._shape(element="edge") + return self._shape(location="edge") @property def _node_shape(self): - return self._shape(element="node") + return self._shape(location="node") @property def all_members(self): @@ -2178,7 +2153,7 @@ def edge_x(self): @edge_x.setter def edge_x(self, coord): self._setter( - element="edge", axis="x", coord=coord, shape=self._edge_shape + location="edge", axis="x", coord=coord, shape=self._edge_shape ) @property @@ -2188,7 +2163,7 @@ def edge_y(self): @edge_y.setter def edge_y(self, coord): self._setter( - element="edge", axis="y", coord=coord, shape=self._edge_shape + location="edge", axis="y", coord=coord, shape=self._edge_shape ) @property @@ -2202,7 +2177,7 @@ def node_x(self): @node_x.setter def node_x(self, coord): self._setter( - element="node", axis="x", coord=coord, shape=self._node_shape + location="node", axis="x", coord=coord, shape=self._node_shape ) @property @@ -2212,7 +2187,7 @@ def node_y(self): @node_y.setter def node_y(self, coord): self._setter( - element="node", axis="y", coord=coord, shape=self._node_shape + location="node", axis="y", coord=coord, shape=self._node_shape ) def _add(self, coords): @@ -2388,7 +2363,7 @@ def __init__( @property def _face_shape(self): - return self._shape(element="face") + return self._shape(location="face") @property def all_members(self): @@ -2405,7 +2380,7 @@ def face_x(self): @face_x.setter def face_x(self, coord): self._setter( - element="face", axis="x", coord=coord, shape=self._face_shape + location="face", axis="x", coord=coord, shape=self._face_shape ) @property @@ -2415,7 +2390,7 @@ def face_y(self): @face_y.setter def face_y(self, coord): self._setter( - element="face", axis="y", coord=coord, shape=self._face_shape + location="face", axis="y", coord=coord, shape=self._face_shape ) def add( @@ -2535,20 +2510,24 @@ def add(self, *connectivities): # Validate shapes. proposed_members = {**self._members, **add_dict} - elements = set( - [c.location for c in proposed_members.values() if c is not None] + locations = set( + [ + c.src_location + for c in proposed_members.values() + if c is not None + ] ) - for element in elements: + for location in locations: counts = [ - len(c.indices_by_location(c.lazy_indices())) + len(c.indices_by_src(c.lazy_indices())) for c in proposed_members.values() - if c is not None and c.location == element + if c is not None and c.src_location == location ] # Check is list values are identical. if not counts.count(counts[0]) == len(counts): message = ( f"Invalid Connectivities provided - inconsistent " - f"{element} counts." + f"{location} counts." ) raise ValueError(message) @@ -2603,16 +2582,13 @@ def filters( instance for instance in members if instance.cf_role == cf_role ] - def element_filter(instances, loc_arg, loc_name): + def location_filter(instances, loc_arg, loc_name): if loc_arg is False: filtered = [ instance for instance in instances if loc_name - not in ( - instance.location, - instance.connected, - ) + not in (instance.src_location, instance.tgt_location) ] elif loc_arg is None: filtered = instances @@ -2621,7 +2597,8 @@ def element_filter(instances, loc_arg, loc_name): filtered = [ instance for instance in instances - if loc_name in (instance.location, instance.connected) + if loc_name + in (instance.src_location, instance.tgt_location) ] return filtered @@ -2631,7 +2608,7 @@ def element_filter(instances, loc_arg, loc_name): (contains_edge, "edge"), (contains_face, "face"), ): - members = element_filter(members, arg, loc) + members = location_filter(members, arg, loc) # No need to actually modify filtering behaviour - already won't return # any face cf-roles if none are present. @@ -2813,10 +2790,10 @@ def __init__( # NOTE: currently *not* included in metadata. In future it might be. self._mesh = mesh - if location not in Mesh.ELEMENTS: + if location not in Mesh.LOCATIONS: msg = ( f"'location' of {location} is not a valid Mesh location', " - f"must be one of {Mesh.ELEMENTS}." + f"must be one of {Mesh.LOCATIONS}." ) raise ValueError(msg) # Held in metadata, readable as self.location, but cannot set it. @@ -2966,62 +2943,62 @@ def __eq__(self, other): def __hash__(self): return hash(id(self)) - def summary(self, *args, **kwargs): - # We need to specialise _DimensionalMetadata.summary, so that we always - # print the mesh+location of a MeshCoord. - if len(args) > 0: - shorten = args[0] - else: - shorten = kwargs.get("shorten", False) - - # Get the default-form result. - if shorten: - # NOTE: we simply aren't interested in the values for the repr, - # so fix linewidth to suppress them - kwargs["linewidth"] = 1 - - # Plug private key, to get back the section structure info - section_indices = {} - kwargs["_section_indices"] = section_indices - result = super().summary(*args, **kwargs) - - # Modify the generic 'default-form' result to produce what we want. - if shorten: - # Single-line form : insert mesh+location before the array part - # Construct a text detailing the mesh + location - mesh_string = self.mesh.name() - if mesh_string == "unknown": - # If no name, replace with the one-line summary - mesh_string = self.mesh.summary(shorten=True) - extra_str = f"mesh({mesh_string}) location({self.location}) " - # find where in the line the data-array text begins - i_line, i_array = section_indices["data"] - assert i_line == 0 - # insert the extra text there - result = result[:i_array] + extra_str + result[i_array:] - # NOTE: this invalidates the original width calculation and may - # easily extend the result beyond the intended maximum linewidth. - # We do treat that as an advisory control over array printing, not - # an absolute contract, so just ignore the problem for now. + def _string_summary(self, repr_style): + # Note: bypass the immediate parent here, which is Coord, because we + # have no interest in reporting coord_system or climatological, or in + # printing out our points/bounds. + # We also want to list our defining properties, i.e. mesh/location/axis + # *first*, before names/units etc, so different from other Coord types. + + # First construct a shortform text summary to identify the Mesh. + # IN 'str-mode', this attempts to use Mesh.name() if it is set, + # otherwise uses an object-id style (as also for 'repr-mode'). + # TODO: use a suitable method provided by Mesh, e.g. something like + # "Mesh.summary(shorten=True)", when it is available. + mesh_name = None + if not repr_style: + mesh_name = self.mesh.name() + if mesh_name in (None, "", "unknown"): + mesh_name = None + if mesh_name: + # Use a more human-readable form + mesh_string = f"Mesh({mesh_name!r})" else: - # Multiline form - # find where the "location: ... " section is - i_location, i_namestart = section_indices["location"] - lines = result.split("\n") - location_line = lines[i_location] - # copy the indent spacing - indent = location_line[:i_namestart] - # use that to construct a suitable 'mesh' line - mesh_string = self.mesh.summary(shorten=True) - mesh_line = f"{indent}mesh: {mesh_string}" - # Move the 'location' line, putting it and the 'mesh' line right at - # the top, immediately after the header line. - del lines[i_location] - lines[1:1] = [mesh_line, location_line] - # Re-join lines to give the result - result = "\n".join(lines) + # Mimic the generic object.__str__ style. + mesh_id = id(self.mesh) + mesh_string = f"" + result = ( + f"mesh={mesh_string}" + f", location={self.location!r}" + f", axis={self.axis!r}" + ) + # Add 'other' metadata that is drawn from the underlying node-coord. + # But put these *afterward*, unlike other similar classes. + for item in ( + "shape", + "standard_name", + "units", + "long_name", + "attributes", + ): + # NOTE: order of these matches Coord.summary, but omit var_name. + val = getattr(self, item, None) + if item == "attributes": + is_blank = len(val) == 0 # an empty dict is as good as none + else: + is_blank = val is None + if not is_blank: + result += f", {item}={val!r}" + + result = f"MeshCoord({result})" return result + def __str__(self): + return self._string_summary(repr_style=False) + + def __repr__(self): + return self._string_summary(repr_style=True) + def _construct_access_arrays(self): """ Build lazy points and bounds arrays, providing dynamic access via the @@ -3058,7 +3035,7 @@ def _construct_access_arrays(self): # Data can be real or lazy, so operations must work in Dask, too. indices = bounds_connectivity.core_indices() # Normalise indices dimension order to [faces/edges, bounds] - indices = bounds_connectivity.indices_by_location(indices) + indices = bounds_connectivity.indices_by_src(indices) # Normalise the start index indices = indices - bounds_connectivity.start_index diff --git a/lib/iris/experimental/ugrid/metadata.py b/lib/iris/experimental/ugrid/metadata.py index ae0b787908..94128cdf50 100644 --- a/lib/iris/experimental/ugrid/metadata.py +++ b/lib/iris/experimental/ugrid/metadata.py @@ -28,9 +28,9 @@ class ConnectivityMetadata(BaseMetadata): """ - # The "location_axis" member is stateful only, and does not participate in + # The "src_dim" member is stateful only, and does not participate in # lenient/strict equivalence. - _members = ("cf_role", "start_index", "location_axis") + _members = ("cf_role", "start_index", "src_dim") __slots__ = () @@ -53,7 +53,7 @@ def _combine_lenient(self, other): A list of combined metadata member values. """ - # Perform "strict" combination for "cf_role", "start_index", "location_axis". + # Perform "strict" combination for "cf_role", "start_index", "src_dim". def func(field): left = getattr(self, field) right = getattr(other, field) @@ -82,10 +82,9 @@ def _compare_lenient(self, other): """ # Perform "strict" comparison for "cf_role", "start_index". - # The "location_axis" member is not part of lenient equivalence. + # The "src_dim" member is not part of lenient equivalence. members = filter( - lambda member: member != "location_axis", - ConnectivityMetadata._members, + lambda member: member != "src_dim", ConnectivityMetadata._members ) result = all( [ @@ -113,7 +112,7 @@ def _difference_lenient(self, other): A list of difference metadata member values. """ - # Perform "strict" difference for "cf_role", "start_index", "location_axis". + # Perform "strict" difference for "cf_role", "start_index", "src_dim". def func(field): left = getattr(self, field) right = getattr(other, field) diff --git a/lib/iris/fileformats/__init__.py b/lib/iris/fileformats/__init__.py index 96a848deb0..f2b0cfc095 100644 --- a/lib/iris/fileformats/__init__.py +++ b/lib/iris/fileformats/__init__.py @@ -17,7 +17,7 @@ UriProtocol, ) -from . import name, netcdf, nimrod, pp, um +from . import abf, name, netcdf, nimrod, pp, um __all__ = ["FORMAT_AGENT"] @@ -224,23 +224,16 @@ def _load_grib(*args, **kwargs): # # ABF/ABL -# TODO: now deprecated, remove later # -def load_cubes_abf_abl(*args, **kwargs): - from . import abf - - return abf.load_cubes(*args, **kwargs) - - FORMAT_AGENT.add_spec( FormatSpecification( - "ABF", FileExtension(), ".abf", load_cubes_abf_abl, priority=3 + "ABF", FileExtension(), ".abf", abf.load_cubes, priority=3 ) ) FORMAT_AGENT.add_spec( FormatSpecification( - "ABL", FileExtension(), ".abl", load_cubes_abf_abl, priority=3 + "ABL", FileExtension(), ".abl", abf.load_cubes, priority=3 ) ) diff --git a/lib/iris/fileformats/_ff_cross_references.py b/lib/iris/fileformats/_ff_cross_references.py index ca41f5257f..0c7af26324 100644 --- a/lib/iris/fileformats/_ff_cross_references.py +++ b/lib/iris/fileformats/_ff_cross_references.py @@ -3,9 +3,11 @@ # 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. +# +# DO NOT EDIT: AUTO-GENERATED """ -Table providing UM grid-code, field-code and pseudolevel-type for (some) -stash codes. Used in UM file i/o. +Auto-generated from iris/tools/gen_stash_refs.py +Relates grid code and field code to the stash code. """ diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index d286abbf3d..e3b63d41d2 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -184,19 +184,16 @@ def action_provides_coordinate(engine, dimcoord_fact): # Identify the "type" of a coordinate variable coord_type = None - - if hh.is_latitude(engine, var_name): - # N.B. result of 'is_rotated_lat/lon' checks are valid ONLY when the - # relevant 'is_lat/lon' is also True. - if hh.is_rotated_latitude(engine, var_name): - coord_type = "rotated_latitude" - else: - coord_type = "latitude" + # NOTE: must test for rotated cases *first*, as 'is_longitude' and + # 'is_latitude' functions also accept rotated cases. + if hh.is_rotated_latitude(engine, var_name): + coord_type = "rotated_latitude" + elif hh.is_rotated_longitude(engine, var_name): + coord_type = "rotated_longitude" + elif hh.is_latitude(engine, var_name): + coord_type = "latitude" elif hh.is_longitude(engine, var_name): - if hh.is_rotated_longitude(engine, var_name): - coord_type = "rotated_longitude" - else: - coord_type = "longitude" + coord_type = "longitude" elif hh.is_time(engine, var_name): coord_type = "time" elif hh.is_time_period(engine, var_name): diff --git a/lib/iris/fileformats/abf.py b/lib/iris/fileformats/abf.py index 5c70c5acf2..678d9b04cf 100644 --- a/lib/iris/fileformats/abf.py +++ b/lib/iris/fileformats/abf.py @@ -23,20 +23,11 @@ import numpy.ma as ma import iris -from iris._deprecation import warn_deprecated from iris.coord_systems import GeogCS from iris.coords import AuxCoord, DimCoord import iris.fileformats import iris.io.format_picker -wmsg = ( - "iris.fileformats.abf has been deprecated and will be removed in a " - "future release. If you make use of this functionality, please contact " - "the Iris Developers to discuss how to retain it (which may involve " - "reversing the deprecation)." -) -warn_deprecated(wmsg) - X_SIZE = 4320 Y_SIZE = 2160 diff --git a/lib/iris/fileformats/dot.py b/lib/iris/fileformats/dot.py index 2fb628bebf..cc857c7f6b 100644 --- a/lib/iris/fileformats/dot.py +++ b/lib/iris/fileformats/dot.py @@ -12,17 +12,8 @@ import subprocess import iris -from iris._deprecation import warn_deprecated import iris.util -wmsg = ( - "iris.fileformats.dot has been deprecated and will be removed in a " - "future release. If you make use of this functionality, please contact " - "the Iris Developers to discuss how to retain it (which may involve " - "reversing the deprecation)." -) -warn_deprecated(wmsg) - _GRAPH_INDENT = " " * 4 _SUBGRAPH_INDENT = " " * 8 diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 73a137b4af..77134259ad 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -972,9 +972,9 @@ def __setitem__(self, keys, arr): self.target[keys] = arr -# NOTE : this matches :class:`iris.experimental.ugrid.mesh.Mesh.ELEMENTS`, +# NOTE : this matches :class:`iris.experimental.ugrid.mesh.Mesh.LOCATIONS`, # but in the preferred order for coord/connectivity variables in the file. -MESH_ELEMENTS = ("node", "edge", "face") +MESH_LOCATIONS = ("node", "edge", "face") class Saver: @@ -1422,7 +1422,7 @@ def _add_mesh(self, cube_or_mesh): mesh_dims = self._mesh_dims[mesh] # Add all the element coordinate variables. - for location in MESH_ELEMENTS: + for location in MESH_LOCATIONS: coords_meshobj_attr = f"{location}_coords" coords_file_attr = f"{location}_coordinates" mesh_coords = getattr(mesh, coords_meshobj_attr, None) @@ -1460,7 +1460,7 @@ def _add_mesh(self, cube_or_mesh): last_dim = f"{cf_mesh_name}_{loc_from}_N_{loc_to}s" # Create if it does not already exist. if last_dim not in self._dataset.dimensions: - length = conn.shape[1 - conn.location_axis] + length = conn.shape[1 - conn.src_dim] self._dataset.createDimension(last_dim, length) # Create variable. @@ -1470,7 +1470,7 @@ def _add_mesh(self, cube_or_mesh): # when it is first created. loc_dim_name = mesh_dims[loc_from] conn_dims = (loc_dim_name, last_dim) - if conn.location_axis == 1: + if conn.src_dim == 1: # Has the 'other' dimension order, =reversed conn_dims = conn_dims[::-1] if iris.util.is_masked(conn.core_indices()): @@ -1494,7 +1494,7 @@ def _add_mesh(self, cube_or_mesh): _setncattr(cf_mesh_var, cf_conn_attr_name, cf_conn_name) # If the connectivity had the 'alternate' dimension order, add the # relevant dimension property - if conn.location_axis == 1: + if conn.src_dim == 1: loc_dim_attr = f"{loc_from}_dimension" # Should only get here once. assert loc_dim_attr not in cf_mesh_var.ncattrs() @@ -1813,7 +1813,7 @@ def record_dimension(names_list, dim_name, length, matching_coords=[]): # NOTE: one of these will be a cube dimension, but that one does not # get any special handling. We *do* want to list/create them in a # definite order (node,edge,face), and before non-mesh dimensions. - for location in MESH_ELEMENTS: + for location in MESH_LOCATIONS: # Find if this location exists in the mesh, and a characteristic # coordinate to identify it with. # To use only _required_ UGRID components, we use a location @@ -1850,9 +1850,7 @@ def record_dimension(names_list, dim_name, length, matching_coords=[]): (dim_length,) = dim_element.shape else: # extract source dim, respecting dim-ordering - dim_length = dim_element.shape[ - dim_element.location_axis - ] + dim_length = dim_element.shape[dim_element.src_dim] # Name it for the relevant mesh dimension location_dim_attr = f"{location}_dimension" dim_name = getattr(mesh, location_dim_attr) @@ -2738,9 +2736,9 @@ def _create_cf_data_variable( cmin, cmax = _co_realise_lazy_arrays([cmin, cmax]) n = dtype.itemsize * 8 if masked: - scale_factor = (cmax - cmin) / (2**n - 2) + scale_factor = (cmax - cmin) / (2 ** n - 2) else: - scale_factor = (cmax - cmin) / (2**n - 1) + scale_factor = (cmax - cmin) / (2 ** n - 1) if dtype.kind == "u": add_offset = cmin elif dtype.kind == "i": diff --git a/lib/iris/fileformats/pp.py b/lib/iris/fileformats/pp.py index 9bda98bf61..9f213ec4db 100644 --- a/lib/iris/fileformats/pp.py +++ b/lib/iris/fileformats/pp.py @@ -403,7 +403,7 @@ def _calculate_str_value_from_value(self): def _calculate_value_from_str_value(self): self._value = np.sum( - [10**i * val for i, val in enumerate(self._strvalue)] + [10 ** i * val for i, val in enumerate(self._strvalue)] ) def __len__(self): @@ -418,7 +418,7 @@ def __getitem__(self, key): # if the key returns a list of values, then combine them together # to an integer if isinstance(val, list): - val = sum([10**i * val for i, val in enumerate(val)]) + val = sum([10 ** i * val for i, val in enumerate(val)]) return val diff --git a/lib/iris/fileformats/um_cf_map.py b/lib/iris/fileformats/um_cf_map.py index 8aee67ae3e..c2a0a5d09e 100644 --- a/lib/iris/fileformats/um_cf_map.py +++ b/lib/iris/fileformats/um_cf_map.py @@ -3,6 +3,14 @@ # 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. +# +# DO NOT EDIT: AUTO-GENERATED +# Created on 29 November 2019 14:11 from +# http://www.metarelate.net/metOcean +# at commit 448f2ef5e676edaaa27408b9f3ddbecbf05e3289 +# +# https://github.com/metarelate/metOcean/commit/448f2ef5e676edaaa27408b9f3ddbecbf05e3289 + """ Provides UM/CF phenomenon translations. @@ -681,7 +689,7 @@ 'm01s09i212': CFName(None, 'cloud_base_altitude_assuming_only_consider_cloud_area_fraction_greater_than_4p5_oktas', 'kft'), 'm01s09i213': CFName(None, 'cloud_base_altitude_assuming_only_consider_cloud_area_fraction_greater_than_5p5_oktas', 'kft'), 'm01s09i214': CFName(None, 'cloud_base_altitude_assuming_only_consider_cloud_area_fraction_greater_than_6p5_oktas', 'kft'), - 'm01s09i215': CFName(None, 'cloud_base_altitude_assuming_only_consider_cloud_area_fraction_greater_than_7p9_oktas', 'kft'), + 'm01s09i215': CFName(None, 'cloud_base_altitude_assuming_only_consider_cloud_area_fraction_greater_than_7p5_oktas', 'kft'), 'm01s09i216': CFName(None, 'cloud_area_fraction_assuming_random_overlap', '1'), 'm01s09i217': CFName(None, 'cloud_area_fraction_assuming_maximum_random_overlap', '1'), 'm01s09i218': CFName(None, 'cloud_area_fraction_assuming_only_consider_surface_to_1000_feet_asl', '1'), diff --git a/lib/iris/io/__init__.py b/lib/iris/io/__init__.py index 034fa4baab..64501afd1e 100644 --- a/lib/iris/io/__init__.py +++ b/lib/iris/io/__init__.py @@ -12,7 +12,6 @@ from collections import OrderedDict import glob import os.path -import pathlib import re import iris.exceptions @@ -86,9 +85,8 @@ def decode_uri(uri, default="file"): r""" Decodes a single URI into scheme and scheme-specific parts. - In addition to well-formed URIs, it also supports bare file paths as strings - or :class:`pathlib.PurePath`. Both Windows and UNIX style paths are - accepted. + In addition to well-formed URIs, it also supports bare file paths. + Both Windows and UNIX style paths are accepted. .. testsetup:: @@ -115,8 +113,6 @@ def decode_uri(uri, default="file"): ('file', 'dataZoo/...') """ - if isinstance(uri, pathlib.PurePath): - uri = str(uri) # make sure scheme has at least 2 letters to avoid windows drives # put - last in the brackets so it refers to the character, not a range # reference on valid schemes: http://tools.ietf.org/html/std66#section-3.1 @@ -316,8 +312,7 @@ def find_saver(filespec): Args: - * filespec - A string such as "my_file.pp" or "PP". + * filespec - A string such as "my_file.pp" or "PP". Returns: A save function or None. @@ -364,8 +359,7 @@ def save(source, target, saver=None, **kwargs): * target: A filename (or writeable, depending on file format). When given a filename or file, Iris can determine the - file format. Filename can be given as a string or - :class:`pathlib.PurePath`. + file format. Kwargs: @@ -420,8 +414,6 @@ def save(source, target, saver=None, **kwargs): from iris.cube import Cube, CubeList # Determine format from filename - if isinstance(target, pathlib.PurePath): - target = str(target) if isinstance(target, str) and saver is None: saver = find_saver(target) elif hasattr(target, "name") and saver is None: diff --git a/lib/iris/plot.py b/lib/iris/plot.py index 0e9645c783..1db60d0fae 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -858,9 +858,9 @@ def _replace_axes_with_cartopy_axes(cartopy_proj): def _ensure_cartopy_axes_and_determine_kwargs(x_coord, y_coord, kwargs): """ - Replace the current non-cartopy axes with - :class:`cartopy.mpl.geoaxes.GeoAxes` and return the appropriate kwargs dict - based on the provided coordinates and kwargs. + Replace the current non-cartopy axes with :class:`cartopy.mpl.GeoAxes` + and return the appropriate kwargs dict based on the provided coordinates + and kwargs. """ # Determine projection. @@ -874,7 +874,7 @@ def _ensure_cartopy_axes_and_determine_kwargs(x_coord, y_coord, kwargs): else: cartopy_proj = ccrs.PlateCarree() - # Ensure the current axes are a cartopy.mpl.geoaxes.GeoAxes instance. + # Ensure the current axes are a cartopy.mpl.GeoAxes instance. axes = kwargs.get("axes") if axes is None: if ( @@ -1430,8 +1430,7 @@ def barbs(u_cube, v_cube, *args, **kwargs): :func:`iris.analysis.cartography.rotate_grid_vectors`. To transform coordinate grid points, you will need to create 2-dimensional arrays of x and y values. These can be transformed with - the :meth:`~cartopy.crs.CRS.transform_points` method of - :class:`cartopy.crs.CRS`. + :meth:`cartopy.crs.CRS.transform_points`. Kwargs: @@ -1479,8 +1478,7 @@ def quiver(u_cube, v_cube, *args, **kwargs): :func:`iris.analysis.cartography.rotate_grid_vectors`. To transform coordinate grid points, you will need to create 2-dimensional arrays of x and y values. These can be transformed with - the :meth:`~cartopy.crs.CRS.transform_points` method of - :class:`cartopy.crs.CRS`. + :meth:`cartopy.crs.CRS.transform_points`. Kwargs: diff --git a/lib/iris/tests/integration/test_netcdf.py b/lib/iris/tests/integration/test_netcdf.py index 2a45561e17..f7aaa1d05c 100644 --- a/lib/iris/tests/integration/test_netcdf.py +++ b/lib/iris/tests/integration/test_netcdf.py @@ -416,7 +416,7 @@ def setUp(self): levels.units = "centimeters" levels.positive = "down" levels.axis = "Z" - levels[:] = np.linspace(0, 10**5, 3) + levels[:] = np.linspace(0, 10 ** 5, 3) volcello.id = "volcello" volcello.out_name = "volcello" @@ -507,9 +507,9 @@ def _get_scale_factor_add_offset(cube, datatype): else: masked = False if masked: - scale_factor = (cmax - cmin) / (2**n - 2) + scale_factor = (cmax - cmin) / (2 ** n - 2) else: - scale_factor = (cmax - cmin) / (2**n - 1) + scale_factor = (cmax - cmin) / (2 ** n - 1) if dt.kind == "u": add_offset = cmin elif dt.kind == "i": diff --git a/lib/iris/tests/integration/test_trajectory.py b/lib/iris/tests/integration/test_trajectory.py index a8e3acaa41..a7d6c89994 100644 --- a/lib/iris/tests/integration/test_trajectory.py +++ b/lib/iris/tests/integration/test_trajectory.py @@ -216,10 +216,8 @@ def setUp(self): cube.coord("depth").bounds = b32 self.cube = cube # define a latitude trajectory (put coords in a different order - # to the cube, just to be awkward) although avoid south pole - # singularity as a sample point and the issue of snapping to - # multi-equidistant closest points from within orca antarctic hole - latitudes = list(range(-80, 90, 2)) + # to the cube, just to be awkward) + latitudes = list(range(-90, 90, 2)) longitudes = [-90] * len(latitudes) self.sample_points = [ ("longitude", longitudes), @@ -228,9 +226,7 @@ def setUp(self): def test_tri_polar(self): # extract - sampled_cube = traj_interpolate( - self.cube, self.sample_points, method="nearest" - ) + sampled_cube = traj_interpolate(self.cube, self.sample_points) self.assertCML( sampled_cube, ("trajectory", "tri_polar_latitude_slice.cml") ) @@ -333,12 +329,8 @@ def test_hybrid_height(self): # Put a lazy array into the cube so we can test deferred loading. cube.data = as_lazy_data(cube.data) - # Use opionated grid-latitudes to avoid the issue of platform - # specific behaviour within SciPy cKDTree choosing a different - # equi-distant nearest neighbour point when there are multiple - # valid candidates. traj = ( - ("grid_latitude", [20.4, 21.6, 22.6, 23.6]), + ("grid_latitude", [20.5, 21.5, 22.5, 23.5]), ("grid_longitude", [31, 32, 33, 34]), ) xsec = traj_interpolate(cube, traj, method="nearest") diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/0d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/0d_str.txt index a4c1157df2..a6738e654f 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/0d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/0d_str.txt @@ -10,4 +10,4 @@ air_potential_temperature / (K) (scalar cube) surface_altitude 413.93686 m time 2009-09-09 17:10:00 Attributes: - source 'Iris test case' \ No newline at end of file + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/1d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/1d_str.txt index 7d43a997da..95f7e7b57e 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/1d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/1d_str.txt @@ -13,4 +13,4 @@ air_potential_temperature / (K) (grid_longitude: 100) sigma 0.9994238, bound=(1.0, 0.99846387) time 2009-09-09 17:10:00 Attributes: - source 'Iris test case' \ No newline at end of file + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/2d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/2d_str.txt index 9adeb35c73..c4184d199a 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/2d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/2d_str.txt @@ -13,4 +13,4 @@ air_potential_temperature / (K) (grid_latitude: 100; grid_longitude: 100) sigma 0.9994238, bound=(1.0, 0.99846387) time 2009-09-09 17:10:00 Attributes: - source 'Iris test case' \ No newline at end of file + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/3d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/3d_str.txt index dc5e71433f..af81d4e991 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/3d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/3d_str.txt @@ -13,4 +13,4 @@ air_potential_temperature / (K) (model_level_number: 70; grid_latitude: 100; forecast_period 0.0 hours time 2009-09-09 17:10:00 Attributes: - source 'Iris test case' \ No newline at end of file + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/4d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/4d_str.txt index 52adc03efb..afcdedf100 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/4d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/4d_str.txt @@ -13,4 +13,4 @@ air_potential_temperature / (K) (time: 6; model_level_number: 70; grid_latit Scalar coordinates: forecast_period 0.0 hours Attributes: - source 'Iris test case' \ No newline at end of file + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/0d_cube.__str__.txt b/lib/iris/tests/results/cdm/str_repr/0d_cube.__str__.txt index 02e9849d38..6a3276d861 100644 --- a/lib/iris/tests/results/cdm/str_repr/0d_cube.__str__.txt +++ b/lib/iris/tests/results/cdm/str_repr/0d_cube.__str__.txt @@ -8,4 +8,4 @@ air_temperature / (K) (scalar cube) time 1998-12-01 00:00:00 Attributes: STASH m01s16i203 - source 'Data from Met Office Unified Model' \ No newline at end of file + source Data from Met Office Unified Model \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/0d_cube.__unicode__.txt b/lib/iris/tests/results/cdm/str_repr/0d_cube.__unicode__.txt index 02e9849d38..6a3276d861 100644 --- a/lib/iris/tests/results/cdm/str_repr/0d_cube.__unicode__.txt +++ b/lib/iris/tests/results/cdm/str_repr/0d_cube.__unicode__.txt @@ -8,4 +8,4 @@ air_temperature / (K) (scalar cube) time 1998-12-01 00:00:00 Attributes: STASH m01s16i203 - source 'Data from Met Office Unified Model' \ No newline at end of file + source Data from Met Office Unified Model \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/cell_methods.__str__.txt b/lib/iris/tests/results/cdm/str_repr/cell_methods.__str__.txt index ffb6a62daf..ba93542e51 100644 --- a/lib/iris/tests/results/cdm/str_repr/cell_methods.__str__.txt +++ b/lib/iris/tests/results/cdm/str_repr/cell_methods.__str__.txt @@ -14,4 +14,4 @@ air_temperature / (K) (latitude: 73; longitude: 96) percentile longitude (6 minutes, This is another test comment) Attributes: STASH m01s16i203 - source 'Data from Met Office Unified Model' \ No newline at end of file + source Data from Met Office Unified Model \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/missing_coords_cube.str.txt b/lib/iris/tests/results/cdm/str_repr/missing_coords_cube.str.txt index 0ac5bd3c8a..1b86bd6597 100644 --- a/lib/iris/tests/results/cdm/str_repr/missing_coords_cube.str.txt +++ b/lib/iris/tests/results/cdm/str_repr/missing_coords_cube.str.txt @@ -11,4 +11,4 @@ air_potential_temperature / (K) (-- : 6; -- : 70; grid_latitude: 100; grid_l Scalar coordinates: forecast_period 0.0 hours Attributes: - source 'Iris test case' \ No newline at end of file + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/similar.__str__.txt b/lib/iris/tests/results/cdm/str_repr/similar.__str__.txt index 394e52e5c9..fc274ed4c1 100644 --- a/lib/iris/tests/results/cdm/str_repr/similar.__str__.txt +++ b/lib/iris/tests/results/cdm/str_repr/similar.__str__.txt @@ -15,4 +15,4 @@ air_temperature / (K) (latitude: 73; longitude: 96) time 1998-12-01 00:00:00 Attributes: STASH m01s16i203 - source 'Data from Met Office Unified Model' \ No newline at end of file + source Data from Met Office Unified Model \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__unicode__.txt b/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__unicode__.txt index 594ad11688..29c181345c 100644 --- a/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__unicode__.txt +++ b/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__unicode__.txt @@ -2,4 +2,4 @@ thingness / (1) (foo: 11) Dimension coordinates: foo x Attributes: - source 'ꀀabcd\u07b4' \ No newline at end of file + source ꀀabcd޴ \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/aux_nontime_repr.txt b/lib/iris/tests/results/coord_api/str_repr/aux_nontime_repr.txt index 3e7aeda309..c1d62f28e4 100644 --- a/lib/iris/tests/results/coord_api/str_repr/aux_nontime_repr.txt +++ b/lib/iris/tests/results/coord_api/str_repr/aux_nontime_repr.txt @@ -1 +1,11 @@ - \ No newline at end of file +DimCoord(array([-0.1278, -0.1269, -0.126 , -0.1251, -0.1242, -0.1233, -0.1224, + -0.1215, -0.1206, -0.1197], dtype=float32), bounds=array([[-0.12825, -0.12735], + [-0.12735, -0.12645], + [-0.12645, -0.12555], + [-0.12555, -0.12465], + [-0.12465, -0.12375], + [-0.12375, -0.12285], + [-0.12285, -0.12195], + [-0.12195, -0.12105], + [-0.12105, -0.12015], + [-0.12015, -0.11925]], dtype=float32), standard_name='grid_latitude', units=Unit('degrees'), coord_system=RotatedGeogCS(37.5, 177.5, ellipsoid=GeogCS(6371229.0))) \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/aux_nontime_str.txt b/lib/iris/tests/results/coord_api/str_repr/aux_nontime_str.txt index 3030ea962a..c1d62f28e4 100644 --- a/lib/iris/tests/results/coord_api/str_repr/aux_nontime_str.txt +++ b/lib/iris/tests/results/coord_api/str_repr/aux_nontime_str.txt @@ -1,15 +1,11 @@ -DimCoord : level_height / (m) - points: [ - 5. , 21.666664, 45. , 75. , 111.66668 , - 155. , 205. , 261.6667 , 325. , 395. ] - bounds: [ - [ 0. , 13.333332], - [ 13.333332, 33.333332], - ..., - [293.3333 , 360. ], - [360. , 433.3332 ]] - shape: (10,) bounds(10, 2) - dtype: float32 - long_name: 'level_height' - attributes: - positive 'up' \ No newline at end of file +DimCoord(array([-0.1278, -0.1269, -0.126 , -0.1251, -0.1242, -0.1233, -0.1224, + -0.1215, -0.1206, -0.1197], dtype=float32), bounds=array([[-0.12825, -0.12735], + [-0.12735, -0.12645], + [-0.12645, -0.12555], + [-0.12555, -0.12465], + [-0.12465, -0.12375], + [-0.12375, -0.12285], + [-0.12285, -0.12195], + [-0.12195, -0.12105], + [-0.12105, -0.12015], + [-0.12015, -0.11925]], dtype=float32), standard_name='grid_latitude', units=Unit('degrees'), coord_system=RotatedGeogCS(37.5, 177.5, ellipsoid=GeogCS(6371229.0))) \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/aux_time_repr.txt b/lib/iris/tests/results/coord_api/str_repr/aux_time_repr.txt index 57d5882e88..120546311f 100644 --- a/lib/iris/tests/results/coord_api/str_repr/aux_time_repr.txt +++ b/lib/iris/tests/results/coord_api/str_repr/aux_time_repr.txt @@ -1 +1,2 @@ - \ No newline at end of file +DimCoord(array([347921.16666667, 347921.33333333, 347921.5 , 347921.66666666, + 347921.83333333, 347922. ]), standard_name='time', units=Unit('hours since 1970-01-01 00:00:00', calendar='gregorian')) \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/aux_time_str.txt b/lib/iris/tests/results/coord_api/str_repr/aux_time_str.txt index f9cd09223a..9d209402e6 100644 --- a/lib/iris/tests/results/coord_api/str_repr/aux_time_str.txt +++ b/lib/iris/tests/results/coord_api/str_repr/aux_time_str.txt @@ -1,5 +1,2 @@ -DimCoord : forecast_period / (hours) - points: [0.] - shape: (1,) - dtype: float64 - standard_name: 'forecast_period' \ No newline at end of file +DimCoord([2009-09-09 17:10:00, 2009-09-09 17:20:00, 2009-09-09 17:30:00, + 2009-09-09 17:40:00, 2009-09-09 17:50:00, 2009-09-09 18:00:00], standard_name='time', calendar='gregorian') \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/dim_nontime_repr.txt b/lib/iris/tests/results/coord_api/str_repr/dim_nontime_repr.txt index cbf44b1dbb..c1d62f28e4 100644 --- a/lib/iris/tests/results/coord_api/str_repr/dim_nontime_repr.txt +++ b/lib/iris/tests/results/coord_api/str_repr/dim_nontime_repr.txt @@ -1 +1,11 @@ - \ No newline at end of file +DimCoord(array([-0.1278, -0.1269, -0.126 , -0.1251, -0.1242, -0.1233, -0.1224, + -0.1215, -0.1206, -0.1197], dtype=float32), bounds=array([[-0.12825, -0.12735], + [-0.12735, -0.12645], + [-0.12645, -0.12555], + [-0.12555, -0.12465], + [-0.12465, -0.12375], + [-0.12375, -0.12285], + [-0.12285, -0.12195], + [-0.12195, -0.12105], + [-0.12105, -0.12015], + [-0.12015, -0.11925]], dtype=float32), standard_name='grid_latitude', units=Unit('degrees'), coord_system=RotatedGeogCS(37.5, 177.5, ellipsoid=GeogCS(6371229.0))) \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/dim_nontime_str.txt b/lib/iris/tests/results/coord_api/str_repr/dim_nontime_str.txt index 95c0a60159..c1d62f28e4 100644 --- a/lib/iris/tests/results/coord_api/str_repr/dim_nontime_str.txt +++ b/lib/iris/tests/results/coord_api/str_repr/dim_nontime_str.txt @@ -1,14 +1,11 @@ -DimCoord : grid_latitude / (degrees) - points: [ - -0.1278, -0.1269, -0.126 , -0.1251, -0.1242, -0.1233, -0.1224, - -0.1215, -0.1206, -0.1197] - bounds: [ - [-0.12825, -0.12735], - [-0.12735, -0.12645], - ..., - [-0.12105, -0.12015], - [-0.12015, -0.11925]] - shape: (10,) bounds(10, 2) - dtype: float32 - standard_name: 'grid_latitude' - coord_system: RotatedGeogCS(37.5, 177.5, ellipsoid=GeogCS(6371229.0)) \ No newline at end of file +DimCoord(array([-0.1278, -0.1269, -0.126 , -0.1251, -0.1242, -0.1233, -0.1224, + -0.1215, -0.1206, -0.1197], dtype=float32), bounds=array([[-0.12825, -0.12735], + [-0.12735, -0.12645], + [-0.12645, -0.12555], + [-0.12555, -0.12465], + [-0.12465, -0.12375], + [-0.12375, -0.12285], + [-0.12285, -0.12195], + [-0.12195, -0.12105], + [-0.12105, -0.12015], + [-0.12015, -0.11925]], dtype=float32), standard_name='grid_latitude', units=Unit('degrees'), coord_system=RotatedGeogCS(37.5, 177.5, ellipsoid=GeogCS(6371229.0))) \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/dim_time_repr.txt b/lib/iris/tests/results/coord_api/str_repr/dim_time_repr.txt index ae1b86ae02..120546311f 100644 --- a/lib/iris/tests/results/coord_api/str_repr/dim_time_repr.txt +++ b/lib/iris/tests/results/coord_api/str_repr/dim_time_repr.txt @@ -1 +1,2 @@ - \ No newline at end of file +DimCoord(array([347921.16666667, 347921.33333333, 347921.5 , 347921.66666666, + 347921.83333333, 347922. ]), standard_name='time', units=Unit('hours since 1970-01-01 00:00:00', calendar='gregorian')) \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/dim_time_str.txt b/lib/iris/tests/results/coord_api/str_repr/dim_time_str.txt index 6b95b57215..9d209402e6 100644 --- a/lib/iris/tests/results/coord_api/str_repr/dim_time_str.txt +++ b/lib/iris/tests/results/coord_api/str_repr/dim_time_str.txt @@ -1,7 +1,2 @@ -DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) - points: [ - 2009-09-09 17:10:00, 2009-09-09 17:20:00, 2009-09-09 17:30:00, - 2009-09-09 17:40:00, 2009-09-09 17:50:00, 2009-09-09 18:00:00] - shape: (6,) - dtype: float64 - standard_name: 'time' \ No newline at end of file +DimCoord([2009-09-09 17:10:00, 2009-09-09 17:20:00, 2009-09-09 17:30:00, + 2009-09-09 17:40:00, 2009-09-09 17:50:00, 2009-09-09 18:00:00], standard_name='time', calendar='gregorian') \ No newline at end of file diff --git a/lib/iris/tests/results/derived/no_orog.__str__.txt b/lib/iris/tests/results/derived/no_orog.__str__.txt index de139592a6..e277b5d276 100644 --- a/lib/iris/tests/results/derived/no_orog.__str__.txt +++ b/lib/iris/tests/results/derived/no_orog.__str__.txt @@ -13,4 +13,4 @@ air_potential_temperature / (K) (time: 6; model_level_number: 70; grid_latit Scalar coordinates: forecast_period 0.0 hours Attributes: - source 'Iris test case' \ No newline at end of file + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/derived/removed_orog.__str__.txt b/lib/iris/tests/results/derived/removed_orog.__str__.txt index a9ef3bd017..0c24cded80 100644 --- a/lib/iris/tests/results/derived/removed_orog.__str__.txt +++ b/lib/iris/tests/results/derived/removed_orog.__str__.txt @@ -12,4 +12,4 @@ air_potential_temperature / (K) (time: 6; model_level_number: 70; grid_latit Scalar coordinates: forecast_period 0.0 hours Attributes: - source 'Iris test case' \ No newline at end of file + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/derived/removed_sigma.__str__.txt b/lib/iris/tests/results/derived/removed_sigma.__str__.txt index c3eaf48483..94e850ec62 100644 --- a/lib/iris/tests/results/derived/removed_sigma.__str__.txt +++ b/lib/iris/tests/results/derived/removed_sigma.__str__.txt @@ -12,4 +12,4 @@ air_potential_temperature / (K) (time: 6; model_level_number: 70; grid_latit Scalar coordinates: forecast_period 0.0 hours Attributes: - source 'Iris test case' \ No newline at end of file + source Iris test case \ No newline at end of file diff --git a/lib/iris/tests/results/trajectory/tri_polar_latitude_slice.cml b/lib/iris/tests/results/trajectory/tri_polar_latitude_slice.cml index 750d597493..701ca7beb7 100644 --- a/lib/iris/tests/results/trajectory/tri_polar_latitude_slice.cml +++ b/lib/iris/tests/results/trajectory/tri_polar_latitude_slice.cml @@ -76,25 +76,8 @@ - + @@ -103,26 +86,8 @@ - + @@ -144,6 +109,6 @@ - + diff --git a/lib/iris/tests/results/util/as_compatible_shape_collapsed.cml b/lib/iris/tests/results/util/as_compatible_shape_collapsed.cml new file mode 100644 index 0000000000..07eeb53157 --- /dev/null +++ b/lib/iris/tests/results/util/as_compatible_shape_collapsed.cml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/iris/tests/test_basic_maths.py b/lib/iris/tests/test_basic_maths.py index 24f2b89442..e753adbae8 100644 --- a/lib/iris/tests/test_basic_maths.py +++ b/lib/iris/tests/test_basic_maths.py @@ -249,7 +249,7 @@ def test_apply_ufunc(self): np.square, a, new_name="squared temperature", - new_unit=a.units**2, + new_unit=a.units ** 2, in_place=False, ) self.assertCMLApproxData(a, ("analysis", "apply_ufunc_original.cml")) @@ -259,14 +259,14 @@ def test_apply_ufunc(self): np.square, a, new_name="squared temperature", - new_unit=a.units**2, + new_unit=a.units ** 2, in_place=True, ) self.assertCMLApproxData(b, ("analysis", "apply_ufunc.cml")) self.assertCMLApproxData(a, ("analysis", "apply_ufunc.cml")) def vec_mag(u, v): - return math.sqrt(u**2 + v**2) + return math.sqrt(u ** 2 + v ** 2) c = a.copy() + 2 @@ -295,7 +295,7 @@ def test_apply_ufunc_fail(self): def test_ifunc(self): a = self.cube - my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units**2) + my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units ** 2) b = my_ifunc(a, new_name="squared temperature", in_place=False) self.assertCMLApproxData(a, ("analysis", "apply_ifunc_original.cml")) @@ -307,7 +307,7 @@ def test_ifunc(self): self.assertCMLApproxData(a, ("analysis", "apply_ifunc.cml")) def vec_mag(u, v): - return math.sqrt(u**2 + v**2) + return math.sqrt(u ** 2 + v ** 2) c = a.copy() + 2 @@ -347,7 +347,7 @@ def test_ifunc_init_fail(self): def test_ifunc_call_fail(self): a = self.cube - my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units**2) + my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units ** 2) # should now NOT fail because giving 2 arguments to an ifunc that # expects only one will now ignore the surplus argument and raise @@ -367,7 +367,7 @@ def test_ifunc_call_fail(self): my_ifunc(a) my_ifunc = iris.analysis.maths.IFunc( - lambda a: (a, a**2.0), lambda cube: cf_units.Unit("1") + lambda a: (a, a ** 2.0), lambda cube: cf_units.Unit("1") ) # should fail because data function returns a tuple @@ -553,9 +553,9 @@ def test_square_root(self): a.data = abs(a.data) a.units **= 2 - e = a**0.5 + e = a ** 0.5 - self.assertArrayAllClose(e.data, a.data**0.5) + self.assertArrayAllClose(e.data, a.data ** 0.5) self.assertCML(e, ("analysis", "sqrt.cml"), checksum=False) self.assertRaises(ValueError, iris.analysis.maths.exponentiate, a, 0.3) @@ -585,26 +585,26 @@ def test_apply_ufunc(self): np.square, a, new_name="more_thingness", - new_unit=a.units**2, + new_unit=a.units ** 2, in_place=False, ) - ans = a.data**2 + ans = a.data ** 2 self.assertArrayEqual(b.data, ans) self.assertEqual(b.name(), "more_thingness") self.assertEqual(b.units, cf_units.Unit("m^2")) def vec_mag(u, v): - return math.sqrt(u**2 + v**2) + return math.sqrt(u ** 2 + v ** 2) c = a.copy() + 2 vec_mag_ufunc = np.frompyfunc(vec_mag, 2, 1) b = iris.analysis.maths.apply_ufunc(vec_mag_ufunc, a, c) - ans = a.data**2 + c.data**2 - b2 = b**2 + ans = a.data ** 2 + c.data ** 2 + b2 = b ** 2 self.assertArrayAlmostEqual(b2.data, ans) @@ -617,17 +617,17 @@ def test_ifunc(self): a = self.cube a.units = cf_units.Unit("meters") - my_ifunc = iris.analysis.maths.IFunc(np.square, lambda x: x.units**2) + my_ifunc = iris.analysis.maths.IFunc(np.square, lambda x: x.units ** 2) b = my_ifunc(a, new_name="more_thingness", in_place=False) - ans = a.data**2 + ans = a.data ** 2 self.assertArrayEqual(b.data, ans) self.assertEqual(b.name(), "more_thingness") self.assertEqual(b.units, cf_units.Unit("m^2")) def vec_mag(u, v): - return math.sqrt(u**2 + v**2) + return math.sqrt(u ** 2 + v ** 2) c = a.copy() + 2 @@ -637,12 +637,12 @@ def vec_mag(u, v): ) b = my_ifunc(a, c) - ans = (a.data**2 + c.data**2) ** 0.5 + ans = (a.data ** 2 + c.data ** 2) ** 0.5 self.assertArrayAlmostEqual(b.data, ans) def vec_mag_data_func(u_data, v_data): - return np.sqrt(u_data**2 + v_data**2) + return np.sqrt(u_data ** 2 + v_data ** 2) vec_mag_ifunc = iris.analysis.maths.IFunc( vec_mag_data_func, lambda a, b: (a + b).units diff --git a/lib/iris/tests/test_coord_api.py b/lib/iris/tests/test_coord_api.py index 87270b524c..1b2ba56300 100644 --- a/lib/iris/tests/test_coord_api.py +++ b/lib/iris/tests/test_coord_api.py @@ -178,9 +178,7 @@ def test_complex(self): @tests.skip_data class TestCoord_ReprStr_nontime(tests.IrisTest): def setUp(self): - cube = iris.tests.stock.realistic_4d() - self.lat = cube.coord("grid_latitude")[:10] - self.height = cube.coord("level_height")[:10] + self.lat = iris.tests.stock.realistic_4d().coord("grid_latitude")[:10] def test_DimCoord_repr(self): self.assertRepr( @@ -189,7 +187,7 @@ def test_DimCoord_repr(self): def test_AuxCoord_repr(self): self.assertRepr( - self.height, ("coord_api", "str_repr", "aux_nontime_repr.txt") + self.lat, ("coord_api", "str_repr", "aux_nontime_repr.txt") ) def test_DimCoord_str(self): @@ -199,16 +197,14 @@ def test_DimCoord_str(self): def test_AuxCoord_str(self): self.assertString( - str(self.height), ("coord_api", "str_repr", "aux_nontime_str.txt") + str(self.lat), ("coord_api", "str_repr", "aux_nontime_str.txt") ) @tests.skip_data class TestCoord_ReprStr_time(tests.IrisTest): def setUp(self): - cube = iris.tests.stock.realistic_4d() - self.time = cube.coord("time") - self.fp = cube.coord("forecast_period") + self.time = iris.tests.stock.realistic_4d().coord("time") def test_DimCoord_repr(self): self.assertRepr( @@ -217,7 +213,7 @@ def test_DimCoord_repr(self): def test_AuxCoord_repr(self): self.assertRepr( - self.fp, ("coord_api", "str_repr", "aux_time_repr.txt") + self.time, ("coord_api", "str_repr", "aux_time_repr.txt") ) def test_DimCoord_str(self): @@ -227,7 +223,7 @@ def test_DimCoord_str(self): def test_AuxCoord_str(self): self.assertString( - str(self.fp), ("coord_api", "str_repr", "aux_time_str.txt") + str(self.time), ("coord_api", "str_repr", "aux_time_str.txt") ) @@ -236,29 +232,23 @@ def test_basic(self): a = iris.coords.AuxCoord( np.arange(10), "air_temperature", units="kelvin" ) - result = "\n".join( - [ - "AuxCoord : air_temperature / (kelvin)", - " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", - " shape: (10,)", - " dtype: int64", - " standard_name: 'air_temperature'", - ] + result = ( + "AuxCoord(" + "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," + " standard_name='air_temperature'," + " units=Unit('kelvin'))" ) self.assertEqual(result, str(a)) b = iris.coords.AuxCoord( list(range(10)), attributes={"monty": "python"} ) - result = "\n".join( - [ - "AuxCoord : unknown / (unknown)", - " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", - " shape: (10,)", - " dtype: int64", - " attributes:", - " monty 'python'", - ] + result = ( + "AuxCoord(" + "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," + " standard_name=None," + " units=Unit('unknown')," + " attributes={'monty': 'python'})" ) self.assertEqual(result, str(b)) @@ -286,15 +276,12 @@ def test_coord_system(self): units="kelvin", coord_system=iris.coord_systems.GeogCS(6000), ) - result = "\n".join( - [ - "AuxCoord : air_temperature / (kelvin)", - " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", - " shape: (10,)", - " dtype: int64", - " standard_name: 'air_temperature'", - " coord_system: GeogCS(6000.0)", - ] + result = ( + "AuxCoord(" + "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," + " standard_name='air_temperature'," + " units=Unit('kelvin')," + " coord_system=GeogCS(6000.0))" ) self.assertEqual(result, str(a)) @@ -305,20 +292,14 @@ def test_bounded(self): units="kelvin", bounds=np.arange(0, 20).reshape(10, 2), ) - result = "\n".join( - [ - "AuxCoord : air_temperature / (kelvin)", - " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", - " bounds: [", - " [ 0, 1],", - " [ 2, 3],", - " ...,", - " [16, 17],", - " [18, 19]]", - " shape: (10,) bounds(10, 2)", - " dtype: int64", - " standard_name: 'air_temperature'", - ] + result = ( + "AuxCoord(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])" + ", bounds=array([" + "[ 0, 1],\n [ 2, 3],\n [ 4, 5],\n " + "[ 6, 7],\n [ 8, 9],\n [10, 11],\n " + "[12, 13],\n [14, 15],\n [16, 17],\n " + "[18, 19]])" + ", standard_name='air_temperature', units=Unit('kelvin'))" ) self.assertEqual(result, str(a)) @@ -341,29 +322,23 @@ def test_basic(self): a = iris.coords.DimCoord( np.arange(10), "air_temperature", units="kelvin" ) - result = "\n".join( - [ - "DimCoord : air_temperature / (kelvin)", - " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", - " shape: (10,)", - " dtype: int64", - " standard_name: 'air_temperature'", - ] + result = ( + "DimCoord(" + "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," + " standard_name='air_temperature'," + " units=Unit('kelvin'))" ) self.assertEqual(result, str(a)) b = iris.coords.DimCoord( list(range(10)), attributes={"monty": "python"} ) - result = "\n".join( - [ - "DimCoord : unknown / (unknown)", - " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", - " shape: (10,)", - " dtype: int64", - " attributes:", - " monty 'python'", - ] + result = ( + "DimCoord(" + "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," + " standard_name=None," + " units=Unit('unknown')," + " attributes={'monty': 'python'})" ) self.assertEqual(result, str(b)) @@ -391,15 +366,12 @@ def test_coord_system(self): units="kelvin", coord_system=iris.coord_systems.GeogCS(6000), ) - result = "\n".join( - [ - "DimCoord : air_temperature / (kelvin)", - " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", - " shape: (10,)", - " dtype: int64", - " standard_name: 'air_temperature'", - " coord_system: GeogCS(6000.0)", - ] + result = ( + "DimCoord(" + "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," + " standard_name='air_temperature'," + " units=Unit('kelvin')," + " coord_system=GeogCS(6000.0))" ) self.assertEqual(result, str(a)) @@ -410,20 +382,14 @@ def test_bounded(self): units="kelvin", bounds=np.arange(0, 20).reshape(10, 2), ) - result = "\n".join( - [ - "DimCoord : air_temperature / (kelvin)", - " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", - " bounds: [", - " [ 0, 1],", - " [ 2, 3],", - " ...,", - " [16, 17],", - " [18, 19]]", - " shape: (10,) bounds(10, 2)", - " dtype: int64", - " standard_name: 'air_temperature'", - ] + result = ( + "DimCoord(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])" + ", bounds=array([" + "[ 0, 1],\n [ 2, 3],\n [ 4, 5],\n " + "[ 6, 7],\n [ 8, 9],\n [10, 11],\n " + "[12, 13],\n [14, 15],\n [16, 17],\n " + "[18, 19]])" + ", standard_name='air_temperature', units=Unit('kelvin'))" ) self.assertEqual(result, str(a)) diff --git a/lib/iris/tests/test_coordsystem.py b/lib/iris/tests/test_coordsystem.py index 4229125969..2c0569f0d6 100644 --- a/lib/iris/tests/test_coordsystem.py +++ b/lib/iris/tests/test_coordsystem.py @@ -188,29 +188,6 @@ def test_as_cartopy_globe(self): self.assertEqual(res, expected) -class Test_GeogCS_as_cartopy_projection(tests.IrisTest): - def test_as_cartopy_projection(self): - geogcs_args = { - "semi_major_axis": 6543210, - "semi_minor_axis": 6500000, - "longitude_of_prime_meridian": 30, - } - cs = GeogCS(**geogcs_args) - res = cs.as_cartopy_projection() - - globe = ccrs.Globe( - semimajor_axis=geogcs_args["semi_major_axis"], - semiminor_axis=geogcs_args["semi_minor_axis"], - ellipse=None, - ) - expected = ccrs.PlateCarree( - globe=globe, - central_longitude=geogcs_args["longitude_of_prime_meridian"], - ) - - self.assertEqual(res, expected) - - class Test_GeogCS_as_cartopy_crs(tests.IrisTest): def test_as_cartopy_crs(self): cs = GeogCS(6543210, 6500000) diff --git a/lib/iris/tests/test_file_save.py b/lib/iris/tests/test_file_save.py index 3b751cfcbe..2b5619c656 100644 --- a/lib/iris/tests/test_file_save.py +++ b/lib/iris/tests/test_file_save.py @@ -13,7 +13,6 @@ from io import StringIO import os -import pathlib import unittest import iris @@ -98,23 +97,6 @@ def test_filename(self): CHKSUM_ERR.format(self.ext), ) - def test_filename_path_object(self): - # Save using iris.save and pp.save, passing filename for - # iris.save as pathlib.Path - save_by_filename( - self.temp_filename1, - pathlib.Path(self.temp_filename2), - self.cube1, - pp.save, - ) - - # Compare files - self.assertEqual( - self.file_checksum(self.temp_filename2), - self.file_checksum(self.temp_filename1), - CHKSUM_ERR.format(self.ext), - ) - def test_filehandle(self): # Save using iris.save and pp.save save_by_filehandle( diff --git a/lib/iris/tests/test_io_init.py b/lib/iris/tests/test_io_init.py index d33b76ddeb..e88eaabaed 100644 --- a/lib/iris/tests/test_io_init.py +++ b/lib/iris/tests/test_io_init.py @@ -12,58 +12,37 @@ import iris.tests as tests # isort:skip from io import BytesIO -from pathlib import Path import iris.fileformats as iff import iris.io class TestDecodeUri(tests.IrisTest): - def test_decode_uri__str(self): + def test_decode_uri(self): tests = { - (uri := "/data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp"): ( + "/data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp": ( "file", - uri, + "/data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp", ), - (uri := r"C:\data\local\someDir\PP\COLPEX\COLPEX_16a_pj001.pp"): ( + r"C:\data\local\someDir\PP\COLPEX\COLPEX_16a_pj001.pp": ( "file", - uri, + r"C:\data\local\someDir\PP\COLPEX\COLPEX_16a_pj001.pp", ), - ( - uri := "file:///data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp" - ): ( - uri[:4], - uri[5:], - ), - (uri := "http://www.somehost.com:8080/resource/thing.grib"): ( - uri[:4], - uri[5:], - ), - (uri := "/data/local/someDir/2013-11-25T13:49:17.632797"): ( + "file:///data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp": ( "file", - uri, + "///data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp", ), - } - for uri, expected in tests.items(): - self.assertEqual(expected, iris.io.decode_uri(uri)) - - def test_decode_uri__path(self): - tests = { - (uri := "/data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp"): ( - "file", - uri, - ), - (uri := r"C:\data\local\someDir\PP\COLPEX\COLPEX_16a_pj001.pp"): ( - "file", - uri, + "http://www.somehost.com:8080/resource/thing.grib": ( + "http", + "//www.somehost.com:8080/resource/thing.grib", ), - (uri := "/data/local/someDir/2013-11-25T13:49:17.632797"): ( + "/data/local/someDir/2013-11-25T13:49:17.632797": ( "file", - uri, + "/data/local/someDir/2013-11-25T13:49:17.632797", ), } - for uri, expected in tests.items(): - self.assertEqual(expected, iris.io.decode_uri(Path(uri))) + for uri, pair in tests.items(): + self.assertEqual(pair, iris.io.decode_uri(uri)) class TestFileFormatPicker(tests.IrisTest): diff --git a/lib/iris/tests/test_load.py b/lib/iris/tests/test_load.py index 86ff2f1ece..0674768a54 100644 --- a/lib/iris/tests/test_load.py +++ b/lib/iris/tests/test_load.py @@ -11,8 +11,6 @@ # import iris tests first so that some things can be initialised before importing anything else import iris.tests as tests # isort:skip -import pathlib - import iris import iris.io @@ -24,13 +22,6 @@ def test_normal(self): cubes = iris.load(paths) self.assertEqual(len(cubes), 1) - def test_path_object(self): - paths = ( - pathlib.Path(tests.get_data_path(["PP", "aPPglob1", "global.pp"])), - ) - cubes = iris.load(paths) - self.assertEqual(len(cubes), 1) - def test_nonexist(self): paths = ( tests.get_data_path(["PP", "aPPglob1", "global.pp"]), @@ -80,12 +71,6 @@ def test_normal(self): paths = (tests.get_data_path(["PP", "aPPglob1", "global.pp"]),) _ = iris.load_cube(paths) - def test_path_object(self): - paths = ( - pathlib.Path(tests.get_data_path(["PP", "aPPglob1", "global.pp"])), - ) - _ = iris.load_cube(paths) - def test_not_enough(self): paths = (tests.get_data_path(["PP", "aPPglob1", "global.pp"]),) with self.assertRaises(iris.exceptions.ConstraintMismatchError): @@ -107,13 +92,6 @@ def test_normal(self): cubes = iris.load_cubes(paths) self.assertEqual(len(cubes), 1) - def test_path_object(self): - paths = ( - pathlib.Path(tests.get_data_path(["PP", "aPPglob1", "global.pp"])), - ) - cubes = iris.load_cubes(paths) - self.assertEqual(len(cubes), 1) - def test_not_enough(self): paths = (tests.get_data_path(["PP", "aPPglob1", "global.pp"]),) with self.assertRaises(iris.exceptions.ConstraintMismatchError): @@ -133,21 +111,6 @@ def test_too_many(self): iris.load_cube(paths) -@tests.skip_data -class TestLoadRaw(tests.IrisTest): - def test_normal(self): - paths = (tests.get_data_path(["PP", "aPPglob1", "global.pp"]),) - cubes = iris.load_raw(paths) - self.assertEqual(len(cubes), 1) - - def test_path_object(self): - paths = ( - pathlib.Path(tests.get_data_path(["PP", "aPPglob1", "global.pp"])), - ) - cubes = iris.load_raw(paths) - self.assertEqual(len(cubes), 1) - - class TestOpenDAP(tests.IrisTest): def test_load(self): # Check that calling iris.load_* with a http URI triggers a call to diff --git a/lib/iris/tests/test_mapping.py b/lib/iris/tests/test_mapping.py index a71385b5bc..06bedd497b 100644 --- a/lib/iris/tests/test_mapping.py +++ b/lib/iris/tests/test_mapping.py @@ -211,10 +211,7 @@ def test_grid(self): def test_default_projection_and_extent(self): self.assertEqual( - iplt.default_projection(self.cube), - ccrs.PlateCarree( - globe=self.cube.coord_system("CoordSystem").as_cartopy_globe() - ), + iplt.default_projection(self.cube), ccrs.PlateCarree() ) np_testing.assert_array_almost_equal( iplt.default_projection_extent(self.cube), diff --git a/lib/iris/tests/test_pickling.py b/lib/iris/tests/test_pickling.py index 26247e795b..e01f791d03 100644 --- a/lib/iris/tests/test_pickling.py +++ b/lib/iris/tests/test_pickling.py @@ -16,10 +16,9 @@ import pickle import cf_units -import numpy as np import iris -from iris._lazy_data import as_concrete_data, as_lazy_data +from iris._lazy_data import as_concrete_data class TestPickle(tests.IrisTest): @@ -77,14 +76,6 @@ def test_cube_with_coord_points(self): _, recon_cube = next(self.pickle_then_unpickle(cube)) self.assertEqual(recon_cube, cube) - def test_cube_with_deferred_unit_conversion(self): - real_data = np.arange(12.0).reshape((3, 4)) - lazy_data = as_lazy_data(real_data) - cube = iris.cube.Cube(lazy_data, units="m") - cube.convert_units("ft") - _, recon_cube = next(self.pickle_then_unpickle(cube)) - self.assertEqual(recon_cube, cube) - @tests.skip_data def test_cubelist_pickle(self): cubelist = iris.load( diff --git a/lib/iris/tests/test_util.py b/lib/iris/tests/test_util.py index ec7f8d1023..cf921ae210 100644 --- a/lib/iris/tests/test_util.py +++ b/lib/iris/tests/test_util.py @@ -276,5 +276,103 @@ def test_output_file(self): self.assertFilesEqual(filename, "incompatible_cubes.str.txt") +@tests.skip_data +class TestAsCompatibleShape(tests.IrisTest): + def test_slice(self): + cube = tests.stock.realistic_4d() + sliced = cube[1, :, 2, :-2] + expected = cube[1:2, :, 2:3, :-2] + res = iris.util.as_compatible_shape(sliced, cube) + self.assertEqual(res, expected) + + def test_transpose(self): + cube = tests.stock.realistic_4d() + transposed = cube.copy() + transposed.transpose() + expected = cube + res = iris.util.as_compatible_shape(transposed, cube) + self.assertEqual(res, expected) + + def test_slice_and_transpose(self): + cube = tests.stock.realistic_4d() + sliced_and_transposed = cube[1, :, 2, :-2] + sliced_and_transposed.transpose() + expected = cube[1:2, :, 2:3, :-2] + res = iris.util.as_compatible_shape(sliced_and_transposed, cube) + self.assertEqual(res, expected) + + def test_collapsed(self): + cube = tests.stock.realistic_4d() + collapsed = cube.collapsed("model_level_number", iris.analysis.MEAN) + expected_shape = list(cube.shape) + expected_shape[1] = 1 + expected_data = collapsed.data.reshape(expected_shape) + res = iris.util.as_compatible_shape(collapsed, cube) + self.assertCML( + res, ("util", "as_compatible_shape_collapsed.cml"), checksum=False + ) + self.assertMaskedArrayEqual(expected_data, res.data) + + def test_reduce_dimensionality(self): + # Test that as_compatible_shape() can demote + # length one dimensions to scalars. + cube = tests.stock.realistic_4d() + src = cube[:, 2:3] + expected = reduced = cube[:, 2] + res = iris.util.as_compatible_shape(src, reduced) + self.assertEqual(res, expected) + + def test_anonymous_dims(self): + cube = tests.stock.realistic_4d() + # Move all coords from dim_coords to aux_coords. + for coord in cube.dim_coords: + dim = cube.coord_dims(coord) + cube.remove_coord(coord) + cube.add_aux_coord(coord, dim) + + sliced = cube[1, :, 2, :-2] + expected = cube[1:2, :, 2:3, :-2] + res = iris.util.as_compatible_shape(sliced, cube) + self.assertEqual(res, expected) + + def test_scalar_auxcoord(self): + def dim_to_aux(cube, coord_name): + """Convert coordinate on cube from DimCoord to AuxCoord.""" + coord = cube.coord(coord_name) + coord = iris.coords.AuxCoord.from_coord(coord) + cube.replace_coord(coord) + + cube = tests.stock.realistic_4d() + src = cube[:, :, 3] + dim_to_aux(src, "grid_latitude") + expected = cube[:, :, 3:4] + dim_to_aux(expected, "grid_latitude") + res = iris.util.as_compatible_shape(src, cube) + self.assertEqual(res, expected) + + def test_2d_auxcoord_transpose(self): + dim_coord1 = iris.coords.DimCoord(range(3), long_name="first_dim") + dim_coord2 = iris.coords.DimCoord(range(4), long_name="second_dim") + aux_coord_2d = iris.coords.AuxCoord( + np.arange(12).reshape(3, 4), long_name="spanning" + ) + aux_coord_2d_T = iris.coords.AuxCoord( + np.arange(12).reshape(3, 4).T, long_name="spanning" + ) + src = iris.cube.Cube( + np.ones((3, 4)), + dim_coords_and_dims=[(dim_coord1, 0), (dim_coord2, 1)], + aux_coords_and_dims=[(aux_coord_2d, (0, 1))], + ) + target = iris.cube.Cube( + np.ones((4, 3)), + dim_coords_and_dims=[(dim_coord1, 1), (dim_coord2, 0)], + aux_coords_and_dims=[(aux_coord_2d_T, (0, 1))], + ) + + res = iris.util.as_compatible_shape(src, target) + self.assertEqual(res[0], target[0]) + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py b/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py index ecaa028ab3..a44ccb32bd 100644 --- a/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py +++ b/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py @@ -16,13 +16,11 @@ import numpy as np from iris import load_cube -from iris.analysis._area_weighted import ( - AreaWeightedRegridder, - _regrid_area_weighted_rectilinear_src_and_grid__prepare, -) +from iris.analysis._area_weighted import AreaWeightedRegridder from iris.coord_systems import GeogCS from iris.coords import DimCoord from iris.cube import Cube +import iris.experimental.regrid as eregrid class Test(tests.IrisTest): @@ -48,17 +46,19 @@ def extract_grid(self, cube): def check_mdtol(self, mdtol=None): src_grid, target_grid = self.grids() # Get _regrid_info result - _regrid_info = _regrid_area_weighted_rectilinear_src_and_grid__prepare( - src_grid, target_grid + _regrid_info = ( + eregrid._regrid_area_weighted_rectilinear_src_and_grid__prepare( + src_grid, target_grid + ) ) self.assertEqual(len(_regrid_info), 10) with mock.patch( - "iris.analysis._area_weighted." + "iris.experimental.regrid." "_regrid_area_weighted_rectilinear_src_and_grid__prepare", return_value=_regrid_info, ) as prepare: with mock.patch( - "iris.analysis._area_weighted." + "iris.experimental.regrid." "_regrid_area_weighted_rectilinear_src_and_grid__perform", return_value=mock.sentinel.result, ) as perform: @@ -253,6 +253,7 @@ class TestLazy(tests.IrisTest): # Setup def setUp(self) -> None: # Prepare a cube and a template + cube_file_path = tests.get_data_path( ["NetCDF", "regrid", "regrid_xyt.nc"] ) diff --git a/lib/iris/tests/unit/analysis/cartography/test_project.py b/lib/iris/tests/unit/analysis/cartography/test_project.py index 8649cc55ea..4dfa1a4a2e 100644 --- a/lib/iris/tests/unit/analysis/cartography/test_project.py +++ b/lib/iris/tests/unit/analysis/cartography/test_project.py @@ -52,7 +52,7 @@ def setUp(self): 1, ) - self.tcs = iris.coord_systems.GeogCS(6371229) + self.tcs = iris.coord_systems.GeogCS(6000000) def test_is_iris_coord_system(self): res, _ = project(self.cube, self.tcs) diff --git a/lib/iris/tests/unit/analysis/cartography/test_rotate_grid_vectors.py b/lib/iris/tests/unit/analysis/cartography/test_rotate_grid_vectors.py index f5c882a983..e9294f27dc 100644 --- a/lib/iris/tests/unit/analysis/cartography/test_rotate_grid_vectors.py +++ b/lib/iris/tests/unit/analysis/cartography/test_rotate_grid_vectors.py @@ -33,7 +33,7 @@ def _check_angles_calculation( u_cube.rename("dx") u_cube.data[...] = 0 v_cube = u_cube.copy() - v_cube.rename("dy") + v_cube.name("dy") # Define 6 different vectors, repeated in each data row. in_vu = np.array([(0, 1), (2, -1), (-1, -1), (-3, 1), (2, 0), (0, 0)]) @@ -71,7 +71,7 @@ def _check_angles_calculation( ang_diffs = out_angs - expect_angs # Fix for null vectors, and +/-360 differences. ang_diffs[np.abs(out_mags) < 0.001] = 0.0 - ang_diffs[np.isclose(np.abs(ang_diffs), 360.0)] = 0.0 + ang_diffs = ang_diffs % 360.0 # Check that any differences are very small. self.assertArrayAllClose(ang_diffs, 0.0) @@ -97,7 +97,7 @@ def test_angles_from_grid(self): u_cube.rename("dx") u_cube.data[...] = 1.0 v_cube = u_cube.copy() - v_cube.rename("dy") + v_cube.name("dy") v_cube.data[...] = 0.0 # Setup a fake angles result from the inner call to 'gridcell_angles'. diff --git a/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py b/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py index eafaa20ec8..9e3af90603 100644 --- a/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py +++ b/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py @@ -343,8 +343,8 @@ def test_orig_coords(self): def test_magnitude_preservation(self): u, v = self._uv_cubes_limited_extent() ut, vt = rotate_winds(u, v, iris.coord_systems.OSGB()) - orig_sq_mag = u.data**2 + v.data**2 - res_sq_mag = ut.data**2 + vt.data**2 + orig_sq_mag = u.data ** 2 + v.data ** 2 + res_sq_mag = ut.data ** 2 + vt.data ** 2 self.assertArrayAllClose(orig_sq_mag, res_sq_mag, rtol=5e-4) def test_data_values(self): @@ -437,9 +437,9 @@ def test_rotated_to_osgb(self): self.assertArrayEqual(expected_mask, vt.data.mask) # Check unmasked values have sufficiently small error in mag. - expected_mag = np.sqrt(u.data**2 + v.data**2) + expected_mag = np.sqrt(u.data ** 2 + v.data ** 2) # Use underlying data to ignore mask in calculation. - res_mag = np.sqrt(ut.data.data**2 + vt.data.data**2) + res_mag = np.sqrt(ut.data.data ** 2 + vt.data.data ** 2) # Calculate percentage error (note there are no zero magnitudes # so we can divide safely). anom = 100.0 * np.abs(res_mag - expected_mag) / expected_mag diff --git a/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py b/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py index a018507fb3..f0dba83748 100644 --- a/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py +++ b/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py @@ -33,7 +33,7 @@ def setUp(self): self.xs, self.ys = np.meshgrid(self.x.points, self.y.points) def transformation(x, y): - return x + y**2 + return x + y ** 2 # Construct a function which adds dimensions to the 2D data array # so that we can test higher dimensional functionality. diff --git a/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py b/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py index 88a88be567..1fa579ef94 100644 --- a/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py +++ b/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py @@ -286,15 +286,13 @@ def test_class_cellmeasuremetadata(self): ) def test_class_connectivitymetadata(self): - self.args.update( - dict(cf_role=None, start_index=None, location_axis=None) - ) + self.args.update(dict(cf_role=None, start_index=None, src_dim=None)) metadata = ConnectivityMetadata(**self.args) self.item.metadata = metadata expected = metadata._asdict() del expected["cf_role"] del expected["start_index"] - del expected["location_axis"] + del expected["src_dim"] self.assertEqual(self.item._metadata_manager.values, expected) self.assertIsNot( self.item._metadata_manager.attributes, metadata.attributes diff --git a/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py b/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py index 0c20f16f5a..b7c59ff566 100644 --- a/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py +++ b/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py @@ -36,7 +36,7 @@ def test_vectorise_call(self): # The reason we use numpy.vectorize is to support multi-dimensional # coordinate points. def fn(coord, v): - return v**2 + return v ** 2 with mock.patch( "numpy.vectorize", return_value=self.vectorised diff --git a/lib/iris/tests/unit/coords/test_AncillaryVariable.py b/lib/iris/tests/unit/coords/test_AncillaryVariable.py index 4d520ac414..e94ad0cf70 100644 --- a/lib/iris/tests/unit/coords/test_AncillaryVariable.py +++ b/lib/iris/tests/unit/coords/test_AncillaryVariable.py @@ -433,18 +433,11 @@ def test_non_time_values(self): units="m", attributes={"notes": "Measured from sea level"}, ) - expected = "\n".join( - [ - "AncillaryVariable : height / (m)", - " data: [2, 5, 9]", - " shape: (3,)", - " dtype: int64", - " standard_name: 'height'", - " long_name: 'height of detector'", - " var_name: 'height'", - " attributes:", - " notes 'Measured from sea level'", - ] + expected = ( + "AncillaryVariable(array([2, 5, 9]), " + "standard_name='height', units=Unit('m'), " + "long_name='height of detector', var_name='height', " + "attributes={'notes': 'Measured from sea level'})" ) self.assertEqual(expected, ancillary_var.__str__()) @@ -454,20 +447,11 @@ def test_time_values(self): units="hours since 1970-01-01 01:00", long_name="time of previous valid detection", ) - expected = "\n".join( - [ - ( - "AncillaryVariable : time of previous valid detection / " - "(hours since 1970-01-01 01:00, gregorian calendar)" - ), - ( - " data: [1970-01-01 03:00:00, 1970-01-01 06:00:00, " - "1970-01-01 10:00:00]" - ), - " shape: (3,)", - " dtype: int64", - " long_name: 'time of previous valid detection'", - ] + expected = ( + "AncillaryVariable([1970-01-01 03:00:00, " + "1970-01-01 06:00:00, 1970-01-01 10:00:00], " + "standard_name=None, calendar='gregorian', " + "long_name='time of previous valid detection')" ) self.assertEqual(expected, ancillary_var.__str__()) @@ -482,7 +466,12 @@ def test_non_time_values(self): units="m", attributes={"notes": "Measured from sea level"}, ) - expected = "" + expected = ( + "AncillaryVariable(array([2, 5, 9]), " + "standard_name='height', units=Unit('m'), " + "long_name='height of detector', var_name='height', " + "attributes={'notes': 'Measured from sea level'})" + ) self.assertEqual(expected, ancillary_var.__repr__()) def test_time_values(self): @@ -492,8 +481,10 @@ def test_time_values(self): long_name="time of previous valid detection", ) expected = ( - "" + "AncillaryVariable(array([2, 5, 9]), standard_name=None, " + "units=Unit('hours since 1970-01-01 01:00', " + "calendar='gregorian'), " + "long_name='time of previous valid detection')" ) self.assertEqual(expected, ancillary_var.__repr__()) diff --git a/lib/iris/tests/unit/coords/test_CellMeasure.py b/lib/iris/tests/unit/coords/test_CellMeasure.py index 0bd66c6e98..c5016e6c73 100644 --- a/lib/iris/tests/unit/coords/test_CellMeasure.py +++ b/lib/iris/tests/unit/coords/test_CellMeasure.py @@ -93,29 +93,30 @@ def test_copy(self): copy_measure = self.measure.copy(new_vals) self.assertArrayEqual(copy_measure.data, new_vals) + def test_repr_other_metadata(self): + expected = ( + ", long_name='measured_area', " + "var_name='area', attributes={'notes': '1m accuracy'}" + ) + self.assertEqual(self.measure._repr_other_metadata(), expected) + def test___str__(self): - expected = "\n".join( - [ - "CellMeasure : cell_area / (m^2)", - " data: [10., 12., 16., 9.]", - " shape: (4,)", - " dtype: float64", - " standard_name: 'cell_area'", - " long_name: 'measured_area'", - " var_name: 'area'", - " attributes:", - " notes '1m accuracy'", - " measure: 'area'", - ] + expected = ( + "CellMeasure(array([10., 12., 16., 9.]), " + "measure='area', standard_name='cell_area', " + "units=Unit('m^2'), long_name='measured_area', " + "var_name='area', attributes={'notes': '1m accuracy'})" ) self.assertEqual(self.measure.__str__(), expected) def test___repr__(self): expected = ( - "" + "CellMeasure(array([10., 12., 16., 9.]), " + "measure='area', standard_name='cell_area', " + "units=Unit('m^2'), long_name='measured_area', " + "var_name='area', attributes={'notes': '1m accuracy'})" ) - self.assertEqual(expected, self.measure.__repr__()) + self.assertEqual(self.measure.__repr__(), expected) def test__eq__(self): self.assertEqual(self.measure, self.measure) diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index 43170b6c4e..640dbcd131 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -883,14 +883,9 @@ def test_short_time_interval(self): coord = DimCoord( [5], standard_name="time", units="days since 1970-01-01" ) - expected = "\n".join( - [ - "DimCoord : time / (days since 1970-01-01, gregorian calendar)", - " points: [1970-01-06 00:00:00]", - " shape: (1,)", - " dtype: int64", - " standard_name: 'time'", - ] + expected = ( + "DimCoord([1970-01-06 00:00:00], standard_name='time', " + "calendar='gregorian')" ) result = coord.__str__() self.assertEqual(expected, result) @@ -900,17 +895,11 @@ def test_short_time_interval__bounded(self): [5, 6], standard_name="time", units="days since 1970-01-01" ) coord.guess_bounds() - expected = "\n".join( - [ - "DimCoord : time / (days since 1970-01-01, gregorian calendar)", - " points: [1970-01-06 00:00:00, 1970-01-07 00:00:00]", - " bounds: [", - " [1970-01-05 12:00:00, 1970-01-06 12:00:00],", - " [1970-01-06 12:00:00, 1970-01-07 12:00:00]]", - " shape: (2,) bounds(2, 2)", - " dtype: int64", - " standard_name: 'time'", - ] + expected = ( + "DimCoord([1970-01-06 00:00:00, 1970-01-07 00:00:00], " + "bounds=[[1970-01-05 12:00:00, 1970-01-06 12:00:00],\n" + " [1970-01-06 12:00:00, 1970-01-07 12:00:00]], " + "standard_name='time', calendar='gregorian')" ) result = coord.__str__() self.assertEqual(expected, result) @@ -919,15 +908,7 @@ def test_long_time_interval(self): coord = DimCoord( [5], standard_name="time", units="years since 1970-01-01" ) - expected = "\n".join( - [ - "DimCoord : time / (years since 1970-01-01, gregorian calendar)", - " points: [5]", - " shape: (1,)", - " dtype: int64", - " standard_name: 'time'", - ] - ) + expected = "DimCoord([5], standard_name='time', calendar='gregorian')" result = coord.__str__() self.assertEqual(expected, result) @@ -936,31 +917,16 @@ def test_long_time_interval__bounded(self): [5, 6], standard_name="time", units="years since 1970-01-01" ) coord.guess_bounds() - expected = "\n".join( - [ - "DimCoord : time / (years since 1970-01-01, gregorian calendar)", - " points: [5, 6]", - " bounds: [", - " [4.5, 5.5],", - " [5.5, 6.5]]", - " shape: (2,) bounds(2, 2)", - " dtype: int64", - " standard_name: 'time'", - ] + expected = ( + "DimCoord([5 6], bounds=[[4.5 5.5]\n [5.5 6.5]], " + "standard_name='time', calendar='gregorian')" ) result = coord.__str__() self.assertEqual(expected, result) def test_non_time_unit(self): coord = DimCoord([1.0]) - expected = "\n".join( - [ - "DimCoord : unknown / (unknown)", - " points: [1.]", - " shape: (1,)", - " dtype: float64", - ] - ) + expected = repr(coord) result = coord.__str__() self.assertEqual(expected, result) diff --git a/lib/iris/tests/unit/coords/test__DimensionalMetadata.py b/lib/iris/tests/unit/coords/test__DimensionalMetadata.py index fd10a6f264..82bd51a8af 100644 --- a/lib/iris/tests/unit/coords/test__DimensionalMetadata.py +++ b/lib/iris/tests/unit/coords/test__DimensionalMetadata.py @@ -9,22 +9,7 @@ # importing anything else. import iris.tests as tests # isort:skip - -from cf_units import Unit -import numpy as np - -import iris._lazy_data as lazy -from iris.coord_systems import GeogCS -from iris.coords import ( - AncillaryVariable, - AuxCoord, - CellMeasure, - DimCoord, - _DimensionalMetadata, -) -from iris.experimental.ugrid.mesh import Connectivity -from iris.tests.stock import climatology_3d as cube_with_climatology -from iris.tests.stock.mesh import sample_meshcoord +from iris.coords import _DimensionalMetadata class Test___init____abstractmethod(tests.IrisTest): @@ -37,1046 +22,5 @@ def test(self): _ = _DimensionalMetadata(0) -class Mixin__string_representations: - """ - Common testcode for generic `__str__`, `__repr__` and `summary` methods. - - Effectively, __str__ and __repr__ are thin wrappers around `summary`. - These are used by all the subclasses : notably Coord/DimCoord/AuxCoord, - but also AncillaryVariable, CellMeasure and MeshCoord. - - There are a lot of different aspects to consider: - - * different object classes with different class-specific properties - * changing with array sizes + dimensionalities - * masked data - * data types : int, float, string and (special) dates - * for Coords, handling of bounds - * "summary" controls (also can be affected by numpy printoptions). - - NOTE: since the details of formatting are important to us here, the basic - test method is to check printout results against an exact 'snapshot' - embedded (visibly) in the test itself. - - """ - - def repr_str_strings(self, dm, linewidth=55): - """ - Return a simple combination of repr and str printouts. - - N.B. we control linewidth to make the outputs easier to compare. - """ - with np.printoptions(linewidth=linewidth): - result = repr(dm) + "\n" + str(dm) - return result - - def sample_data(self, datatype=float, units="m", shape=(5,), masked=False): - """Make a sample data array for a test _DimensionalMetadata object.""" - # Get an actual Unit - units = Unit(units) - if units.calendar: - # fix string datatypes for date-based units - datatype = float - - # Get a dtype - dtype = np.dtype(datatype) - - # Make suitable test values for type/shape/masked - length = int(np.prod(shape)) - if dtype.kind == "U": - # String content. - digit_strs = [str(i) * (i + 1) for i in range(0, 10)] - if length < 10: - # ['0', '11', '222, '3333', ..] - values = np.array(digit_strs[:length]) - else: - # [... '9999999999', '0', '11' ....] - indices = [(i % 10) for i in range(length)] - values = np.array(digit_strs)[indices] - else: - # numeric content : a simple [0, 1, 2 ...] - values = np.arange(length).astype(dtype) - - if masked: - if np.prod(shape) >= 3: - # Mask 1 in 3 points : [x -- x x -- x ...] - i_firstmasked = 1 - else: - # Few points, mask 1 in 3 starting at 0 [-- x x -- x x -- ...] - i_firstmasked = 0 - masked_points = [(i % 3) == i_firstmasked for i in range(length)] - values = np.ma.masked_array(values, mask=masked_points) - - values = values.reshape(shape) - return values - - # Make a sample Coord, as _DimensionalMetadata is abstract and this is the - # obvious concrete subclass to use for testing - def sample_coord( - self, - datatype=float, - dates=False, - units="m", - long_name="x", - shape=(5,), - masked=False, - bounded=False, - dimcoord=False, - lazy_points=False, - lazy_bounds=False, - *coord_args, - **coord_kwargs, - ): - if masked: - dimcoord = False - if dates: - # Use a pre-programmed date unit. - units = Unit("days since 1970-03-5") - if not isinstance(units, Unit): - # This operation is *not* a no-op, it will wipe calendars ! - units = Unit(units) - values = self.sample_data( - datatype=datatype, units=units, shape=shape, masked=masked - ) - cls = DimCoord if dimcoord else AuxCoord - coord = cls( - points=values, - units=units, - long_name=long_name, - *coord_args, - **coord_kwargs, - ) - if bounded or lazy_bounds: - if shape == (1,): - # Guess-bounds doesn't work ! - val = coord.points[0] - bounds = [val - 10, val + 10] - # NB preserve masked/unmasked : avoid converting masks to NaNs - if np.ma.isMaskedArray(coord.points): - array = np.ma.array - else: - array = np.array - coord.bounds = array(bounds) - else: - coord.guess_bounds() - if lazy_points: - coord.points = lazy.as_lazy_data(coord.points) - if lazy_bounds: - coord.bounds = lazy.as_lazy_data(coord.bounds) - return coord - - def coord_representations(self, *args, **kwargs): - """ - Create a test coord and return its string representations. - - Pass args+kwargs to 'sample_coord' and return the 'repr_str_strings'. - - """ - coord = self.sample_coord(*args, **kwargs) - return self.repr_str_strings(coord) - - def assertLines(self, list_of_expected_lines, string_result): - """ - Assert equality between a result and expected output lines. - - For convenience, the 'expected lines' are joined with a '\\n', - because a list of strings is nicer to construct in code. - They should then match the actual result, which is a simple string. - - """ - self.assertEqual(list_of_expected_lines, string_result.split("\n")) - - -class Test__print_common(Mixin__string_representations, tests.IrisTest): - """ - Test aspects of __str__ and __repr__ output common to all - _DimensionalMetadata instances. - I.E. those from CFVariableMixin, plus values array (data-manager). - - Aspects : - * standard_name: - * long_name: - * var_name: - * attributes - * units - * shape - * dtype - - """ - - def test_simple(self): - result = self.coord_representations() - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0., 1., 2., 3., 4.]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_minimal(self): - result = self.coord_representations( - long_name=None, units=None, shape=(1,) - ) - expected = [ - "", - "AuxCoord : unknown / (unknown)", - " points: [0.]", - " shape: (1,)", - " dtype: float64", - ] - self.assertLines(expected, result) - - def test_names(self): - result = self.coord_representations( - standard_name="height", long_name="this", var_name="x_var" - ) - expected = [ - "", - "AuxCoord : height / (m)", - " points: [0., 1., 2., 3., 4.]", - " shape: (5,)", - " dtype: float64", - " standard_name: 'height'", - " long_name: 'this'", - " var_name: 'x_var'", - ] - self.assertLines(expected, result) - - def test_bounded(self): - result = self.coord_representations(shape=(3,), bounded=True) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0., 1., 2.]", - " bounds: [", - " [-0.5, 0.5],", - " [ 0.5, 1.5],", - " [ 1.5, 2.5]]", - " shape: (3,) bounds(3, 2)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_masked(self): - result = self.coord_representations(masked=True) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0.0, -- , 2.0, 3.0, -- ]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_dtype_int(self): - result = self.coord_representations(units="1", datatype=np.int16) - expected = [ - "", - "AuxCoord : x / (1)", - " points: [0, 1, 2, 3, 4]", - " shape: (5,)", - " dtype: int16", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_dtype_date(self): - # Note: test with a date 'longer' than the built-in one in - # 'sample_coord(dates=True)', because it includes a time-of-day - full_date_unit = Unit( - "days since 1892-05-17 03:00:25", calendar="360_day" - ) - result = self.coord_representations(units=full_date_unit) - expected = [ - ( - "" - ), - ( - "AuxCoord : x / (days since 1892-05-17 03:00:25, " - "360_day calendar)" - ), - " points: [", - " 1892-05-17 03:00:25, 1892-05-18 03:00:25,", - " 1892-05-19 03:00:25, 1892-05-20 03:00:25,", - " 1892-05-21 03:00:25]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_attributes(self): - # NOTE: scheduled for future change, to put each attribute on a line - coord = self.sample_coord( - attributes={ - "array": np.arange(7.0), - "list": [1, 2, 3], - "empty": [], - "None": None, - "string": "this", - "long_long_long_long_long_name": 3, - "other": ( - "long_long_long_long_long_long_long_long_" - "long_long_long_long_long_long_long_long_value" - ), - "float": 4.3, - } - ) - result = self.repr_str_strings(coord) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0., 1., 2., 3., 4.]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - " attributes:", - " array [0. 1. 2. 3. 4. 5. 6.]", - " list [1, 2, 3]", - " empty []", - " None None", - " string 'this'", - " long_long_long_long_long_name 3", - ( - " other " - "'long_long_long_long_long_long_long_long_" - "long_long_long_long_long_long..." - ), - " float 4.3", - ] - self.assertLines(expected, result) - - def test_lazy_points(self): - result = self.coord_representations(lazy_points=True) - expected = [ - " shape(5,)>", - "AuxCoord : x / (m)", - " points: ", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_lazy_bounds(self): - result = self.coord_representations(lazy_bounds=True) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0., 1., 2., 3., 4.]", - " bounds: ", - " shape: (5,) bounds(5, 2)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_lazy_points_and_bounds(self): - result = self.coord_representations(lazy_points=True, lazy_bounds=True) - expected = [ - "+bounds shape(5,)>", - "AuxCoord : x / (m)", - " points: ", - " bounds: ", - " shape: (5,) bounds(5, 2)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_scalar(self): - result = self.coord_representations(shape=(1,), bounded=True) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0.]", - " bounds: [[-10., 10.]]", - " shape: (1,) bounds(1, 2)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_scalar_masked(self): - result = self.coord_representations( - shape=(1,), bounded=True, masked=True - ) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [--]", - " bounds: [[--, --]]", - " shape: (1,) bounds(1, 2)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_length_short(self): - result = self.coord_representations(shape=(2,), bounded=True) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0., 1.]", - " bounds: [", - " [-0.5, 0.5],", - " [ 0.5, 1.5]]", - " shape: (2,) bounds(2, 2)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_length_medium(self): - # Where bounds are truncated, but points not. - result = self.coord_representations(shape=(14,), bounded=True) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [", - " 0., 1., 2., 3., 4., 5., 6., 7., 8.,", - " 9., 10., 11., 12., 13.]", - " bounds: [", - " [-0.5, 0.5],", - " [ 0.5, 1.5],", - " ...,", - " [11.5, 12.5],", - " [12.5, 13.5]]", - " shape: (14,) bounds(14, 2)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_length_long(self): - # Completely truncated representations - result = self.coord_representations(shape=(150,), bounded=True) - expected = [ - ( - "" - ), - "AuxCoord : x / (m)", - " points: [ 0., 1., ..., 148., 149.]", - " bounds: [", - " [ -0.5, 0.5],", - " [ 0.5, 1.5],", - " ...,", - " [147.5, 148.5],", - " [148.5, 149.5]]", - " shape: (150,) bounds(150, 2)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_strings(self): - result = self.coord_representations(datatype=str) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0 , 11 , 222 , 3333 , 44444]", - " shape: (5,)", - " dtype: ", - "AuxCoord : x / (m)", - " points: [", - " 0 , 11 , 222 ,", - " 3333 , 44444 , 555555 ,", - " 6666666 , 77777777 , 888888888 ,", - " 9999999999, 0 , 11 ,", - " 222 , 3333 , 44444 ]", - " shape: (15,)", - " dtype: ", - "AuxCoord : x / (days since 1970-03-5, gregorian calendar)", - " points: [1970-03-05 00:00:00, 1970-03-06 00:00:00]", - " shape: (2,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_dates_scalar(self): - # Printouts for a scalar date coord. - # Demonstrate that a "typical" datetime coord can print with the date - # value visible in the repr. - long_time_unit = Unit("hours since 2025-03-23 01:00:00") - coord = self.sample_coord( - standard_name="time", - long_name=None, - shape=(1,), - units=long_time_unit, - ) - # Do this one with a default linewidth, not our default reduced one, so - # that we can get the date value in the repr output. - result = self.repr_str_strings(coord, linewidth=None) - expected = [ - ( - "" - ), - ( - "AuxCoord : time / (hours since 2025-03-23 01:00:00, " - "gregorian calendar)" - ), - " points: [2025-03-23 01:00:00]", - " shape: (1,)", - " dtype: float64", - " standard_name: 'time'", - ] - self.assertLines(expected, result) - - def test_dates_bounds(self): - result = self.coord_representations(dates=True, bounded=True) - expected = [ - "", - "AuxCoord : x / (days since 1970-03-5, gregorian calendar)", - " points: [", - " 1970-03-05 00:00:00, 1970-03-06 00:00:00,", - " 1970-03-07 00:00:00, 1970-03-08 00:00:00,", - " 1970-03-09 00:00:00]", - " bounds: [", - " [1970-03-04 12:00:00, 1970-03-05 12:00:00],", - " [1970-03-05 12:00:00, 1970-03-06 12:00:00],", - " [1970-03-06 12:00:00, 1970-03-07 12:00:00],", - " [1970-03-07 12:00:00, 1970-03-08 12:00:00],", - " [1970-03-08 12:00:00, 1970-03-09 12:00:00]]", - " shape: (5,) bounds(5, 2)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_dates_masked(self): - result = self.coord_representations(dates=True, masked=True) - expected = [ - "", - "AuxCoord : x / (days since 1970-03-5, gregorian calendar)", - " points: [", - " 1970-03-05 00:00:00, -- ,", - " 1970-03-07 00:00:00, 1970-03-08 00:00:00,", - " -- ]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_untypical_bounds(self): - # Check printing when n-bounds > 2 - coord = self.sample_coord() - bounds = coord.points.reshape((5, 1)) + np.array([[-3.0, -2, 2, 3]]) - coord.bounds = bounds - result = self.repr_str_strings(coord) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0., 1., 2., 3., 4.]", - " bounds: [", - " [-3., -2., 2., 3.],", - " [-2., -1., 3., 4.],", - " ...,", - " [ 0., 1., 5., 6.],", - " [ 1., 2., 6., 7.]]", - " shape: (5,) bounds(5, 4)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_multidimensional(self): - # Demonstrate formatting of multdimensional arrays - result = self.coord_representations(shape=(7, 5, 3)) - # This one is a bit unavoidably long .. - expected = [ - "", - "AuxCoord : x / (m)", - " points: [", - " [[ 0., 1., 2.],", - " [ 3., 4., 5.],", - " ...,", - " [ 9., 10., 11.],", - " [ 12., 13., 14.]],", - " ", - " [[ 15., 16., 17.],", - " [ 18., 19., 20.],", - " ...,", - " [ 24., 25., 26.],", - " [ 27., 28., 29.]],", - " ", - " ...,", - " ", - " [[ 75., 76., 77.],", - " [ 78., 79., 80.],", - " ...,", - " [ 84., 85., 86.],", - " [ 87., 88., 89.]],", - " ", - " [[ 90., 91., 92.],", - " [ 93., 94., 95.],", - " ...,", - " [ 99., 100., 101.],", - " [102., 103., 104.]]]", - " shape: (7, 5, 3)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_multidimensional_small(self): - # Demonstrate that a small-enough multidim will print in the repr. - result = self.coord_representations(shape=(2, 2), datatype=int) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [", - " [0, 1],", - " [2, 3]]", - " shape: (2, 2)", - " dtype: int64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_integers_short(self): - result = self.coord_representations(datatype=np.int16) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0, 1, 2, 3, 4]", - " shape: (5,)", - " dtype: int16", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_integers_masked(self): - result = self.coord_representations(datatype=int, masked=True) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0 , --, 2 , 3 , --]", - " shape: (5,)", - " dtype: int64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_integers_masked_long(self): - result = self.coord_representations( - shape=(20,), datatype=int, masked=True - ) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0 , --, ..., 18, --]", - " shape: (20,)", - " dtype: int64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - -class Test__print_Coord(Mixin__string_representations, tests.IrisTest): - """ - Test Coord-specific aspects of __str__ and __repr__ output. - - Aspects : - * DimCoord / AuxCoord - * coord_system - * climatological - * circular - - """ - - def test_dimcoord(self): - result = self.coord_representations(dimcoord=True) - expected = [ - "", - "DimCoord : x / (m)", - " points: [0., 1., 2., 3., 4.]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_coord_system(self): - result = self.coord_representations(coord_system=GeogCS(1000.0)) - expected = [ - "", - "AuxCoord : x / (m)", - " points: [0., 1., 2., 3., 4.]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - " coord_system: GeogCS(1000.0)", - ] - self.assertLines(expected, result) - - def test_climatological(self): - cube = cube_with_climatology() - coord = cube.coord("time") - coord = coord[:1] # Just to make it a bit shorter - result = self.repr_str_strings(coord) - expected = [ - ( - "" - ), - ( - "DimCoord : time / (days since 1970-01-01 00:00:00-00, " - "gregorian calendar)" - ), - " points: [2001-01-10 00:00:00]", - " bounds: [[2001-01-10 00:00:00, 2011-01-10 00:00:00]]", - " shape: (1,) bounds(1, 2)", - " dtype: float64", - " standard_name: 'time'", - " climatological: True", - ] - self.assertLines(expected, result) - - def test_circular(self): - coord = self.sample_coord(shape=(2,), dimcoord=True) - coord.circular = True - result = self.repr_str_strings(coord) - expected = [ - "", - "DimCoord : x / (m)", - " points: [0., 1.]", - " shape: (2,)", - " dtype: float64", - " long_name: 'x'", - " circular: True", - ] - self.assertLines(expected, result) - - -class Test__print_noncoord(Mixin__string_representations, tests.IrisTest): - """ - Limited testing of other _DimensionalMetadata subclasses. - - * AncillaryVariable - * CellMeasure - * Connectivity - * MeshCoord - - """ - - def test_ancillary(self): - # Check we can print an AncillaryVariable - # Practically, ~identical to an AuxCoord, but without bounds, and the - # array is called 'data'. - data = self.sample_data() - ancil = AncillaryVariable(data, long_name="v_aux", units="m s-1") - result = self.repr_str_strings(ancil) - expected = [ - "", - "AncillaryVariable : v_aux / (m s-1)", - " data: [0., 1., 2., 3., 4.]", - " shape: (5,)", - " dtype: float64", - " long_name: 'v_aux'", - ] - self.assertLines(expected, result) - - def test_cellmeasure(self): - # Check we can print an AncillaryVariable - # N.B. practically, identical to an AuxCoord (without bounds) - # Check we can print an AncillaryVariable - # Practically, ~identical to an AuxCoord, but without bounds, and the - # array is called 'data'. - data = self.sample_data() - cell_measure = CellMeasure( - data, measure="area", long_name="cell_area", units="m^2" - ) - result = self.repr_str_strings(cell_measure) - expected = [ - "", - "CellMeasure : cell_area / (m^2)", - " data: [0., 1., 2., 3., 4.]", - " shape: (5,)", - " dtype: float64", - " long_name: 'cell_area'", - " measure: 'area'", - ] - self.assertLines(expected, result) - - def test_connectivity(self): - # Check we can print a Connectivity - # Like a Coord, but always print : cf_role, location_axis, start_index - data = self.sample_data(shape=(3, 2), datatype=int) - conn = Connectivity( - data, cf_role="edge_node_connectivity", long_name="enc", units="1" - ) - result = self.repr_str_strings(conn) - expected = [ - "", - "Connectivity : enc / (1)", - " data: [", - " [0, 1],", - " [2, 3],", - " [4, 5]]", - " shape: (3, 2)", - " dtype: int64", - " long_name: 'enc'", - " cf_role: 'edge_node_connectivity'", - " start_index: 0", - " location_axis: 0", - ] - self.assertLines(expected, result) - - def test_connectivity__start_index(self): - # Check we can print a Connectivity - # Like a Coord, but always print : cf_role, location_axis, start_index - data = self.sample_data(shape=(3, 2), datatype=int) - conn = Connectivity( - data + 1, - start_index=1, - cf_role="edge_node_connectivity", - long_name="enc", - units="1", - ) - result = self.repr_str_strings(conn) - expected = [ - "", - "Connectivity : enc / (1)", - " data: [", - " [1, 2],", - " [3, 4],", - " [5, 6]]", - " shape: (3, 2)", - " dtype: int64", - " long_name: 'enc'", - " cf_role: 'edge_node_connectivity'", - " start_index: 1", - " location_axis: 0", - ] - self.assertLines(expected, result) - - def test_connectivity__location_axis(self): - # Check we can print a Connectivity - # Like a Coord, but always print : cf_role, location_axis, start_index - data = self.sample_data(shape=(3, 2), datatype=int) - conn = Connectivity( - data.transpose(), - location_axis=1, - cf_role="edge_node_connectivity", - long_name="enc", - units="1", - ) - result = self.repr_str_strings(conn) - expected = [ - "", - "Connectivity : enc / (1)", - " data: [", - " [0, 2, 4],", - " [1, 3, 5]]", - " shape: (2, 3)", - " dtype: int64", - " long_name: 'enc'", - " cf_role: 'edge_node_connectivity'", - " start_index: 0", - " location_axis: 1", - ] - self.assertLines(expected, result) - - def test_meshcoord(self): - meshco = sample_meshcoord() - meshco.mesh.long_name = "test_mesh" # For stable printout of the Mesh - result = self.repr_str_strings(meshco) - expected = [ - ( - "" - ), - "MeshCoord : longitude / (degrees_east)", - " mesh: ", - " location: 'face'", - " points: [3100, 3101, 3102]", - " bounds: [", - " [1100, 1101, 1102, 1103],", - " [1104, 1105, 1106, 1107],", - " [1108, 1109, 1110, 1111]]", - " shape: (3,) bounds(3, 4)", - " dtype: int64", - " standard_name: 'longitude'", - " long_name: 'long-name'", - " attributes:", - " a 1", - " b 'c'", - " axis: 'x'", - ] - self.assertLines(expected, result) - - -class Test_summary(Mixin__string_representations, tests.IrisTest): - """ - Test the controls of the 'summary' method. - """ - - def test_shorten(self): - coord = self.sample_coord() - expected = self.repr_str_strings(coord) - result = coord.summary(shorten=True) + "\n" + coord.summary() - self.assertEqual(expected, result) - - def test_max_values__default(self): - coord = self.sample_coord() - result = coord.summary() - expected = [ - "AuxCoord : x / (m)", - " points: [0., 1., 2., 3., 4.]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_max_values__2(self): - coord = self.sample_coord() - result = coord.summary(max_values=2) - expected = [ - "AuxCoord : x / (m)", - " points: [0., 1., ..., 3., 4.]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_max_values__bounded__2(self): - coord = self.sample_coord(bounded=True) - result = coord.summary(max_values=2) - expected = [ - "AuxCoord : x / (m)", - " points: [0., 1., ..., 3., 4.]", - " bounds: [", - " [-0.5, 0.5],", - " [ 0.5, 1.5],", - " ...,", - " [ 2.5, 3.5],", - " [ 3.5, 4.5]]", - " shape: (5,) bounds(5, 2)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_max_values__0(self): - coord = self.sample_coord(bounded=True) - result = coord.summary(max_values=0) - expected = [ - "AuxCoord : x / (m)", - " points: [...]", - " bounds: [...]", - " shape: (5,) bounds(5, 2)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_linewidth__default(self): - coord = self.sample_coord() - coord.points = coord.points + 1000.003 # Make the output numbers wider - result = coord.summary() - expected = [ - "AuxCoord : x / (m)", - " points: [1000.003, 1001.003, 1002.003, 1003.003, 1004.003]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - # Show that, when unset, it follows the numpy setting - with np.printoptions(linewidth=35): - result = coord.summary() - expected = [ - "AuxCoord : x / (m)", - " points: [", - " 1000.003, 1001.003,", - " 1002.003, 1003.003,", - " 1004.003]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - def test_linewidth__set(self): - coord = self.sample_coord() - coord.points = coord.points + 1000.003 # Make the output numbers wider - expected = [ - "AuxCoord : x / (m)", - " points: [", - " 1000.003, 1001.003,", - " 1002.003, 1003.003,", - " 1004.003]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - result = coord.summary(linewidth=35) - self.assertLines(expected, result) - - with np.printoptions(linewidth=999): - # Show that, when set, it ignores the numpy setting - result = coord.summary(linewidth=35) - self.assertLines(expected, result) - - def test_convert_dates(self): - coord = self.sample_coord(dates=True) - result = coord.summary() - expected = [ - "AuxCoord : x / (days since 1970-03-5, gregorian calendar)", - " points: [", - ( - " 1970-03-05 00:00:00, 1970-03-06 00:00:00, " - "1970-03-07 00:00:00," - ), - " 1970-03-08 00:00:00, 1970-03-09 00:00:00]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - result = coord.summary(convert_dates=False) - expected = [ - "AuxCoord : x / (days since 1970-03-5, gregorian calendar)", - " points: [0., 1., 2., 3., 4.]", - " shape: (5,)", - " dtype: float64", - " long_name: 'x'", - ] - self.assertLines(expected, result) - - if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 7d56b505bd..f4daf64bae 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -757,29 +757,7 @@ def test_agg_by_label(self): res_cube = self.cube.aggregated_by("label", self.mock_agg) val_coord = AuxCoord( np.array([1.0, 0.5, 1.0]), - bounds=np.array([[0, 2], [0, 1], [0, 2]]), - long_name="val", - ) - label_coord = AuxCoord( - np.array(["alpha", "beta", "gamma"]), - long_name="label", - units="no_unit", - ) - self.assertEqual(res_cube.coord("val"), val_coord) - self.assertEqual(res_cube.coord("label"), label_coord) - - def test_agg_by_label_bounded(self): - # Aggregate a cube on a string coordinate label where label - # and val entries are not in step; the resulting cube has a val - # coord of bounded cells and a label coord of single string entries. - val_points = self.cube.coord("val").points - self.cube.coord("val").bounds = np.array( - [val_points - 0.5, val_points + 0.5] - ).T - res_cube = self.cube.aggregated_by("label", self.mock_agg) - val_coord = AuxCoord( - np.array([1.0, 0.5, 1.0]), - bounds=np.array([[-0.5, 2.5], [-0.5, 1.5], [-0.5, 2.5]]), + bounds=np.array([[0, 2], [0, 1], [2, 0]]), long_name="val", ) label_coord = AuxCoord( @@ -912,7 +890,7 @@ def test_agg_by_label__lazy(self): res_cube = self.cube.aggregated_by("label", MEAN) val_coord = AuxCoord( np.array([1.0, 0.5, 1.0]), - bounds=np.array([[0, 2], [0, 1], [0, 2]]), + bounds=np.array([[0, 2], [0, 1], [2, 0]]), long_name="val", ) label_coord = AuxCoord( diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Connectivity.py b/lib/iris/tests/unit/experimental/ugrid/mesh/test_Connectivity.py index 9a81c79d44..5d6f48fdda 100644 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Connectivity.py +++ b/lib/iris/tests/unit/experimental/ugrid/mesh/test_Connectivity.py @@ -29,30 +29,28 @@ def setUp(self): "var_name": "face_nodes", "attributes": {"notes": "this is a test"}, "start_index": 1, - "location_axis": 1, + "src_dim": 1, } self.connectivity = Connectivity(**self.kwargs) def test_cf_role(self): self.assertEqual(self.kwargs["cf_role"], self.connectivity.cf_role) - def test_location(self): + def test_src_location(self): expected = self.kwargs["cf_role"].split("_")[0] - self.assertEqual(expected, self.connectivity.location) + self.assertEqual(expected, self.connectivity.src_location) - def test_connected(self): + def test_tgt_location(self): expected = self.kwargs["cf_role"].split("_")[1] - self.assertEqual(expected, self.connectivity.connected) + self.assertEqual(expected, self.connectivity.tgt_location) def test_start_index(self): self.assertEqual( self.kwargs["start_index"], self.connectivity.start_index ) - def test_location_axis(self): - self.assertEqual( - self.kwargs["location_axis"], self.connectivity.location_axis - ) + def test_src_dim(self): + self.assertEqual(self.kwargs["src_dim"], self.connectivity.src_dim) def test_indices(self): self.assertArrayEqual( @@ -60,7 +58,7 @@ def test_indices(self): ) def test_read_only(self): - attributes = ("indices", "cf_role", "start_index", "location_axis") + attributes = ("indices", "cf_role", "start_index", "src_dim") for attribute in attributes: self.assertRaisesRegex( AttributeError, @@ -72,10 +70,10 @@ def test_read_only(self): ) def test_transpose(self): - expected_dim = 1 - self.kwargs["location_axis"] + expected_dim = 1 - self.kwargs["src_dim"] expected_indices = self.kwargs["indices"].transpose() new_connectivity = self.connectivity.transpose() - self.assertEqual(expected_dim, new_connectivity.location_axis) + self.assertEqual(expected_dim, new_connectivity.src_dim) self.assertArrayEqual(expected_indices, new_connectivity.indices) def test_lazy_indices(self): @@ -89,52 +87,39 @@ def test_core_indices(self): def test_has_lazy_indices(self): self.assertFalse(self.connectivity.has_lazy_indices()) - def test_lazy_location_lengths(self): - self.assertTrue( - is_lazy_data(self.connectivity.lazy_location_lengths()) - ) + def test_lazy_src_lengths(self): + self.assertTrue(is_lazy_data(self.connectivity.lazy_src_lengths())) - def test_location_lengths(self): + def test_src_lengths(self): expected = [4, 4, 4] - self.assertArrayEqual(expected, self.connectivity.location_lengths()) + self.assertArrayEqual(expected, self.connectivity.src_lengths()) def test___str__(self): - expected = "\n".join( - [ - "Connectivity : my_face_nodes / (unknown)", - " data: [", - " [ 1, 2, 3],", - " [ 4, 5, 6],", - " [ 7, 8, 9],", - " [10, 11, 12]]", - " shape: (4, 3)", - " dtype: int64", - " long_name: 'my_face_nodes'", - " var_name: 'face_nodes'", - " attributes:", - " notes 'this is a test'", - " cf_role: 'face_node_connectivity'", - " start_index: 1", - " location_axis: 1", - ] + expected = ( + "Connectivity(cf_role='face_node_connectivity', start_index=1)" ) self.assertEqual(expected, self.connectivity.__str__()) def test___repr__(self): - expected = "" + expected = ( + "Connectivity(array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]]), " + "cf_role='face_node_connectivity', long_name='my_face_nodes', " + "var_name='face_nodes', attributes={'notes': 'this is a test'}, " + "start_index=1, src_dim=1)" + ) self.assertEqual(expected, self.connectivity.__repr__()) def test_xml_element(self): doc = minidom.Document() connectivity_element = self.connectivity.xml_element(doc) self.assertEqual(connectivity_element.tagName, "connectivity") - for attribute in ("cf_role", "start_index", "location_axis"): + for attribute in ("cf_role", "start_index", "src_dim"): self.assertIn(attribute, connectivity_element.attributes) def test___eq__(self): equivalent_kwargs = self.kwargs equivalent_kwargs["indices"] = self.kwargs["indices"].transpose() - equivalent_kwargs["location_axis"] = 1 - self.kwargs["location_axis"] + equivalent_kwargs["src_dim"] = 1 - self.kwargs["src_dim"] equivalent = Connectivity(**equivalent_kwargs) self.assertFalse( np.array_equal(equivalent.indices, self.connectivity.indices) @@ -165,18 +150,16 @@ def test_copy(self): copy_connectivity = self.connectivity.copy(new_indices) self.assertArrayEqual(new_indices, copy_connectivity.indices) - def test_indices_by_location(self): + def test_indices_by_src(self): expected = self.kwargs["indices"].transpose() - self.assertArrayEqual( - expected, self.connectivity.indices_by_location() - ) + self.assertArrayEqual(expected, self.connectivity.indices_by_src()) - def test_indices_by_location_input(self): + def test_indices_by_src_input(self): expected = as_lazy_data(self.kwargs["indices"].transpose()) - by_location = self.connectivity.indices_by_location( + by_src = self.connectivity.indices_by_src( self.connectivity.lazy_indices() ) - self.assertArrayEqual(expected, by_location) + self.assertArrayEqual(expected, by_src) class TestAltIndices(tests.IrisTest): @@ -227,14 +210,14 @@ def test_start_index(self): ValueError, "Invalid start_index .", Connectivity, **kwargs ) - def test_location_axis(self): + def test_src_dim(self): kwargs = { "indices": np.linspace(1, 9, 9, dtype=int).reshape((-1, 3)), "cf_role": "face_node_connectivity", - "location_axis": 2, + "src_dim": 2, } self.assertRaisesRegex( - ValueError, "Invalid location_axis .", Connectivity, **kwargs + ValueError, "Invalid src_dim .", Connectivity, **kwargs ) def test_cf_role(self): @@ -292,7 +275,7 @@ def test_indices_locations_edge(self): } self.assertRaisesRegex( ValueError, - "Not all edges meet requirement: len=2", + "Not all src_locations meet requirement: len=2", Connectivity, **kwargs, ) @@ -304,7 +287,7 @@ def test_indices_locations_face(self): } self.assertRaisesRegex( ValueError, - "Not all faces meet requirement: len>=3", + "Not all src_locations meet requirement: len>=3", Connectivity, **kwargs, ) @@ -316,7 +299,7 @@ def test_indices_locations_volume_face(self): } self.assertRaisesRegex( ValueError, - "Not all volumes meet requirement: len>=4", + "Not all src_locations meet requirement: len>=4", Connectivity, **kwargs, ) @@ -328,7 +311,7 @@ def test_indices_locations_volume_edge(self): } self.assertRaisesRegex( ValueError, - "Not all volumes meet requirement: len>=6", + "Not all src_locations meet requirement: len>=6", Connectivity, **kwargs, ) @@ -338,11 +321,11 @@ def test_indices_locations_alt_dim(self): kwargs = { "indices": np.linspace(1, 9, 9, dtype=int).reshape((3, -1)), "cf_role": "volume_face_connectivity", - "location_axis": 1, + "src_dim": 1, } self.assertRaisesRegex( ValueError, - "Not all volumes meet requirement: len>=4", + "Not all src_locations meet requirement: len>=4", Connectivity, **kwargs, ) @@ -359,10 +342,6 @@ def test_indices_locations_masked(self): connectivity = Connectivity(**kwargs) self.assertRaisesRegex( ValueError, - "Not all faces meet requirement: len>=3", + "Not all src_locations meet requirement: len>=3", connectivity.validate_indices, ) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh.py b/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh.py index f39f3706ee..9808660016 100644 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh.py +++ b/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh.py @@ -106,33 +106,23 @@ def test___getstate__(self): self.assertEqual(expected, self.mesh.__getstate__()) def test___repr__(self): - expected = "" - self.assertEqual(expected, repr(self.mesh)) - - def test___str__(self): - expected = [ - "Mesh : 'my_topology_mesh'", - " topology_dimension: 1", - " node", - " node_dimension: 'NodeDim'", - " node coordinates", - " ", - " ", - " edge", - " edge_dimension: 'EdgeDim'", - ( - " edge_node_connectivity: " - "" - ), - " edge coordinates", - " ", - " ", - " long_name: 'my_topology_mesh'", - " var_name: 'mesh'", - " attributes:", - " notes 'this is a test'", - ] - self.assertEqual(expected, str(self.mesh).split("\n")) + expected = ( + "Mesh(topology_dimension=1, node_coords_and_axes=[(AuxCoord(" + "array([0, 2, 1]), standard_name='longitude', units=Unit(" + "'unknown'), long_name='long_name', var_name='node_lon', " + "attributes={'test': 1}), 'x'), (AuxCoord(array([0, 0, 1]), " + "standard_name='latitude', units=Unit('unknown'), " + "var_name='node_lat'), 'y')], connectivities=Connectivity(" + "cf_role='edge_node_connectivity', start_index=0), " + "edge_coords_and_axes=[(AuxCoord(array([1. , 1.5, 0.5]), " + "standard_name='longitude', units=Unit('unknown'), " + "var_name='edge_lon'), 'x'), (AuxCoord(array([0. , 0.5, 0.5]), " + "standard_name='latitude', units=Unit('unknown'), " + "var_name='edge_lat'), 'y')], long_name='my_topology_mesh', " + "var_name='mesh', attributes={'notes': 'this is a test'}, " + "node_dimension='NodeDim', edge_dimension='EdgeDim')" + ) + self.assertEqual(expected, self.mesh.__repr__()) def test___eq__(self): # The dimension names do not participate in equality. @@ -211,7 +201,7 @@ def test_connectivities(self): for kwargs in negative_kwargs: self.assertEqual([], func(**kwargs)) - def test_connectivities_elements(self): + def test_connectivities_locations(self): # topology_dimension-specific results. Method intended to be overridden. positive_kwargs = ( {"contains_node": True}, @@ -271,7 +261,7 @@ def test_coords(self): for kwargs in negative_kwargs: self.assertNotIn(self.NODE_LON, func(**kwargs)) - def test_coords_elements(self): + def test_coords_locations(self): # topology_dimension-specific results. Method intended to be overridden. all_expected = { "node_x": self.NODE_LON, @@ -383,103 +373,33 @@ def setUpClass(cls): cls.mesh = mesh.Mesh(**cls.kwargs) def test___repr__(self): - expected = "" - self.assertEqual(expected, repr(self.mesh)) - - def test___str__(self): - expected = [ - "Mesh : 'my_topology_mesh'", - " topology_dimension: 2", - " node", - " node_dimension: 'NodeDim'", - " node coordinates", - " ", - " ", - " edge", - " edge_dimension: 'EdgeDim'", - ( - " edge_node_connectivity: " - "" - ), - " edge coordinates", - " ", - " ", - " face", - " face_dimension: 'FaceDim'", - ( - " face_node_connectivity: " - "" - ), - " face coordinates", - " ", - " ", - " optional connectivities", - ( - " face_face_connectivity: " - "" - ), - ( - " face_edge_connectivity: " - "" - ), - ( - " edge_face_connectivity: " - "" - ), - " long_name: 'my_topology_mesh'", - " var_name: 'mesh'", - " attributes:", - " notes 'this is a test'", - ] - self.assertEqual(expected, str(self.mesh).split("\n")) - - # Test some different options of the str() operation here. - def test___str__noedgecoords(self): - mesh_kwargs = self.kwargs.copy() - del mesh_kwargs["edge_coords_and_axes"] - alt_mesh = mesh.Mesh(**mesh_kwargs) - expected = [ - "Mesh : 'my_topology_mesh'", - " topology_dimension: 2", - " node", - " node_dimension: 'NodeDim'", - " node coordinates", - " ", - " ", - " edge", - " edge_dimension: 'EdgeDim'", - ( - " edge_node_connectivity: " - "" - ), - " face", - " face_dimension: 'FaceDim'", - ( - " face_node_connectivity: " - "" - ), - " face coordinates", - " ", - " ", - " optional connectivities", - ( - " face_face_connectivity: " - "" - ), - ( - " face_edge_connectivity: " - "" - ), - ( - " edge_face_connectivity: " - "" - ), - " long_name: 'my_topology_mesh'", - " var_name: 'mesh'", - " attributes:", - " notes 'this is a test'", - ] - self.assertEqual(expected, str(alt_mesh).split("\n")) + expected = ( + "Mesh(topology_dimension=2, node_coords_and_axes=[(AuxCoord(" + "array([0, 2, 1]), standard_name='longitude', units=Unit(" + "'unknown'), long_name='long_name', var_name='node_lon', " + "attributes={'test': 1}), 'x'), (AuxCoord(array([0, 0, 1]), " + "standard_name='latitude', units=Unit('unknown'), " + "var_name='node_lat'), 'y')], connectivities=[Connectivity(" + "cf_role='face_node_connectivity', start_index=0), Connectivity(" + "cf_role='edge_node_connectivity', start_index=0), Connectivity(" + "cf_role='face_edge_connectivity', start_index=0), Connectivity(" + "cf_role='face_face_connectivity', start_index=0), Connectivity(" + "cf_role='edge_face_connectivity', start_index=0), Connectivity(" + "cf_role='boundary_node_connectivity', start_index=0)], " + "edge_coords_and_axes=[(AuxCoord(array([1. , 1.5, 0.5]), " + "standard_name='longitude', units=Unit('unknown'), " + "var_name='edge_lon'), 'x'), (AuxCoord(array([0. , 0.5, 0.5]), " + "standard_name='latitude', units=Unit('unknown'), " + "var_name='edge_lat'), 'y')], face_coords_and_axes=[(AuxCoord(" + "array([0.5]), standard_name='longitude', units=Unit('unknown'), " + "var_name='face_lon'), 'x'), (AuxCoord(array([0.5]), " + "standard_name='latitude', units=Unit('unknown'), " + "var_name='face_lat'), 'y')], long_name='my_topology_mesh', " + "var_name='mesh', attributes={'notes': 'this is a test'}, " + "node_dimension='NodeDim', edge_dimension='EdgeDim', " + "face_dimension='FaceDim')" + ) + self.assertEqual(expected, self.mesh.__repr__()) def test_all_connectivities(self): expected = mesh.Mesh2DConnectivities( @@ -525,7 +445,7 @@ def test_connectivity(self): contains_face=False, ) - def test_connectivities_elements(self): + def test_connectivities_locations(self): kwargs_expected = ( ( {"contains_node": True}, @@ -581,7 +501,7 @@ def test_connectivities_elements(self): for item in expected: self.assertIn(item, result) - def test_coords_elements(self): + def test_coords_locations(self): all_expected = { "node_x": self.NODE_LON, "node_y": self.NODE_LAT, @@ -649,93 +569,6 @@ def test_face_node(self): self.assertEqual(self.FACE_NODE, self.mesh.face_node_connectivity) -class Test__str__various(TestMeshCommon): - # Some extra testing for the str() operation : based on 1D meshes as simpler - def setUp(self): - # All the tests here want modified meshes, so use standard setUp to - # create afresh for each test, allowing them to modify it. - super().setUp() - # Mesh kwargs with topology_dimension=1 and all applicable - # arguments populated - this tests correct property setting. - self.kwargs = { - "topology_dimension": 1, - "node_coords_and_axes": ( - (self.NODE_LON, "x"), - (self.NODE_LAT, "y"), - ), - "connectivities": [self.EDGE_NODE], - "long_name": "my_topology_mesh", - "var_name": "mesh", - "attributes": {"notes": "this is a test"}, - "node_dimension": "NodeDim", - "edge_dimension": "EdgeDim", - "edge_coords_and_axes": ( - (self.EDGE_LON, "x"), - (self.EDGE_LAT, "y"), - ), - } - self.mesh = mesh.Mesh(**self.kwargs) - - def test___repr__basic(self): - expected = "" - self.assertEqual(expected, repr(self.mesh)) - - def test___repr__varname(self): - self.mesh.long_name = None - expected = "" - self.assertEqual(expected, repr(self.mesh)) - - def test___repr__noname(self): - self.mesh.long_name = None - self.mesh.var_name = None - expected = "" - self.assertRegex(repr(self.mesh), expected) - - def test___str__noattributes(self): - self.mesh.attributes = None - self.assertNotIn("attributes", str(self.mesh)) - - def test___str__emptyattributes(self): - self.mesh.attributes.clear() - self.assertNotIn("attributes", str(self.mesh)) - - def test__str__longstringattribute(self): - self.mesh.attributes["long_string"] = ( - "long_x_10_long_x_20_long_x_30_long_x_40_" - "long_x_50_long_x_60_long_x_70_long_x_80_" - ) - result = str(self.mesh) - # Note: initial single-quote, but no final one : this is correct ! - expected = ( - "'long_x_10_long_x_20_long_x_30_long_x_40_" - "long_x_50_long_x_60_long_x_70..." - ) - self.assertIn(expected + ":END", result + ":END") - - def test___str__units_stdname(self): - # These are usually missing, but they *can* be present. - mesh_kwargs = self.kwargs.copy() - mesh_kwargs["standard_name"] = "height" # Odd choice ! - mesh_kwargs["units"] = "m" - alt_mesh = mesh.Mesh(**mesh_kwargs) - result = str(alt_mesh) - # We expect these to appear at the end. - expected = "\n".join( - [ - " edge coordinates", - " ", - " ", - " standard_name: 'height'", - " long_name: 'my_topology_mesh'", - " var_name: 'mesh'", - " units: Unit('m')", - " attributes:", - " notes 'this is a test'", - ] - ) - self.assertTrue(result.endswith(expected)) - - class TestOperations1D(TestMeshCommon): # Tests that cannot re-use an existing Mesh instance, instead need a new # one each time. @@ -749,7 +582,7 @@ def setUp(self): @staticmethod def new_connectivity(connectivity, new_len=False): """Provide a new connectivity recognisably different from the original.""" - # NOTE: assumes non-transposed connectivity (location_axis=0). + # NOTE: assumes non-transposed connectivity (src_dim=0). if new_len: shape = (connectivity.shape[0] + 1, connectivity.shape[1]) else: @@ -1342,7 +1175,3 @@ def test_minimum_coords(self): mesh.Mesh, **kwargs, ) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/test_MeshCoord.py b/lib/iris/tests/unit/experimental/ugrid/mesh/test_MeshCoord.py index ce99a8b4be..740258b77c 100644 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/test_MeshCoord.py +++ b/lib/iris/tests/unit/experimental/ugrid/mesh/test_MeshCoord.py @@ -11,13 +11,12 @@ # importing anything else. import iris.tests as tests # isort:skip -import re import unittest.mock as mock import dask.array as da import numpy as np -from iris._lazy_data import as_lazy_data, is_lazy_data +from iris._lazy_data import is_lazy_data from iris.common.metadata import BaseMetadata from iris.coords import AuxCoord, Coord from iris.cube import Cube @@ -61,7 +60,7 @@ def test_fail_bad_mesh(self): sample_meshcoord(mesh=mock.sentinel.odd) def test_valid_locations(self): - for loc in Mesh.ELEMENTS: + for loc in Mesh.LOCATIONS: meshcoord = sample_meshcoord(location=loc) self.assertEqual(meshcoord.location, loc) @@ -269,117 +268,42 @@ def setUp(self): def _expected_elements_regexp( self, - standard_name="longitude", - long_name="long-name", + mesh_strstyle=True, + standard_name=True, + long_name=True, attributes=True, - location="face", - axis="x", ): - # Printed name is standard or long -- we don't have a case with neither - coord_name = standard_name or long_name - # Construct regexp in 'sections' - # NB each consumes upto first non-space in the next line - regexp = f"MeshCoord : {coord_name} / [^\n]+\n *" - regexp += r"mesh: \\n *" - regexp += f"location: '{location}'\n *" - # Now some optional sections : whichever comes first will match - # arbitrary content leading up to it. - matched_any_upto = False + regexp = r"^MeshCoord\(mesh=" + if mesh_strstyle: + regexp += r"Mesh\('test_mesh'\)" + else: + regexp += "" + regexp += r", location='face', axis='x', shape=\(3,\)" if standard_name: - regexp += ".*" - matched_any_upto = True - regexp += f"standard_name: '{standard_name}'\n *" + regexp += ", standard_name='longitude'" + regexp += r", units=Unit\('degrees_east'\)" if long_name: - if not matched_any_upto: - regexp += ".*" - matched_any_upto = True - regexp += f"long_name: '{long_name}'\n *" + regexp += ", long_name='long-name'" if attributes: - # if we expected attributes, they should come next - # TODO: change this when each attribute goes on a new line - if not matched_any_upto: - regexp += ".*" - matched_any_upto = True - # match 'attributes:' followed by N*lines with larger indent - regexp += "attributes:(\n [^ \n]+ +[^ \n]+)+\n " - # After those items, expect 'axis' next - # N.B. this FAILS if we had attributes when we didn't expect them - regexp += f"axis: '{axis}'$" # N.B. this is always the end - - # Compile regexp, also allowing matches across newlines - regexp = re.compile(regexp, flags=re.DOTALL) + regexp += r", attributes={'a': 1, 'b': 'c'}" + regexp += r"\)$" return regexp def test_repr(self): - # A simple check for the condensed form. - result = repr(self.meshcoord) - expected = ( - "" - ) - self.assertEqual(expected, result) - - def test_repr_lazy(self): - # Displays lazy content (and does not realise!). - self.meshcoord.points = as_lazy_data(self.meshcoord.points) - self.meshcoord.bounds = as_lazy_data(self.meshcoord.bounds) - self.assertTrue(self.meshcoord.has_lazy_points()) - self.assertTrue(self.meshcoord.has_lazy_bounds()) - - result = repr(self.meshcoord) - self.assertTrue(self.meshcoord.has_lazy_points()) - self.assertTrue(self.meshcoord.has_lazy_bounds()) - - expected = ( - "+bounds shape(3,)>" - ) - self.assertEqual(expected, result) - - def test_repr__nameless_mesh(self): - # Check what it does when the Mesh doesn't have a name. - self.mesh.long_name = None - assert self.mesh.name() == "unknown" result = repr(self.meshcoord) - re_expected = ( - r".MeshCoord: longitude / \(degrees_east\) " - r"mesh\(.Mesh object at 0x[^>]+.\) location\(face\) " - ) + re_expected = self._expected_elements_regexp(mesh_strstyle=False) self.assertRegex(result, re_expected) def test__str__(self): - # Basic output contains mesh, location, standard_name, long_name, - # attributes, mesh, location and axis - result = str(self.meshcoord) - re_expected = self._expected_elements_regexp() - self.assertRegex(result, re_expected) - - def test__str__lazy(self): - # Displays lazy content (and does not realise!). - self.meshcoord.points = as_lazy_data(self.meshcoord.points) - self.meshcoord.bounds = as_lazy_data(self.meshcoord.bounds) - result = str(self.meshcoord) - self.assertTrue(self.meshcoord.has_lazy_points()) - self.assertTrue(self.meshcoord.has_lazy_bounds()) - - self.assertIn("points: ", result) - self.assertIn("bounds: ", result) - re_expected = self._expected_elements_regexp() + re_expected = self._expected_elements_regexp(mesh_strstyle=True) self.assertRegex(result, re_expected) def test_alternative_location_and_axis(self): meshcoord = sample_meshcoord(mesh=self.mesh, location="edge", axis="y") result = str(meshcoord) - re_expected = self._expected_elements_regexp( - standard_name="latitude", - long_name=None, - location="edge", - axis="y", - attributes=None, - ) + re_expected = r", location='edge', axis='y'" self.assertRegex(result, re_expected) - # Basic output contains standard_name, long_name, attributes def test_str_no_long_name(self): mesh = self.mesh @@ -537,12 +461,12 @@ def _make_test_meshcoord( lazy_sources=False, location="face", inds_start_index=0, - inds_location_axis=0, + inds_src_dim=0, facenodes_changes=None, ): # Construct a miniature face-nodes mesh for testing. # NOTE: we will make our connectivity arrays with standard - # start_index=0 and location_axis=0 : We only adjust that (if required) when + # start_index=0 and src_dim=0 : We only adjust that (if required) when # creating the actual connectivities. face_nodes_array = np.array( [ @@ -627,26 +551,26 @@ def lazify(arr): inds_start_index + ( face_nodes_array.transpose() - if inds_location_axis == 1 + if inds_src_dim == 1 else face_nodes_array ), cf_role="face_node_connectivity", long_name="face_nodes", start_index=inds_start_index, - location_axis=inds_location_axis, + src_dim=inds_src_dim, ) edge_node_conn = Connectivity( inds_start_index + ( edge_nodes_array.transpose() - if inds_location_axis == 1 + if inds_src_dim == 1 else edge_nodes_array ), cf_role="edge_node_connectivity", long_name="edge_nodes", start_index=inds_start_index, - location_axis=inds_location_axis, + src_dim=inds_src_dim, ) self.mesh = Mesh( @@ -730,9 +654,9 @@ def test_edge_bounds(self): # NB simpler than faces : no possibility of missing points self.assertArrayAlmostEqual(result, expected) - def test_bounds_connectivity__location_axis_1(self): + def test_bounds_connectivity__src_dim_1(self): # Test with a transposed indices array. - self._make_test_meshcoord(inds_location_axis=1) + self._make_test_meshcoord(inds_src_dim=1) self._check_expected_bounds_values() def test_bounds_connectivity__start_index_1(self): diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh__from_coords.py b/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh__from_coords.py index edd34f94a1..aee5018e5b 100644 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh__from_coords.py +++ b/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh__from_coords.py @@ -217,7 +217,7 @@ def test_mixed_shapes(self): mesh = self.create() self.assertArrayEqual( - mesh.face_node_connectivity.location_lengths(), [4, 4, 3] + mesh.face_node_connectivity.src_lengths(), [4, 4, 3] ) self.assertEqual(mesh.node_coords.node_x.points[-1], 0.0) self.assertEqual(mesh.node_coords.node_y.points[-1], 0.0) diff --git a/lib/iris/tests/unit/experimental/ugrid/metadata/test_ConnectivityMetadata.py b/lib/iris/tests/unit/experimental/ugrid/metadata/test_ConnectivityMetadata.py index af92e69b08..f119f53729 100644 --- a/lib/iris/tests/unit/experimental/ugrid/metadata/test_ConnectivityMetadata.py +++ b/lib/iris/tests/unit/experimental/ugrid/metadata/test_ConnectivityMetadata.py @@ -29,7 +29,7 @@ def setUp(self): self.attributes = mock.sentinel.attributes self.cf_role = mock.sentinel.cf_role self.start_index = mock.sentinel.start_index - self.location_axis = mock.sentinel.location_axis + self.src_dim = mock.sentinel.src_dim self.cls = ConnectivityMetadata def test_repr(self): @@ -41,12 +41,12 @@ def test_repr(self): attributes=self.attributes, cf_role=self.cf_role, start_index=self.start_index, - location_axis=self.location_axis, + src_dim=self.src_dim, ) fmt = ( "ConnectivityMetadata(standard_name={!r}, long_name={!r}, " "var_name={!r}, units={!r}, attributes={!r}, cf_role={!r}, " - "start_index={!r}, location_axis={!r})" + "start_index={!r}, src_dim={!r})" ) expected = fmt.format( self.standard_name, @@ -56,7 +56,7 @@ def test_repr(self): self.attributes, self.cf_role, self.start_index, - self.location_axis, + self.src_dim, ) self.assertEqual(expected, repr(metadata)) @@ -69,7 +69,7 @@ def test__fields(self): "attributes", "cf_role", "start_index", - "location_axis", + "src_dim", ) self.assertEqual(self.cls._fields, expected) @@ -87,14 +87,14 @@ def setUp(self): attributes=sentinel.attributes, cf_role=sentinel.cf_role, start_index=sentinel.start_index, - location_axis=sentinel.location_axis, + src_dim=sentinel.src_dim, ) self.dummy = sentinel.dummy self.cls = ConnectivityMetadata - # The "location_axis" member is stateful only, and does not participate in + # The "src_dim" member is stateful only, and does not participate in # lenient/strict equivalence. - self.members_no_location_axis = filter( - lambda member: member != "location_axis", self.cls._members + self.members_no_src_dim = filter( + lambda member: member != "src_dim", self.cls._members ) def test_wraps_docstring(self): @@ -140,7 +140,7 @@ def test_op_lenient_same_none(self): self.assertTrue(rmetadata.__eq__(lmetadata)) def test_op_lenient_same_members_none(self): - for member in self.members_no_location_axis: + for member in self.members_no_src_dim: lmetadata = self.cls(**self.values) right = self.values.copy() right[member] = None @@ -152,10 +152,10 @@ def test_op_lenient_same_members_none(self): self.assertFalse(lmetadata.__eq__(rmetadata)) self.assertFalse(rmetadata.__eq__(lmetadata)) - def test_op_lenient_same_location_axis_none(self): + def test_op_lenient_same_src_dim_none(self): lmetadata = self.cls(**self.values) right = self.values.copy() - right["location_axis"] = None + right["src_dim"] = None rmetadata = self.cls(**right) with mock.patch("iris.common.metadata._LENIENT", return_value=True): @@ -173,7 +173,7 @@ def test_op_lenient_different(self): self.assertFalse(rmetadata.__eq__(lmetadata)) def test_op_lenient_different_members(self): - for member in self.members_no_location_axis: + for member in self.members_no_src_dim: lmetadata = self.cls(**self.values) right = self.values.copy() right[member] = self.dummy @@ -185,10 +185,10 @@ def test_op_lenient_different_members(self): self.assertFalse(lmetadata.__eq__(rmetadata)) self.assertFalse(rmetadata.__eq__(lmetadata)) - def test_op_lenient_different_location_axis(self): + def test_op_lenient_different_src_dim(self): lmetadata = self.cls(**self.values) right = self.values.copy() - right["location_axis"] = self.dummy + right["src_dim"] = self.dummy rmetadata = self.cls(**right) with mock.patch("iris.common.metadata._LENIENT", return_value=True): @@ -214,7 +214,7 @@ def test_op_strict_different(self): self.assertFalse(rmetadata.__eq__(lmetadata)) def test_op_strict_different_members(self): - for member in self.members_no_location_axis: + for member in self.members_no_src_dim: lmetadata = self.cls(**self.values) right = self.values.copy() right[member] = self.dummy @@ -226,10 +226,10 @@ def test_op_strict_different_members(self): self.assertFalse(lmetadata.__eq__(rmetadata)) self.assertFalse(rmetadata.__eq__(lmetadata)) - def test_op_strict_different_location_axis(self): + def test_op_strict_different_src_dim(self): lmetadata = self.cls(**self.values) right = self.values.copy() - right["location_axis"] = self.dummy + right["src_dim"] = self.dummy rmetadata = self.cls(**right) with mock.patch("iris.common.metadata._LENIENT", return_value=False): @@ -247,7 +247,7 @@ def test_op_strict_different_none(self): self.assertFalse(rmetadata.__eq__(lmetadata)) def test_op_strict_different_members_none(self): - for member in self.members_no_location_axis: + for member in self.members_no_src_dim: lmetadata = self.cls(**self.values) right = self.values.copy() right[member] = None @@ -259,10 +259,10 @@ def test_op_strict_different_members_none(self): self.assertFalse(lmetadata.__eq__(rmetadata)) self.assertFalse(rmetadata.__eq__(lmetadata)) - def test_op_strict_different_location_axis_none(self): + def test_op_strict_different_src_dim_none(self): lmetadata = self.cls(**self.values) right = self.values.copy() - right["location_axis"] = None + right["src_dim"] = None rmetadata = self.cls(**right) with mock.patch("iris.common.metadata._LENIENT", return_value=False): @@ -311,7 +311,7 @@ def setUp(self): attributes=sentinel.attributes, cf_role=sentinel.cf_role, start_index=sentinel.start_index, - location_axis=sentinel.location_axis, + src_dim=sentinel.src_dim, ) self.dummy = sentinel.dummy self.cls = ConnectivityMetadata @@ -508,7 +508,7 @@ def setUp(self): attributes=sentinel.attributes, cf_role=sentinel.cf_role, start_index=sentinel.start_index, - location_axis=sentinel.location_axis, + src_dim=sentinel.src_dim, ) self.dummy = sentinel.dummy self.cls = ConnectivityMetadata diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py index 0d3ed932e8..717e5b5c41 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py @@ -6,6 +6,9 @@ """ Unit tests for the module :mod:`iris.fileformats._nc_load_rules.actions`. +This module provides the engine.activate() call used in the function +`iris.fileformats.netcdf._load_cube`. + """ from pathlib import Path import shutil diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__hybrid_formulae.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__hybrid_formulae.py index 3413090a3d..ca92926542 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__hybrid_formulae.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__hybrid_formulae.py @@ -238,75 +238,44 @@ def test_two_formulae(self): formula_terms=["a", "b", "depth", "eta", "orog", "sigma"], ) - def test_atmosphere_sigma_coordinate(self): - hybrid_type = "atmosphere_sigma_coordinate" - term_names = hh.CF_COORD_VERTICAL[hybrid_type] - result = self.run_testcase( - formula_root_name=hybrid_type, term_names=term_names - ) - self.check_result( - result, factory_type=hybrid_type, formula_terms=term_names - ) - - def test_atmosphere_hybrid_sigma_pressure_coordinate(self): - hybrid_type = "atmosphere_hybrid_sigma_pressure_coordinate" - term_names = hh.CF_COORD_VERTICAL[hybrid_type] - result = self.run_testcase( - formula_root_name=hybrid_type, term_names=term_names - ) - self.check_result( - result, factory_type=hybrid_type, formula_terms=term_names - ) - - def test_ocean_sigma_z_coordinate(self): - hybrid_type = "ocean_sigma_z_coordinate" - term_names = hh.CF_COORD_VERTICAL[hybrid_type] - result = self.run_testcase( - formula_root_name=hybrid_type, term_names=term_names - ) - self.check_result( - result, factory_type=hybrid_type, formula_terms=term_names - ) - - def test_ocean_sigma_coordinate(self): - hybrid_type = "ocean_sigma_coordinate" - term_names = hh.CF_COORD_VERTICAL[hybrid_type] - result = self.run_testcase( - formula_root_name=hybrid_type, term_names=term_names - ) - self.check_result( - result, factory_type=hybrid_type, formula_terms=term_names - ) - def test_ocean_s_coordinate(self): - hybrid_type = "ocean_s_coordinate" - term_names = hh.CF_COORD_VERTICAL[hybrid_type] - result = self.run_testcase( - formula_root_name=hybrid_type, term_names=term_names - ) - self.check_result( - result, factory_type=hybrid_type, formula_terms=term_names - ) +# Add in tests methods to exercise each (supported) vertical coordinate type +# individually. +# NOTE: hh.CF_COORD_VERTICAL lists all the valid types, but we don't yet +# support all of them. +_SUPPORTED_FORMULA_TYPES = ( + # NOTE: omit "atmosphere_hybrid_height_coordinate" : our basic testcase + "atmosphere_sigma_coordinate", + "atmosphere_hybrid_sigma_pressure_coordinate", + "ocean_sigma_z_coordinate", + "ocean_sigma_coordinate", + "ocean_s_coordinate", + "ocean_s_coordinate_g1", + "ocean_s_coordinate_g2", +) +for hybrid_type in _SUPPORTED_FORMULA_TYPES: - def test_ocean_s_coordinate_g1(self): - hybrid_type = "ocean_s_coordinate_g1" + def construct_inner_func(hybrid_type): term_names = hh.CF_COORD_VERTICAL[hybrid_type] - result = self.run_testcase( - formula_root_name=hybrid_type, term_names=term_names - ) - self.check_result( - result, factory_type=hybrid_type, formula_terms=term_names - ) - def test_ocean_s_coordinate_g2(self): - hybrid_type = "ocean_s_coordinate_g2" - term_names = hh.CF_COORD_VERTICAL[hybrid_type] - result = self.run_testcase( - formula_root_name=hybrid_type, term_names=term_names - ) - self.check_result( - result, factory_type=hybrid_type, formula_terms=term_names - ) + def inner(self): + result = self.run_testcase( + formula_root_name=hybrid_type, term_names=term_names + ) + self.check_result( + result, factory_type=hybrid_type, formula_terms=term_names + ) + + return inner + + # Note: use an intermediate function to generate each test method, simply to + # generate a new local variable for 'hybrid_type' on each iteration. + # Otherwise all the test methods will refer to the *same* 'hybrid_type' + # variable, i.e. the loop variable, which does not work ! + method_name = f"test_{hybrid_type}_coord" + setattr( + Test__formulae_tests, method_name, construct_inner_func(hybrid_type) + ) if __name__ == "__main__": diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__latlon_dimcoords.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__latlon_dimcoords.py deleted file mode 100644 index dfa862c4d1..0000000000 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__latlon_dimcoords.py +++ /dev/null @@ -1,337 +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 engine.activate() call within the -`iris.fileformats.netcdf._load_cube` function. - -Tests for rules behaviour in identifying latitude/longitude dim-coords, both -rotated and non-rotated. - -""" -import iris.tests as tests # isort: skip - -from iris.coord_systems import GeogCS, RotatedGeogCS -from iris.tests.unit.fileformats.nc_load_rules.actions import ( - Mixin__nc_load_actions, -) - - -class Mixin_latlon_dimcoords(Mixin__nc_load_actions): - # Tests for the recognition and construction of latitude/longitude coords. - - # Control to test either longitude or latitude coords. - # Set by inheritor classes, which are actual TestCases. - lat_1_or_lon_0 = None - - def setUp(self): - super().setUp() - # Generate some useful settings : just to generalise operation over - # both latitude and longitude. - islat = self.lat_1_or_lon_0 - assert islat in (0, 1) - self.unrotated_name = "latitude" if islat else "longitude" - self.rotated_name = "grid_latitude" if islat else "grid_longitude" - self.unrotated_units = "degrees_north" if islat else "degrees_east" - # Note: there are many alternative valid forms for the rotated units, - # but we are not testing that here. - self.rotated_units = "degrees" # NB this one is actually constant - self.axis = "y" if islat else "x" - - def _make_testcase_cdl( - self, - standard_name=None, - long_name=None, - var_name=None, - units=None, - axis=None, - grid_mapping=None, - ): - # Inner routine called by 'run_testcase' (in Mixin__nc_load_actions), - # to generate CDL which is then translated into a testfile and loaded. - if var_name is None: - # Can't have *no* var-name - # N.B. it is also the name of the dimension. - var_name = "dim" - - def attribute_str(name, value): - if value is None or value == "": - result = "" - else: - result = f'{var_name}:{name} = "{value}" ;' - - return result - - standard_name_str = attribute_str("standard_name", standard_name) - long_name_str = attribute_str("long_name", long_name) - units_str = attribute_str("units", units) - axis_str = attribute_str("axis", axis) - if grid_mapping: - grid_mapping_str = 'phenom:grid_mapping = "crs" ;' - else: - grid_mapping_str = "" - - assert grid_mapping in (None, "latlon", "rotated") - if grid_mapping is None: - crs_str = "" - elif grid_mapping == "latlon": - crs_str = """ - int crs ; - crs:grid_mapping_name = "latitude_longitude" ; - crs:semi_major_axis = 6371000.0 ; - crs:inverse_flattening = 1000. ; -""" - elif grid_mapping == "rotated": - crs_str = """ - int crs ; - crs:grid_mapping_name = "rotated_latitude_longitude" ; - crs:grid_north_pole_latitude = 32.5 ; - crs:grid_north_pole_longitude = 170. ; -""" - - cdl_string = f""" -netcdf test {{ - dimensions: - {var_name} = 2 ; - variables: - double {var_name}({var_name}) ; - {standard_name_str} - {units_str} - {long_name_str} - {axis_str} - double phenom({var_name}) ; - phenom:standard_name = "air_temperature" ; - phenom:units = "K" ; - {grid_mapping_str} - {crs_str} - data: - {var_name} = 0., 1. ; -}} -""" - return cdl_string - - def check_result( - self, - cube, - standard_name, - long_name, - units, - crs=None, - context_message="", - ): - # Check the existence, standard-name, long-name, units and coord-system - # of the resulting coord. Also that it is always a dim-coord. - # NOTE: there is no "axis" arg, as this information does *not* appear - # as a separate property (or attribute) of the resulting coord. - # However, whether the file variable has an axis attribute *does* - # affect the results here, in some cases. - coords = cube.coords() - # There should be one and only one coord. - self.assertEqual(1, len(coords)) - # It should also be a dim-coord - self.assertEqual(1, len(cube.coords(dim_coords=True))) - (coord,) = coords - if self.debug: - print("") - print("DEBUG : result coord =", coord) - print("") - - coord_stdname, coord_longname, coord_units, coord_crs = [ - getattr(coord, name) - for name in ("standard_name", "long_name", "units", "coord_system") - ] - self.assertEqual(standard_name, coord_stdname, context_message) - self.assertEqual(long_name, coord_longname, context_message) - self.assertEqual(units, coord_units, context_message) - assert crs in (None, "latlon", "rotated") - if crs is None: - self.assertEqual(None, coord_crs, context_message) - elif crs == "latlon": - self.assertIsInstance(coord_crs, GeogCS, context_message) - elif crs == "rotated": - self.assertIsInstance(coord_crs, RotatedGeogCS, context_message) - - # - # Testcase routines - # - # NOTE: all these testcases have been verified against the older behaviour - # in v3.0.4, based on Pyke rules. - # - - def test_minimal(self): - # Nothing but a var-name --> unrecognised dim-coord. - result = self.run_testcase() - self.check_result(result, None, None, "unknown") - - def test_fullinfo_unrotated(self): - # Check behaviour with all normal info elements for 'unrotated' case. - # Includes a grid-mapping, but no axis (should not be needed). - result = self.run_testcase( - standard_name=self.unrotated_name, - units=self.unrotated_units, - grid_mapping="latlon", - ) - self.check_result( - result, self.unrotated_name, None, "degrees", "latlon" - ) - - def test_fullinfo_rotated(self): - # Check behaviour with all normal info elements for 'rotated' case. - # Includes a grid-mapping, but no axis (should not be needed). - result = self.run_testcase( - standard_name=self.rotated_name, - units=self.rotated_units, - grid_mapping="rotated", - ) - self.check_result( - result, self.rotated_name, None, "degrees", "rotated" - ) - - def test_axis(self): - # A suitable axis --> unrotated lat/lon coord, but unknown units. - result = self.run_testcase(axis=self.axis) - self.check_result(result, self.unrotated_name, None, "unknown") - - def test_units_unrotated(self): - # With a unit like 'degrees_east', we automatically identify this as a - # latlon coord, *and* convert units to plain 'degrees' on loading. - result = self.run_testcase(units=self.unrotated_units) - self.check_result(result, self.unrotated_name, None, "degrees") - - def test_units_rotated(self): - # With no info except a "degrees" unit, we **don't** identify a latlon, - # i.e. we do not set the standard-name - result = self.run_testcase(units="degrees") - self.check_result(result, None, None, "degrees") - - def test_units_unrotated_gridmapping(self): - # With an unrotated unit *AND* a suitable grid-mapping, we identify a - # rotated latlon coordinate + assign it the coord-system. - result = self.run_testcase( - units=self.unrotated_units, grid_mapping="latlon" - ) - self.check_result( - result, self.unrotated_name, None, "degrees", "latlon" - ) - - def test_units_rotated_gridmapping_noname(self): - # Rotated units and grid-mapping, but *without* the expected name. - # Does not translate, no coord-system (i.e. grid-mapping is discarded). - result = self.run_testcase( - units="degrees", - grid_mapping="rotated", - ) - self.check_result(result, None, None, "degrees", None) - - def test_units_rotated_gridmapping_withname(self): - # With a "degrees" unit, a rotated grid-mapping *AND* a suitable - # standard-name, it recognises a rotated dimcoord. - result = self.run_testcase( - standard_name=self.rotated_name, - units="degrees", - grid_mapping="rotated", - ) - self.check_result( - result, self.rotated_name, None, "degrees", "rotated" - ) - - def test_units_rotated_gridmapping_varname(self): - # Same but with var-name containing the standard-name : in this case we - # get NO COORDINATE-SYSTEM (which is a bit weird). - result = self.run_testcase( - var_name=self.rotated_name, - units="degrees", - grid_mapping="rotated", - ) - self.check_result(result, self.rotated_name, None, "degrees", None) - - def test_varname_unrotated(self): - # With a recognised name in the var-name, we set standard-name. - # But units are left undetermined. - result = self.run_testcase(var_name=self.unrotated_name) - self.check_result(result, self.unrotated_name, None, "unknown") - - def test_varname_rotated(self): - # With a *rotated* name in the var-name, we set standard-name. - # But units are left undetermined. - result = self.run_testcase(var_name=self.rotated_name) - self.check_result(result, self.rotated_name, None, "unknown") - - def test_varname_unrotated_units_rotated(self): - # With a "degrees" unit and a suitable var-name, we do identify - # (= set standard-name). - # N.B. this accepts "degrees" as a generic term, and so does *not* - # interpret it as a rotated coordinate. - result = self.run_testcase( - var_name=self.unrotated_name, units="degrees" - ) - self.check_result(result, self.unrotated_name, None, "degrees") - - def test_longname(self): - # A recognised form in long-name is *not* translated into standard-name. - result = self.run_testcase(long_name=self.unrotated_name) - self.check_result(result, None, self.unrotated_name, "unknown") - - def test_stdname_unrotated(self): - # Only an (unrotated) standard name : units is not specified - result = self.run_testcase(standard_name=self.unrotated_name) - self.check_result(result, self.unrotated_name, None, None) - - def test_stdname_rotated(self): - # Only a (rotated) standard name : units is not specified - result = self.run_testcase(standard_name=self.rotated_name) - self.check_result(result, self.rotated_name, None, None) - - def test_stdname_unrotated_gridmapping(self): - # An unrotated standard-name and grid-mapping, translates into a - # coordinate system. - result = self.run_testcase( - standard_name=self.unrotated_name, grid_mapping="latlon" - ) - self.check_result( - result, self.unrotated_name, None, "unknown", "latlon" - ) - - def test_stdname_rotated_gridmapping(self): - # An *rotated* standard-name and grid-mapping, translates into a - # coordinate system. - result = self.run_testcase( - standard_name=self.rotated_name, grid_mapping="rotated" - ) - self.check_result(result, self.rotated_name, None, None, "rotated") - - -class Test__longitude_coords(Mixin_latlon_dimcoords, tests.IrisTest): - lat_1_or_lon_0 = 0 - - @classmethod - def setUpClass(cls): - super().setUpClass() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - - def setUp(self): - super().setUp() - - -class Test__latitude_coords(Mixin_latlon_dimcoords, tests.IrisTest): - lat_1_or_lon_0 = 1 - - @classmethod - def setUpClass(cls): - super().setUpClass() - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - - def setUp(self): - super().setUp() - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/fileformats/netcdf/test_Saver__ugrid.py b/lib/iris/tests/unit/fileformats/netcdf/test_Saver__ugrid.py index a914dd3314..87c2df7d45 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test_Saver__ugrid.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test_Saver__ugrid.py @@ -184,7 +184,7 @@ def mesh_location_size(mesh, location): if conn is None: result = 0 else: - result = conn.shape[conn.location_axis] + result = conn.shape[conn.src_dim] return result @@ -705,7 +705,7 @@ def test_connectivity_dim_order(self): # Get the face-node and edge-node connectivities face_nodes_conn = mesh.face_node_connectivity edge_nodes_conn = mesh.edge_node_connectivity - # Transpose them : N.B. this sets location_axis=1, as it should be. + # Transpose them : N.B. this sets src_dim=1, as it should be. nodesfirst_faces_conn = face_nodes_conn.transpose() nodesfirst_edges_conn = edge_nodes_conn.transpose() # Make a new mesh with both face and edge connectivities 'transposed'. diff --git a/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py b/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py index c9c4821e0a..0f2a8a2d4b 100644 --- a/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py +++ b/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py @@ -65,7 +65,7 @@ def test_3d(self): def test_multiple_odd_dims(self): # Test to ensure multiple collapsed dimensions don't interfere. # make a 5-D array where dimensions 0, 2 and 3 are degenerate. - array = np.arange(3**5).reshape([3] * 5) + array = np.arange(3 ** 5).reshape([3] * 5) array[1:] = array[0:1] array[:, :, 1:] = array[:, :, 0:1] array[:, :, :, 1:] = array[:, :, :, 0:1] diff --git a/lib/iris/tests/unit/io/test__generate_cubes.py b/lib/iris/tests/unit/io/test__generate_cubes.py deleted file mode 100755 index 3a896a111c..0000000000 --- a/lib/iris/tests/unit/io/test__generate_cubes.py +++ /dev/null @@ -1,37 +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.io._generate_cubes` function.""" - -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests # isort:skip - -from pathlib import Path - -import iris - - -class TestGenerateCubes(tests.IrisTest): - def test_pathlib_paths(self): - test_variants = [ - ("string", "string"), - (["string"], "string"), - (Path("string"), Path("string")), - ] - - decode_uri_mock = self.patch( - "iris.iris.io.decode_uri", return_value=("file", None) - ) - self.patch("iris.iris.io.load_files") - - for gc_arg, du_arg in test_variants: - decode_uri_mock.reset_mock() - list(iris._generate_cubes(gc_arg, None, None)) - decode_uri_mock.assert_called_with(du_arg) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/io/test_save.py b/lib/iris/tests/unit/io/test_save.py deleted file mode 100755 index b92e26f2d1..0000000000 --- a/lib/iris/tests/unit/io/test_save.py +++ /dev/null @@ -1,45 +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.io.save` function.""" - -# Import iris.tests first so that some things can be initialised before -# importing anything else. -import iris.tests as tests # isort:skip - -from pathlib import Path -from unittest import mock - -import iris -from iris.cube import Cube - - -class TestSave(tests.IrisTest): - def test_pathlib_save(self): - file_mock = mock.Mock() - # Have to configure after creation because "name" is special - file_mock.configure_mock(name="string") - - find_saver_mock = self.patch( - "iris.io.find_saver", return_value=(lambda *args, **kwargs: None) - ) - - test_variants = [ - ("string", "string"), - (Path("string/string"), "string/string"), - (file_mock, "string"), - ] - - for target, fs_val in test_variants: - try: - iris.save(Cube([]), target) - except ValueError: - print("ValueError") - pass - find_saver_mock.assert_called_with(fs_val) - - -if __name__ == "__main__": - tests.main() diff --git a/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py b/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py index 40a932b9e0..8370c719f0 100644 --- a/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py +++ b/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py @@ -142,7 +142,7 @@ def test_columns_long_attribute(self): " Attributes:", ( " very_very_very_very_very_long_name " - "'longish string extends beyond dim columns'" + "longish string extends beyond dim columns" ), ] self.assertEqual(rep, expected) @@ -442,7 +442,7 @@ def test_section_cube_attributes(self): " Attributes:", " list [3]", " number 1.2", - " string 'four five in a string'", + " string four five in a string", " z_tupular (6, (7, 8))", ] self.assertEqual(rep, expected) @@ -464,7 +464,7 @@ def test_section_cube_attributes__string_extras(self): " Attributes:", " escaped 'escaped\\tstring'", ( - " long 'this is very very very " + " long this is very very very " "very very very very very very very very very very..." ), ( diff --git a/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py b/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py index 3e411c020d..c8af3437e6 100644 --- a/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py +++ b/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py @@ -183,11 +183,7 @@ def test_attributes(self): attribute_section = rep.scalar_sections["Attributes:"] attribute_contents = attribute_section.contents - expected_contents = [ - "a: 1", - "b: 'two'", - "c: ' this \\n that\\tand.'", - ] + expected_contents = ["a: 1", "b: two", "c: ' this \\n that\\tand.'"] # Note: a string with \n or \t in it gets "repr-d". # Other strings don't (though in coord 'extra' lines, they do.) diff --git a/lib/iris/util.py b/lib/iris/util.py index 53cd78724e..9ab413a493 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -10,6 +10,8 @@ from abc import ABCMeta, abstractmethod from collections.abc import Hashable, Iterable +from contextlib import contextmanager +import copy import functools import inspect import os @@ -394,24 +396,10 @@ def normalise_array(array): def approx_equal(a, b, max_absolute_error=1e-10, max_relative_error=1e-10): """ - Returns whether two numbers are almost equal, allowing for the finite - precision of floating point numbers. - - .. deprecated:: 3.2.0 - - Instead use :func:`math.isclose`. For example, rather than calling - ``approx_equal(a, b, max_abs, max_rel)`` replace with ``math.isclose(a, - b, max_rel, max_abs)``. Note that :func:`~math.isclose` will return True - if the actual error equals the maximum, whereas :func:`util.approx_equal` - will return False. + Returns whether two numbers are almost equal, allowing for the + finite precision of floating point numbers. """ - wmsg = ( - "iris.util.approx_equal has been deprecated and will be removed, " - "please use math.isclose instead." - ) - warn_deprecated(wmsg) - # Deal with numbers close to zero if abs(a - b) < max_absolute_error: return True @@ -1066,20 +1054,18 @@ def format_array(arr): """ + summary_insert = "" summary_threshold = 85 - summary_insert = "..." if arr.size > summary_threshold else "" edge_items = 3 ffunc = str + formatArray = np.core.arrayprint._formatArray max_line_len = 50 - - # Format the array with version 1.13 legacy behaviour - with np.printoptions(legacy="1.13"): - # Use this (private) routine for more control. - formatArray = np.core.arrayprint._formatArray - # N.B. the 'legacy' arg had different forms in different numpy versions - # -- fetch the required form from the internal options dict - format_options_legacy = np.core.arrayprint._format_options["legacy"] - + legacy = "1.13" + if arr.size > summary_threshold: + summary_insert = "..." + options = np.get_printoptions() + options["legacy"] = legacy + with _printopts_context(**options): result = formatArray( arr, ffunc, @@ -1088,12 +1074,29 @@ def format_array(arr): separator=", ", edge_items=edge_items, summary_insert=summary_insert, - legacy=format_options_legacy, + legacy=legacy, ) return result +@contextmanager +def _printopts_context(**kwargs): + """ + Update the numpy printoptions for the life of this context manager. + + Note: this function can be removed with numpy>=1.15 thanks to + https://github.com/numpy/numpy/pull/10406 + + """ + original_opts = np.get_printoptions() + np.set_printoptions(**kwargs) + try: + yield + finally: + np.set_printoptions(**original_opts) + + def new_axis(src_cube, scalar_coord=None): """ Create a new axis as the leading dimension of the cube, promoting a scalar @@ -1166,6 +1169,133 @@ def new_axis(src_cube, scalar_coord=None): return new_cube +def as_compatible_shape(src_cube, target_cube): + """ + Return a cube with added length one dimensions to match the dimensionality + and dimension ordering of `target_cube`. + + This function can be used to add the dimensions that have been collapsed, + aggregated or sliced out, promoting scalar coordinates to length one + dimension coordinates where necessary. It operates by matching coordinate + metadata to infer the dimensions that need modifying, so the provided + cubes must have coordinates with the same metadata + (see :class:`iris.common.CoordMetadata`). + + .. note:: This function will load and copy the data payload of `src_cube`. + + .. deprecated:: 3.0.0 + + Instead use :class:`~iris.common.resolve.Resolve`. For example, rather + than calling ``as_compatible_shape(src_cube, target_cube)`` replace + with ``Resolve(src_cube, target_cube)(target_cube.core_data())``. + + Args: + + * src_cube: + An instance of :class:`iris.cube.Cube` with missing dimensions. + + * target_cube: + An instance of :class:`iris.cube.Cube` with the desired dimensionality. + + Returns: + A instance of :class:`iris.cube.Cube` with the same dimensionality as + `target_cube` but with the data and coordinates from `src_cube` + suitably reshaped to fit. + + """ + from iris.cube import Cube + + wmsg = ( + "iris.util.as_compatible_shape has been deprecated and will be " + "removed, please use iris.common.resolve.Resolve instead." + ) + warn_deprecated(wmsg) + + dim_mapping = {} + for coord in target_cube.aux_coords + target_cube.dim_coords: + dims = target_cube.coord_dims(coord) + try: + collapsed_dims = src_cube.coord_dims(coord) + except iris.exceptions.CoordinateNotFoundError: + continue + if collapsed_dims: + if len(collapsed_dims) == len(dims): + for dim_from, dim_to in zip(dims, collapsed_dims): + dim_mapping[dim_from] = dim_to + elif dims: + for dim_from in dims: + dim_mapping[dim_from] = None + + if len(dim_mapping) != target_cube.ndim: + raise ValueError( + "Insufficient or conflicting coordinate " + "metadata. Cannot infer dimension mapping " + "to restore cube dimensions." + ) + + new_shape = [1] * target_cube.ndim + for dim_from, dim_to in dim_mapping.items(): + if dim_to is not None: + new_shape[dim_from] = src_cube.shape[dim_to] + + new_data = src_cube.data.copy() + + # Transpose the data (if necessary) to prevent assignment of + # new_shape doing anything except adding length one dims. + order = [v for k, v in sorted(dim_mapping.items()) if v is not None] + if order != sorted(order): + new_order = [order.index(i) for i in range(len(order))] + new_data = np.transpose(new_data, new_order).copy() + + new_cube = Cube(new_data.reshape(new_shape)) + new_cube.metadata = copy.deepcopy(src_cube.metadata) + + # Record a mapping from old coordinate IDs to new coordinates, + # for subsequent use in creating updated aux_factories. + coord_mapping = {} + + reverse_mapping = {v: k for k, v in dim_mapping.items() if v is not None} + + def add_coord(coord): + """Closure used to add a suitably reshaped coord to new_cube.""" + all_dims = target_cube.coord_dims(coord) + src_dims = [ + dim + for dim in src_cube.coord_dims(coord) + if src_cube.shape[dim] > 1 + ] + mapped_dims = [reverse_mapping[dim] for dim in src_dims] + length1_dims = [dim for dim in all_dims if new_cube.shape[dim] == 1] + dims = length1_dims + mapped_dims + shape = [new_cube.shape[dim] for dim in dims] + if not shape: + shape = [1] + points = coord.points.reshape(shape) + bounds = None + if coord.has_bounds(): + bounds = coord.bounds.reshape(shape + [coord.nbounds]) + new_coord = coord.copy(points=points, bounds=bounds) + # If originally in dim_coords, add to dim_coords, otherwise add to + # aux_coords. + if target_cube.coords(coord, dim_coords=True): + try: + new_cube.add_dim_coord(new_coord, dims) + except ValueError: + # Catch cases where the coord is an AuxCoord and therefore + # cannot be added to dim_coords. + new_cube.add_aux_coord(new_coord, dims) + else: + new_cube.add_aux_coord(new_coord, dims) + coord_mapping[id(coord)] = new_coord + + for coord in src_cube.aux_coords + src_cube.dim_coords: + add_coord(coord) + for factory in src_cube.aux_factories: + new_cube.add_aux_factory(factory.updated(coord_mapping)) + + return new_cube + + def squeeze(cube): """ Removes any dimension of length one. If it has an associated DimCoord or diff --git a/noxfile.py b/noxfile.py index 820959060a..e7b7e43f1b 100755 --- a/noxfile.py +++ b/noxfile.py @@ -16,7 +16,7 @@ nox.options.reuse_existing_virtualenvs = True #: Python versions we can run sessions under -_PY_VERSIONS_ALL = ["3.8"] +_PY_VERSIONS_ALL = ["3.7", "3.8"] _PY_VERSION_LATEST = _PY_VERSIONS_ALL[-1] #: One specific python version for docs builds @@ -28,13 +28,6 @@ #: Default cartopy cache directory. CARTOPY_CACHE_DIR = os.environ.get("HOME") / Path(".local/share/cartopy") -# https://github.com/numpy/numpy/pull/19478 -# https://github.com/matplotlib/matplotlib/pull/22099 -#: Common session environment variables. -ENV = dict( - NPY_DISABLE_CPU_FEATURES="AVX512F,AVX512CD,AVX512VL,AVX512BW,AVX512DQ,AVX512_SKX" -) - def session_lockfile(session: nox.sessions.Session) -> Path: """Return the path of the session lockfile.""" @@ -217,7 +210,6 @@ def tests(session: nox.sessions.Session): """ prepare_venv(session) session.install("--no-deps", "--editable", ".") - session.env.update(ENV) session.run( "python", "-m", @@ -240,7 +232,6 @@ def doctest(session: nox.sessions.Session): """ prepare_venv(session) session.install("--no-deps", "--editable", ".") - session.env.update(ENV) session.cd("docs") session.run( "make", diff --git a/pyproject.toml b/pyproject.toml index 26e6ae727a..8d01db2af7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 79 -target-version = ['py38'] +target-version = ['py37', 'py38'] include = '\.pyi?$' extend-exclude = ''' ( diff --git a/requirements/ci/nox.lock/py37-linux-64.lock b/requirements/ci/nox.lock/py37-linux-64.lock new file mode 100644 index 0000000000..a9a1c52811 --- /dev/null +++ b/requirements/ci/nox.lock/py37-linux-64.lock @@ -0,0 +1,231 @@ +# Generated by conda-lock. +# platform: linux-64 +# input_hash: f2449723874977e1bb5b571ab6f475f8d251e472d6f8826bd40fbb774e3916f7 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 +https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2021.10.8-ha878542_0.tar.bz2#575611b8a84f45960e87722eeb51fa26 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb +https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-hab24e00_0.tar.bz2#19410c3df09dfb12d1206132a1d357c5 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.36.1-hea4e1c9_2.tar.bz2#bd4f2e711b39af170e7ff15163fe87ee +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-11.2.0-h5c6108e_11.tar.bz2#2dcb18a9a0fa31f4f29e5a9b3eade394 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-11.2.0-he4da1e4_11.tar.bz2#0bf83958e788f1e75ba26154cb702afe +https://conda.anaconda.org/conda-forge/linux-64/mpi-1.0-mpich.tar.bz2#c1fcff3417b5a22bbc4cf6e8c23648cf +https://conda.anaconda.org/conda-forge/linux-64/mysql-common-8.0.27-ha770c72_1.tar.bz2#ca56dd3e2768f99f9e19869efb3434ec +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-11.2.0-h69a702a_11.tar.bz2#4ea2f9f83b617a7682e8aa05dcb37c6a +https://conda.anaconda.org/conda-forge/linux-64/libgomp-11.2.0-h1d223b6_11.tar.bz2#1d16527c76842bf9c41e9399d39d8097 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-1_gnu.tar.bz2#561e277319a41d4f24f5c05a9ef63c04 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-11.2.0-h1d223b6_11.tar.bz2#e3495f4f93cfd6b68021cbe2b5844cd5 +https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.3-h516909a_0.tar.bz2#1378b88874f42ac31b2f8e4f6975cb7b +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h7f98852_4.tar.bz2#a1fd65c7ccbf10880423d82bca54eb54 +https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.18.1-h7f98852_0.tar.bz2#f26ef8098fab1f719c91eb760d63381a +https://conda.anaconda.org/conda-forge/linux-64/expat-2.4.1-h9c3ff4c_0.tar.bz2#16054ef3cb3ec5d8d29d08772662f65d +https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8 +https://conda.anaconda.org/conda-forge/linux-64/geos-3.10.1-h9c3ff4c_1.tar.bz2#17a5f413039ce1e105fab5df9c668eb5 +https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.1-h36c2ea0_2.tar.bz2#626e68ae9cc5912d6adb79d318cf962d +https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.13-h58526e2_1001.tar.bz2#8c54672728e8ec6aa6db90cf2806d220 +https://conda.anaconda.org/conda-forge/linux-64/icu-68.2-h9c3ff4c_0.tar.bz2#6618c9b191638993f2a818c6529e1b49 +https://conda.anaconda.org/conda-forge/linux-64/jbig-2.1-h7f98852_2003.tar.bz2#1aa0cee79792fa97b7ff4545110b60bf +https://conda.anaconda.org/conda-forge/linux-64/jpeg-9d-h36c2ea0_0.tar.bz2#ea02ce6037dbe81803ae6123e5ba1568 +https://conda.anaconda.org/conda-forge/linux-64/lerc-3.0-h9c3ff4c_0.tar.bz2#7fcefde484980d23f0ec24c11e314d2e +https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.0.9-h7f98852_6.tar.bz2#b0f44f63f7d771d7670747a1dd5d5ac1 +https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.8-h7f98852_0.tar.bz2#91d22aefa665265e8e31988b15145c8a +https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-h516909a_1.tar.bz2#6f8720dff19e17ce5d48cfe7f3d2f0a3 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 +https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.16-h516909a_0.tar.bz2#5c0f338a513a2943c659ae619fca9211 +https://conda.anaconda.org/conda-forge/linux-64/libmo_unpack-3.1.2-hf484d3e_1001.tar.bz2#95f32a6a5a666d33886ca5627239f03d +https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.0-h7f98852_0.tar.bz2#39b1328babf85c7c3a61636d9cd50206 +https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.18-pthreads_h8fe5266_0.tar.bz2#41532e4448c0cce086d6570f95e4e12e +https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f +https://conda.anaconda.org/conda-forge/linux-64/libtool-2.4.6-h9c3ff4c_1008.tar.bz2#16e143a1ed4b4fd169536373957f6fee +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.32.1-h7f98852_1000.tar.bz2#772d69f030955d9646d3d0eaf21d859d +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.2.1-h7f98852_0.tar.bz2#90607c4c0247f04ec98b48997de71c1a +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.11-h36c2ea0_1013.tar.bz2#dcddf696ff5dfcab567100d691678e18 +https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.3-h9c3ff4c_1.tar.bz2#fbe97e8fa6f275d7c76a09e795adc3e6 +https://conda.anaconda.org/conda-forge/linux-64/mpich-3.4.2-h846660c_101.tar.bz2#9478c28797583eff62a58853f92ad7f6 +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.2-h58526e2_4.tar.bz2#509f2a21c4a09214cd737a480dfd80c9 +https://conda.anaconda.org/conda-forge/linux-64/nspr-4.32-h9c3ff4c_1.tar.bz2#29ded371806431b0499aaee146abfc3e +https://conda.anaconda.org/conda-forge/linux-64/openssl-1.1.1l-h7f98852_0.tar.bz2#de7b38a1542dbe6f41653a8ae71adc53 +https://conda.anaconda.org/conda-forge/linux-64/pcre-8.45-h9c3ff4c_0.tar.bz2#c05d1820a6d34ff07aaaab7a9b7eddaa +https://conda.anaconda.org/conda-forge/linux-64/pixman-0.40.0-h36c2ea0_0.tar.bz2#660e72c82f2e75a6b3fe6a6e75c79f19 +https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036 +https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2#4b230e8381279d76131116660f5a241a +https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.0.10-h7f98852_0.tar.bz2#d6b0b50b49eccfe0be0373be628be0f3 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.9-h7f98852_0.tar.bz2#bf6f803a544f26ebbdc3bfff272eb179 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.3-h7f98852_0.tar.bz2#be93aabceefa2fac576e971aef407908 +https://conda.anaconda.org/conda-forge/linux-64/xorg-renderproto-0.11.1-h7f98852_1002.tar.bz2#06feff3d2634e3097ce2fe681474b534 +https://conda.anaconda.org/conda-forge/linux-64/xorg-xextproto-7.3.0-h7f98852_1002.tar.bz2#1e15f6ad85a7d743a2ac68dae6c82b98 +https://conda.anaconda.org/conda-forge/linux-64/xorg-xproto-7.0.31-h7f98852_1007.tar.bz2#b4a4381d54784606820704f7b5f05a15 +https://conda.anaconda.org/conda-forge/linux-64/xxhash-0.8.0-h7f98852_3.tar.bz2#52402c791f35e414e704b7a113f99605 +https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.5-h516909a_1.tar.bz2#33f601066901f3e1a85af3522a8113f9 +https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h516909a_0.tar.bz2#03a530e925414902547cf48da7756db8 +https://conda.anaconda.org/conda-forge/linux-64/gettext-0.19.8.1-h73d1719_1008.tar.bz2#af49250eca8e139378f8ff0ae9e57251 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-12_linux64_openblas.tar.bz2#4f93ba28c628a2c27cf39c055e6b219c +https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.0.9-h7f98852_6.tar.bz2#c7c03a2592cac92246a13a0732bd1573 +https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.0.9-h7f98852_6.tar.bz2#28bfe0a70154e6881da7bae97517c948 +https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 +https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.10-h9b69904_4.tar.bz2#390026683aef81db27ff1b8570ca1336 +https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 +https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.13-h7f98852_1004.tar.bz2#b3653fdc58d03face9724f602218a904 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.1-h46c0cb4_0.tar.bz2#5788de3c8d7a7d64ac56c784c4ef48e6 +https://conda.anaconda.org/conda-forge/linux-64/udunits2-2.2.27.27-hc3e0081_3.tar.bz2#a47110f41fcbf88fcdf8549d7f69a6d8 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.3-hd9c2040_1000.tar.bz2#9e856f78d5c80d5a78f61e72d1d473a3 +https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.11-h36c2ea0_1013.tar.bz2#cf7190238072a41e9579e4476a6a60b8 +https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.0.9-h7f98852_6.tar.bz2#9e94bf16f14c78a36561d5019f490d22 +https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h10796ff_3.tar.bz2#21a8d66dc17f065023b33145c42652fe +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-12_linux64_openblas.tar.bz2#2e5082d4a9a18c21100e6ce5b6bcb4ec +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.70.1-h174f98d_0.tar.bz2#b14ca7dc99d099dccf38d4d31ead3d63 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-12_linux64_openblas.tar.bz2#9f401a6807a97e0c859d7522ae3d51ec +https://conda.anaconda.org/conda-forge/linux-64/libllvm11-11.1.0-hf817b99_2.tar.bz2#646fa2f7c60b69ee8f918668e9c2fd31 +https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.43.0-h812cca2_1.tar.bz2#d0a7846b7b3b8fb0d8b36904a53b8155 +https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.37-h21135ba_2.tar.bz2#b6acf807307d033d4b7e758b4f44b036 +https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.10.0-ha56f1ee_2.tar.bz2#6ab4eaa11ff01801cffca0a27489dc04 +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.9.12-h72842e0_0.tar.bz2#bd14fdf5b9ee5568056a40a6a2f41866 +https://conda.anaconda.org/conda-forge/linux-64/libzip-1.8.0-h4de3113_1.tar.bz2#175a746a43d42c053b91aa765fbc197d +https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.37.0-h9cd32fc_0.tar.bz2#eb66fc098824d25518a79e83d12a81d6 +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.11-h27826a3_1.tar.bz2#84e76fb280e735fec1efd2d21fd9cb27 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.7.2-h7f98852_0.tar.bz2#12a61e640b8894504326aadafccbb790 +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.0-ha95c52a_0.tar.bz2#b56f94865e2de36abf054e7bfa499034 +https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.36.0-h3371d22_4.tar.bz2#661e1ed5d92552785d9f8c781ce68685 +https://conda.anaconda.org/conda-forge/linux-64/brotli-1.0.9-h7f98852_6.tar.bz2#612385c4a83edb0619fe911d9da317f4 +https://conda.anaconda.org/conda-forge/linux-64/freetype-2.10.4-h0708190_1.tar.bz2#4a06f2ac2e5bfae7b6b245171c3f07aa +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.70.1-h780b84a_0.tar.bz2#e60c13f740e8988a6efa2822d63738bd +https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.18.5-h9f60fe5_2.tar.bz2#6221115a24700aa8598ae5aa1574902d +https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h64030ff_2.tar.bz2#112eb9b5b93f0c02e59aea4fd1967363 +https://conda.anaconda.org/conda-forge/linux-64/krb5-1.19.2-hcc1bbae_3.tar.bz2#e29650992ae593bc05fc93722483e5c3 +https://conda.anaconda.org/conda-forge/linux-64/libclang-11.1.0-default_ha53f305_1.tar.bz2#b9b71585ca4fcb5d442c5a9df5dd7e98 +https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.3.0-h6f004c6_2.tar.bz2#34fda41ca84e67232888c9a885903055 +https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.0.3-he3ba5ed_0.tar.bz2#f9dbabc7e01c459ed7a1d1d64b206e9b +https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.0.27-hfa10184_1.tar.bz2#577ac13be280c34b2f0cfd44267d2eac +https://conda.anaconda.org/conda-forge/linux-64/nss-3.73-hb5efdd6_0.tar.bz2#a5b91a14292ac34bac1f0506a3772fd5 +https://conda.anaconda.org/conda-forge/linux-64/python-3.7.12-hb7a2778_100_cpython.tar.bz2#2d94b3e6a9fdaf83f6955d008c8011a7 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.4-h7f98852_1.tar.bz2#536cc5db4d0a3ba0630541aec064b5e4 +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.10-h7f98852_1003.tar.bz2#f59c1242cc1dd93e72c2ee2b360979eb +https://conda.anaconda.org/conda-forge/noarch/alabaster-0.7.12-py_0.tar.bz2#2489a97287f90176ecdc3ca982b4b0a0 +https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyh9f0ad1d_0.tar.bz2#5f095bc6454094e96f146491fd03633b +https://conda.anaconda.org/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_0.tar.bz2#ebb5f5f7dc4f1a3780ef7ea7738db08c +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-2.0.8-pyhd8ed1ab_0.tar.bz2#7bc3465ff423c243bf8b2e77c5075bc7 +https://conda.anaconda.org/conda-forge/noarch/cloudpickle-2.0.0-pyhd8ed1ab_0.tar.bz2#3a8fc8b627d5fb6af827e126a10a86c6 +https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.4-pyh9f0ad1d_0.tar.bz2#c08b4c1326b880ed44f3ffb04803332f +https://conda.anaconda.org/conda-forge/noarch/cycler-0.11.0-pyhd8ed1ab_0.tar.bz2#a50559fad0affdbb33729a68669ca1cb +https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.3-pyhd8ed1ab_0.tar.bz2#cc7dae067bb31c1598e23e151dfca986 +https://conda.anaconda.org/conda-forge/noarch/filelock-3.4.0-pyhd8ed1ab_0.tar.bz2#caff9785491992b3250ed4048fe51e2c +https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.13.1-hba837de_1005.tar.bz2#fd3611672eb91bc9d24fd6fb970037eb +https://conda.anaconda.org/conda-forge/noarch/fsspec-2021.11.1-pyhd8ed1ab_0.tar.bz2#a510ec93fdb50775091d2afba98a8acb +https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.42.6-h04a7f16_0.tar.bz2#b24a1e18325a6e8f8b6b4a2ec5860ce2 +https://conda.anaconda.org/conda-forge/linux-64/glib-2.70.1-h780b84a_0.tar.bz2#bc4795f9d11dba7990c7e9cf9f7021b3 +https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.18.5-hf529b03_2.tar.bz2#3cf866063f2803944ddaee8b1d6da531 +https://conda.anaconda.org/conda-forge/noarch/idna-3.1-pyhd3deb0d_0.tar.bz2#9c9aea4b8391264477df484f798562d0 +https://conda.anaconda.org/conda-forge/noarch/imagesize-1.3.0-pyhd8ed1ab_0.tar.bz2#be807e7606fff9436e5e700f6bffb7c6 +https://conda.anaconda.org/conda-forge/noarch/iris-sample-data-2.4.0-pyhd8ed1ab_0.tar.bz2#18ee9c07cf945a33f92caf1ee3d23ad9 +https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.12-hddcbb42_0.tar.bz2#797117394a4aa588de6d741b06fad80f +https://conda.anaconda.org/conda-forge/linux-64/libcurl-7.80.0-h2574ce0_0.tar.bz2#5d0784b790350f7939bb5d3f2c32e700 +https://conda.anaconda.org/conda-forge/linux-64/libpq-13.5-hd57d9b9_0.tar.bz2#20a3e094316bbaa7b890ccbd9c97acd5 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-1.2.1-h3452ae3_0.tar.bz2#6d4bf6265d998b6c975c26a6a24062a2 +https://conda.anaconda.org/conda-forge/noarch/locket-0.2.0-py_2.tar.bz2#709e8671651c7ec3d1ad07800339ff1d +https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 +https://conda.anaconda.org/conda-forge/noarch/nose-1.3.7-py_1006.tar.bz2#382019d5f8e9362ef6f60a8d4e7bce8f +https://conda.anaconda.org/conda-forge/noarch/olefile-0.46-pyh9f0ad1d_1.tar.bz2#0b2e68acc8c78c8cc392b90983481f58 +https://conda.anaconda.org/conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2#076becd9e05608f8dc72757d5f3a91ff +https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.0.6-pyhd8ed1ab_0.tar.bz2#3087df8c636c5a00e694605c39ce4982 +https://conda.anaconda.org/conda-forge/noarch/pyshp-2.1.3-pyh44b312d_0.tar.bz2#2d1867b980785eb44b8122184d8b42a6 +https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.7-2_cp37m.tar.bz2#afff88bf9a7048da740c70aeb8cdbb82 +https://conda.anaconda.org/conda-forge/noarch/pytz-2021.3-pyhd8ed1ab_0.tar.bz2#7e4f811bff46a5a6a7e0094921389395 +https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 +https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e +https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-1.0.2-py_0.tar.bz2#20b2eaeaeea4ef9a9a0d99770620fd09 +https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-1.0.2-py_0.tar.bz2#68e01cac9d38d0e717cd5c87bc3d2cc9 +https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.0.0-pyhd8ed1ab_0.tar.bz2#77dad82eb9c8c1525ff7953e0756d708 +https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-py_0.tar.bz2#67cd9d9c0382d37479b4d306c369a2d4 +https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-1.0.3-py_0.tar.bz2#d01180388e6d1838c3e1ad029590aa7a +https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 +https://conda.anaconda.org/conda-forge/noarch/toolz-0.11.2-pyhd8ed1ab_0.tar.bz2#f348d1590550371edfac5ed3c1d44f7e +https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.0.1-pyha770c72_0.tar.bz2#1fc03816925d3cb7fdab9ab234e7fea7 +https://conda.anaconda.org/conda-forge/noarch/wheel-0.37.0-pyhd8ed1ab_1.tar.bz2#3aa2c3e25dd361b453d010388b9cdff1 +https://conda.anaconda.org/conda-forge/noarch/zipp-3.6.0-pyhd8ed1ab_0.tar.bz2#855e2c4622f5eb50a4f6f7167b9ba17a +https://conda.anaconda.org/conda-forge/linux-64/antlr-python-runtime-4.7.2-py37h89c1867_1003.tar.bz2#490366305378c8690b65c4bce9b9f6a4 +https://conda.anaconda.org/conda-forge/noarch/babel-2.9.1-pyh44b312d_0.tar.bz2#74136ed39bfea0832d338df1e58d013e +https://conda.anaconda.org/conda-forge/linux-64/cairo-1.16.0-h6cf1ce9_1008.tar.bz2#a43fb47d15e116f8be4be7e6b17ab59f +https://conda.anaconda.org/conda-forge/linux-64/certifi-2021.10.8-py37h89c1867_1.tar.bz2#48e8442b6097c7d4a0e3494c74ff9eeb +https://conda.anaconda.org/conda-forge/linux-64/cffi-1.15.0-py37h036bc23_0.tar.bz2#05ab26c7685bcb7dd8bc8752c121f823 +https://conda.anaconda.org/conda-forge/linux-64/curl-7.80.0-h2574ce0_0.tar.bz2#4d8fd67e5ab7e00fde8ad085464f43b7 +https://conda.anaconda.org/conda-forge/linux-64/cython-0.29.24-py37hcd2ae1e_1.tar.bz2#c32a8d5bea21a1a7074b005199a45ede +https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h48d8840_2.tar.bz2#eba672c69baf366fdedd1c6f702dbb81 +https://conda.anaconda.org/conda-forge/linux-64/docutils-0.17.1-py37h89c1867_1.tar.bz2#e0a3be74a594032b73f22762ba9941cc +https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.12.1-mpi_mpich_h9c45103_2.tar.bz2#39a57ab5ac7b7ea3342ba45d8c905a5e +https://conda.anaconda.org/conda-forge/linux-64/importlib-metadata-4.8.2-py37h89c1867_0.tar.bz2#a62f2bad6654c1a91e3241ca7979fe05 +https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.3.2-py37h2527ec5_1.tar.bz2#441ac4d93d0d57d21ea9dcac48cb5d0d +https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h6ad9fb6_0.tar.bz2#45142dc44fcd04934f9ad68ce205e54d +https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.0.1-py37h5e8e339_1.tar.bz2#6c7c14c95d4c435b66261639b64c7c51 +https://conda.anaconda.org/conda-forge/linux-64/mpi4py-3.1.3-py37h1e5cb63_0.tar.bz2#3d5ca9f081a7756df4f027776ff23b73 +https://conda.anaconda.org/conda-forge/linux-64/numpy-1.21.4-py37h31617e3_0.tar.bz2#332cef055880482fe23f6ac1e7ee6e57 +https://conda.anaconda.org/conda-forge/noarch/packaging-21.3-pyhd8ed1ab_0.tar.bz2#71f1ab2de48613876becddd496371c85 +https://conda.anaconda.org/conda-forge/noarch/partd-1.2.0-pyhd8ed1ab_0.tar.bz2#0c32f563d7f22e3a34c95cad8cc95651 +https://conda.anaconda.org/conda-forge/linux-64/pillow-6.2.2-py37h718be6c_0.tar.bz2#ecac4e308b87ff93d44ea5e56ab39084 +https://conda.anaconda.org/conda-forge/noarch/pockets-0.9.1-py_0.tar.bz2#1b52f0c42e8077e5a33e00fe72269364 +https://conda.anaconda.org/conda-forge/linux-64/proj-8.2.0-h277dcde_0.tar.bz2#7ba8c7a9bf1c2fedf4a6d6dc92839baf +https://conda.anaconda.org/conda-forge/linux-64/pyqt5-sip-4.19.18-py37hcd2ae1e_8.tar.bz2#ae12b17bbd5733cb8884b42dcc5c59f0 +https://conda.anaconda.org/conda-forge/linux-64/pysocks-1.7.1-py37h89c1867_4.tar.bz2#44df88d27e2891f90e3f06dcfcca0927 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2#dd999d1cc9f79e67dbb855c8924c7984 +https://conda.anaconda.org/conda-forge/linux-64/python-xxhash-2.0.2-py37h5e8e339_1.tar.bz2#c89489cddb9e53155e241e9aacd35e4b +https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0-py37h5e8e339_3.tar.bz2#7f167ecf4d4771ee33589e09479238e7 +https://conda.anaconda.org/conda-forge/linux-64/setuptools-59.4.0-py37h89c1867_0.tar.bz2#01f5271a7c204862ca564ee63b35548d +https://conda.anaconda.org/conda-forge/linux-64/tornado-6.1-py37h5e8e339_2.tar.bz2#ec86ae00c96dea5f2d810957a8fabc26 +https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-13.0.0.post2-py37h5e8e339_4.tar.bz2#6702ec56e12a0be3c70bf05991187bfd +https://conda.anaconda.org/conda-forge/linux-64/asv-0.4.2-py37hcd2ae1e_3.tar.bz2#0f076ca34a73c99253999aa65d787034 +https://conda.anaconda.org/conda-forge/linux-64/brotlipy-0.7.0-py37h5e8e339_1003.tar.bz2#4ad2e74470a3c08b0f6d59699f0d9a32 +https://conda.anaconda.org/conda-forge/linux-64/cftime-1.5.1.1-py37hb1e94ed_1.tar.bz2#1b5b81088bc7d7e0bef7de4ef4bd1221 +https://conda.anaconda.org/conda-forge/linux-64/cryptography-36.0.0-py37hf1a17b8_0.tar.bz2#2fd357ed549ff7f39a04f329938de4b3 +https://conda.anaconda.org/conda-forge/noarch/dask-core-2021.11.2-pyhd8ed1ab_0.tar.bz2#c3615fd90d2e5b11c5da6ea1f9a5730b +https://conda.anaconda.org/conda-forge/linux-64/editdistance-s-1.0.0-py37h2527ec5_2.tar.bz2#9aba6bcb02d12dbd2fead23b85720712 +https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.28.3-py37h5e8e339_0.tar.bz2#5b7840e065bcdbfc6aaa72d772fcd190 +https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-3.1.1-h83ec7ef_0.tar.bz2#ca8faaee04a83e3c4d6f708a35ac2ec3 +https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-4.8.2-hd8ed1ab_0.tar.bz2#4fcc9776fdf4c35c5a10662bbbd21a61 +https://conda.anaconda.org/conda-forge/noarch/jinja2-3.0.3-pyhd8ed1ab_0.tar.bz2#036d872c653780cb26e797e2e2f61b4c +https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.8.1-mpi_mpich_h319fa22_1.tar.bz2#7583fbaea3648f692c0c019254bc196c +https://conda.anaconda.org/conda-forge/linux-64/mo_pack-0.2.0-py37hb1e94ed_1006.tar.bz2#e06cf91c2624284413641be2cb8c3198 +https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.6.0-pyhd8ed1ab_0.tar.bz2#0941325bf48969e2b3b19d0951740950 +https://conda.anaconda.org/conda-forge/linux-64/pandas-1.3.4-py37he8f5f7f_1.tar.bz2#b8e4308f454bc3e9ceeab49eb7fe7405 +https://conda.anaconda.org/conda-forge/noarch/pip-21.3.1-pyhd8ed1ab_0.tar.bz2#e4fe2a9af78ff11f1aced7e62128c6a8 +https://conda.anaconda.org/conda-forge/noarch/pygments-2.10.0-pyhd8ed1ab_0.tar.bz2#32bcce837f1316f1c3208118b6c5e5fc +https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.2.1-py37hb589d83_5.tar.bz2#ea78cbba7d43ad17ec043a9ebdee3bf5 +https://conda.anaconda.org/conda-forge/linux-64/python-stratify-0.1.1-py37h6f94858_1004.tar.bz2#42b37830a63405589fef3d13db505e7d +https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.2.0-py37hb1e94ed_1.tar.bz2#3a94b25c520754b56cdfa7d865806524 +https://conda.anaconda.org/conda-forge/linux-64/qt-5.12.9-hda022c4_4.tar.bz2#afebab1f5049d66baaaec67d9ce893f0 +https://conda.anaconda.org/conda-forge/linux-64/scipy-1.7.3-py37hf2a6cf1_0.tar.bz2#129c613e1d0f09d9fd0473a0da6161a9 +https://conda.anaconda.org/conda-forge/linux-64/shapely-1.8.0-py37h9b0f7a3_4.tar.bz2#568474687cd6be5f834cb682637ac0de +https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-napoleon-0.7-py_0.tar.bz2#0bc25ff6f2e34af63ded59692df5f749 +https://conda.anaconda.org/conda-forge/linux-64/virtualenv-20.4.7-py37h89c1867_1.tar.bz2#cbe5a8c8ae88d1e73b4297a73d08408a +https://conda.anaconda.org/conda-forge/linux-64/cf-units-3.0.1-py37hb1e94ed_1.tar.bz2#63d03e033e18576d104474a76d56b6de +https://conda.anaconda.org/conda-forge/noarch/identify-2.3.7-pyhd8ed1ab_0.tar.bz2#ae1a5e834fbca62ee88ab55fb276be63 +https://conda.anaconda.org/conda-forge/noarch/imagehash-4.2.1-pyhd8ed1ab_0.tar.bz2#01cc8698b6e1a124dc4f585516c27643 +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.5.0-py37h1058ff1_0.tar.bz2#de800e20af9257645b36eac046b98080 +https://conda.anaconda.org/conda-forge/linux-64/netcdf-fortran-4.5.3-mpi_mpich_h1364a43_6.tar.bz2#9caa0cf923af3d037897c6d7f8ea57c0 +https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.5.8-nompi_py37hf784469_101.tar.bz2#5b05dc55e51be0696878e9a575c12f77 +https://conda.anaconda.org/conda-forge/linux-64/pango-1.48.10-h54213e6_2.tar.bz2#b7ed7c76c9360db1f91afba2e220007b +https://conda.anaconda.org/conda-forge/noarch/pyopenssl-21.0.0-pyhd8ed1ab_0.tar.bz2#8c49efecb7dca466e18b06015e8c88ce +https://conda.anaconda.org/conda-forge/linux-64/pyqt-impl-5.12.3-py37hac37412_8.tar.bz2#148f2e971a67831ed0691f63cd826468 +https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.20.1-py37h9a08e6e_5.tar.bz2#e44dc116f747b0a7bceaf1533acc6b48 +https://conda.anaconda.org/conda-forge/linux-64/esmf-8.2.0-mpi_mpich_h4975321_100.tar.bz2#56f5c650937b1667ad0a557a0dff3bc4 +https://conda.anaconda.org/conda-forge/linux-64/gtk2-2.24.33-h539f30e_1.tar.bz2#606777b4da3664d5c9415f5f165349fd +https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.52.4-hc3c00ef_0.tar.bz2#310ae1e113d6febc26fe3b44e3519757 +https://conda.anaconda.org/conda-forge/noarch/nc-time-axis-1.4.0-pyhd8ed1ab_0.tar.bz2#9113b4e4fa2fa4a7f129c71a6f319475 +https://conda.anaconda.org/conda-forge/linux-64/pre-commit-2.16.0-py37h89c1867_0.tar.bz2#43b270fe44130353e540037ad27da097 +https://conda.anaconda.org/conda-forge/linux-64/pyqtchart-5.12-py37he336c9b_8.tar.bz2#2fe25d82cb4e59191df561c40870ca6b +https://conda.anaconda.org/conda-forge/linux-64/pyqtwebengine-5.12.1-py37he336c9b_8.tar.bz2#0a67d477c0524897883ca0f86d6fb15c +https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.7-pyhd8ed1ab_0.tar.bz2#be75bab4820a56f77ba1a3fc9139c36a +https://conda.anaconda.org/conda-forge/linux-64/esmpy-8.2.0-mpi_mpich_py37h7352969_101.tar.bz2#64fd02e7a0cefe0b5c604fea03774c73 +https://conda.anaconda.org/conda-forge/linux-64/graphviz-2.49.3-h85b4f2f_0.tar.bz2#71062453180260ef85c861996b29bd6a +https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.12.3-py37h89c1867_8.tar.bz2#8038f9765a907fcf6fdfa6a9db71e371 +https://conda.anaconda.org/conda-forge/noarch/requests-2.26.0-pyhd8ed1ab_1.tar.bz2#358581cc782802270d77c454c73a811a +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.5.0-py37h89c1867_0.tar.bz2#dc2167a62be1ca5253727201368ddc89 +https://conda.anaconda.org/conda-forge/noarch/sphinx-4.3.1-pyh6c4a22f_0.tar.bz2#74d14a40bf09d1d8d788a8433f04e0d8 +https://conda.anaconda.org/conda-forge/noarch/sphinx-copybutton-0.4.0-pyhd8ed1ab_0.tar.bz2#80fd2cc25ad45911b4e42d5b91593e2f +https://conda.anaconda.org/conda-forge/noarch/sphinx-gallery-0.10.1-pyhd8ed1ab_0.tar.bz2#4918585fe5e5341740f7e63c61743efb +https://conda.anaconda.org/conda-forge/noarch/sphinx-panels-0.6.0-pyhd8ed1ab_0.tar.bz2#6eec6480601f5d15babf9c3b3987f34a +https://conda.anaconda.org/conda-forge/noarch/sphinx_rtd_theme-1.0.0-pyhd8ed1ab_0.tar.bz2#9f633f2f2869184e31acfeae95b24345 +https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.5-pyhd8ed1ab_1.tar.bz2#63d2f874f990fdcab47c822b608d6ade diff --git a/requirements/ci/py37.yml b/requirements/ci/py37.yml new file mode 100644 index 0000000000..8e4b9d9c10 --- /dev/null +++ b/requirements/ci/py37.yml @@ -0,0 +1,48 @@ +name: iris-dev + +channels: + - conda-forge + +dependencies: + - python =3.7 + +# Setup dependencies. + - setuptools >=40.8.0 + +# Core dependencies. + - cartopy >=0.20 + - cf-units >=3 + - cftime >=1.5 + - dask-core >=2 + - matplotlib + - netcdf4 + - numpy >=1.19 + - python-xxhash + - scipy + +# Optional dependencies. + - esmpy >=7.0 + - graphviz + - iris-sample-data >=2.4.0 + - mo_pack + - nc-time-axis >=1.3 + - pandas + - pip + - python-stratify + +# Test dependencies. + - asv + - filelock + - imagehash >=4.0 + - nose + - pillow <7 + - pre-commit + - requests + +# Documentation dependencies. + - sphinx + - sphinxcontrib-napoleon + - sphinx-copybutton + - sphinx-gallery + - sphinx-panels + - sphinx_rtd_theme diff --git a/requirements/ci/py38.yml b/requirements/ci/py38.yml index d3d7f9d0c2..d7ccb0b9d5 100644 --- a/requirements/ci/py38.yml +++ b/requirements/ci/py38.yml @@ -31,6 +31,7 @@ dependencies: - python-stratify # Test dependencies. + - asv - filelock - imagehash >=4.0 - nose diff --git a/setup.cfg b/setup.cfg index 1d3fb8b7c9..26516ee1af 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,7 +59,7 @@ packages = find: package_dir = =lib python_requires = - >=3.8 + >=3.7 zip_safe = False [options.packages.find] @@ -74,6 +74,7 @@ docs = sphinxcontrib-napoleon sphinx-panels test = + asv filelock imagehash>=4.0 nose diff --git a/tools/gen_helpers.py b/tools/gen_helpers.py new file mode 100644 index 0000000000..825c78139e --- /dev/null +++ b/tools/gen_helpers.py @@ -0,0 +1,36 @@ +# 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. + +from datetime import datetime +import os +import os.path + +HEADER = \ + '''# 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. +# +# DO NOT EDIT: AUTO-GENERATED''' + + +def absolute_path(path): + return os.path.abspath(os.path.join(os.path.dirname(__file__), path)) + + +def prep_module_file(module_path): + """ + prepare a module file, creating directory if needed and writing the + header into that file + + """ + module_path = absolute_path(module_path) + module_dir = os.path.dirname(module_path) + if not os.path.isdir(module_dir): + os.makedirs(module_dir) + with open(module_path, 'w') as module_file: + module_file.write(HEADER.format(datetime.utcnow().year)) diff --git a/tools/gen_stash_refs.py b/tools/gen_stash_refs.py new file mode 100644 index 0000000000..e614b52ab2 --- /dev/null +++ b/tools/gen_stash_refs.py @@ -0,0 +1,126 @@ +# 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. + +import json +import urllib +import urllib2 + +from iris.fileformats.pp import STASH + +import gen_helpers + + +HEADER = ''' +""" +Auto-generated from iris/tools/gen_stash_refs.py +Relates grid code and field code to the stash code. + +""" +''' + +CODE_PREAMBLE = ("from collections import namedtuple\n\n\n" + "Stash = namedtuple('Stash', " + "'grid_code field_code pseudo_level_type')\n\n\n") + + +def _value_from_xref(xref, name): + """Return the value for the key name from xref. + + Will return 0 if the key does not look like an integer. + """ + + result = xref.get(name) + try: + int(result) + except (ValueError, TypeError): + result = 0 + return result + + +def write_cross_reference_module(module_path, xrefs): + gen_helpers.prep_module_file(module_path) + with open(module_path, 'a') as module_file: + module_file.write(HEADER) + module_file.write(CODE_PREAMBLE) + module_file.write('STASH_TRANS = {\n') + for xref in xrefs: + stash = xref.get('stash') + try: + STASH.from_msi(stash.replace('"', '')) + except ValueError: + msg = ('stash code is not of a recognised' + '"m??s??i???" form: {}'.format(stash)) + print(msg) + grid = xref.get('grid') + if grid is not None: + try: + int(grid) + except ValueError: + msg = ('grid code retrieved from STASH lookup' + 'is not an integer: {}'.format(grid)) + print(msg) + else: + grid = 0 + + lbfc = _value_from_xref(xref, 'lbfcn') + pseudT = _value_from_xref(xref, 'pseudT') + + module_file.write( + ' "{}": Stash({}, {}, {}),\n'.format(stash, + grid, + lbfc, + pseudT)) + module_file.write('}\n') + + +def stash_grid_retrieve(): + """return a dictionary of stash codes and rel;ated information from + the Met Office Reference Registry + """ + baseurl = 'http://reference.metoffice.gov.uk/system/query?query=' + query = '''prefix rdf: +prefix rdfs: +prefix skos: + +SELECT ?stash ?grid ?lbfcn ?pseudT +WHERE { + ?stashcode rdf:type ; + skos:notation ?stash ; + ?gridcode . +OPTIONAL { ?gridcode skos:notation ?grid .} +OPTIONAL {?stashcode ?lbfc . + ?lbfc skos:notation ?lbfcn .} +OPTIONAL {?stashcode ?pseudT_id . + ?pseudT_id skos:notation ?pseudT . } +} +order by ?stash''' + + encquery = urllib.quote_plus(query) + out_format = '&output=json' + url = baseurl + encquery + out_format + + response = urllib2.urlopen(url) + stash = json.loads(response.read()) + + ## heads will be of the form [u'stash', u'grid', u'lbfcn', u'pseudT'] + ## as defined in the query string + heads = stash['head']['vars'] + + stashcodes = [] + + for result in stash['results']['bindings']: + res = {} + for head in heads: + if head in result: + res[head] = result[head]['value'] + stashcodes.append(res) + return stashcodes + + +if __name__ == '__main__': + xrefs = stash_grid_retrieve() + outfile = '../lib/iris/fileformats/_ff_cross_references.py' + write_cross_reference_module(outfile, xrefs) diff --git a/tools/gen_translations.py b/tools/gen_translations.py new file mode 100644 index 0000000000..5ac0dc02ba --- /dev/null +++ b/tools/gen_translations.py @@ -0,0 +1,216 @@ +# 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. +""" +Processing of metarelate metOcean content to provide Iris encodings of +metOcean mapping translations. + +""" + +from datetime import datetime +import os.path +import requests +import sys + +import metarelate +from metarelate.fuseki import FusekiServer + +from translator import (FORMAT_URIS, FieldcodeCFMappings, StashCFNameMappings, + StashCFHeightConstraintMappings, + CFFieldcodeMappings, + GRIB1LocalParamCFConstrainedMappings, + GRIB1LocalParamCFMappings, GRIB2ParamCFMappings, + CFConstrainedGRIB1LocalParamMappings, + CFGRIB2ParamMappings, CFGRIB1LocalParamMappings) + +HEADER = """# Copyright {name} contributors +# +# This file is part of {name} and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +# +# DO NOT EDIT: AUTO-GENERATED +# Created on {datestamp} from +# http://www.metarelate.net/metOcean +# at commit {git_sha} +# https://github.com/metarelate/metOcean/commit/{git_sha} +{doc_string} + + +from collections import namedtuple + + +CFName = namedtuple('CFName', 'standard_name long_name units') +""" + +HEADER_GRIB = """ +DimensionCoordinate = namedtuple('DimensionCoordinate', + 'standard_name units points') + +G1LocalParam = namedtuple('G1LocalParam', 'edition t2version centre iParam') +G2Param = namedtuple('G2Param', 'edition discipline category number') +""" + +DOC_STRING_GRIB = r'''""" +Provides GRIB/CF phenomenon translations. + +"""''' + +DOC_STRING_UM = r'''""" +Provides UM/CF phenomenon translations. + +"""''' + +YEAR = datetime.utcnow().year + +def _retrieve_mappings(fuseki, source, target): + """ + Interrogate the metarelate triple store for all + phenomenon translation mappings from the source + scheme to the target scheme. + + Args: + * fuseki: + The :class:`metrelate.fuseki.FusekiServer` instance. + * source: + The source metarelate metadata type for the mapping. + * target: + The target metarelate metadata type for the mapping. + + Return: + The sequence of :class:`metarelate.Mapping` + instances. + + """ + suri = 'http://www.metarelate.net/sparql/metOcean' + msg = 'Retrieving {!r} to {!r} mappings ...' + print(msg.format(source, target)) + return fuseki.retrieve_mappings(source, target, service=suri) + + +def build_um_cf_map(fuseki, now, git_sha, base_dir): + """ + Encode the UM/CF phenomenon translation mappings + within the specified file. + + Args: + * fuseki: + The :class:`metarelate.fuseki.FusekiServer` instance. + * now: + Time stamp to write into the file + * git_sha: + The git SHA1 of the metarelate commit + * base_dir: + The root directory of the Iris source. + + """ + filename = os.path.join(base_dir, 'lib', 'iris', 'fileformats', + 'um_cf_map.py') + + # Create the base directory. + if not os.path.exists(os.path.dirname(filename)): + os.makedirs(os.path.dirname(filename)) + + # Create the file to contain UM/CF translations. + with open(filename, 'w') as fh: + fh.write(HEADER.format(year=YEAR, doc_string=DOC_STRING_UM, + datestamp=now, git_sha=git_sha, name='Iris')) + fh.write('\n') + + # Encode the relevant UM to CF translations. + maps = _retrieve_mappings(fuseki, FORMAT_URIS['umf'], + FORMAT_URIS['cff']) + # create the collections, then call lines on each one + # for thread safety during lines and encode + fccf = FieldcodeCFMappings(maps) + stcf = StashCFNameMappings(maps) + stcfhcon = StashCFHeightConstraintMappings(maps) + fh.writelines(fccf.lines(fuseki)) + fh.writelines(stcf.lines(fuseki)) + fh.writelines(stcfhcon.lines(fuseki)) + + # Encode the relevant CF to UM translations. + maps = _retrieve_mappings(fuseki, FORMAT_URIS['cff'], + FORMAT_URIS['umf']) + # create the collections, then call lines on each one + # for thread safety during lines and encode + cffc = CFFieldcodeMappings(maps) + fh.writelines(cffc.lines(fuseki)) + + +def build_grib_cf_map(fuseki, now, git_sha, base_dir): + """ + Encode the GRIB/CF phenomenon translation mappings + within the specified file. + + Args: + * fuseki: + The :class:`metarelate.fuseki.FusekiServer` instance. + * now: + Time stamp to write into the file + * git_sha: + The git SHA1 of the metarelate commit + * base_dir: + The root directory of the Iris source. + + """ + filename = os.path.join(base_dir, 'lib', 'iris', 'fileformats', + 'grib', '_grib_cf_map.py') + if not os.path.exists(os.path.dirname(filename)): + os.makedirs(os.path.dirname(filename)) + + # Create the file to contain GRIB/CF translations. + with open(filename, 'w') as fh: + fh.write(HEADER.format(year=YEAR, doc_string=DOC_STRING_GRIB, + datestamp=now, git_sha=git_sha, + name='iris-grib')) + fh.write(HEADER_GRIB) + fh.write('\n') + + # Encode the relevant GRIB to CF translations. + maps = _retrieve_mappings(fuseki, FORMAT_URIS['gribm'], + FORMAT_URIS['cff']) + # create the collections, then call lines on each one + # for thread safety during lines and encode + g1cfc = GRIB1LocalParamCFConstrainedMappings(maps) + g1c = GRIB1LocalParamCFMappings(maps) + g2c = GRIB2ParamCFMappings(maps) + fh.writelines(g1cfc.lines(fuseki)) + fh.writelines(g1c.lines(fuseki)) + fh.writelines(g2c.lines(fuseki)) + + # Encode the relevant CF to GRIB translations. + maps = _retrieve_mappings(fuseki, FORMAT_URIS['cff'], + FORMAT_URIS['gribm']) + # create the collections, then call lines on each one + # for thread safety during lines and encode + cfcg1 = CFConstrainedGRIB1LocalParamMappings(maps) + cg1 = CFGRIB1LocalParamMappings(maps) + cg2 = CFGRIB2ParamMappings(maps) + fh.writelines(cfcg1.lines(fuseki)) + fh.writelines(cg1.lines(fuseki)) + fh.writelines(cg2.lines(fuseki)) + + +def main(): + # Protect metarelate resource from 1.0 emergent bug + if not float(metarelate.__version__) >= 1.1: + raise ValueError("Please ensure that Metarelate Version is >= 1.1") + now = datetime.utcnow().strftime('%d %B %Y %H:%m') + git_sha = requests.get('http://www.metarelate.net/metOcean/latest_sha').text + gen_path = os.path.abspath(sys.modules['__main__'].__file__) + iris_path = os.path.dirname(os.path.dirname(gen_path)) + with FusekiServer() as fuseki: + build_um_cf_map(fuseki, now, git_sha, iris_path) + build_grib_cf_map(fuseki, now, git_sha, iris_path) + + if (git_sha != + requests.get('http://www.metarelate.net/metOcean/latest_sha').text): + raise ValueError('The metarelate translation store has altered during' + 'your retrieval, the results may not be stable.\n' + 'Please rerun your retrieval.') + +if __name__ == '__main__': + main() diff --git a/tools/generate_std_names.py b/tools/generate_std_names.py index 08bacbe1e0..95dcce8171 100644 --- a/tools/generate_std_names.py +++ b/tools/generate_std_names.py @@ -13,9 +13,7 @@ By default, Iris will use the source XML file: etc/cf-standard-name-table.xml as obtained from: - http://cfconventions.org/standard-names.html - E.G. http://cfconventions.org/Data/cf-standard-names/78/src/cf-standard-name-table.xml - - N.B. no fixed 'latest' url is provided. + http://cf-pcmdi.llnl.gov/documents/cf-standard-names """ diff --git a/tools/translator/__init__.py b/tools/translator/__init__.py new file mode 100644 index 0000000000..a83fee4edd --- /dev/null +++ b/tools/translator/__init__.py @@ -0,0 +1,1116 @@ +# 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. +""" +Provides the framework to support the encoding of metarelate mapping +translations. + +""" + +from abc import ABCMeta, abstractmethod +from collections import deque, namedtuple +import copy +from queue import Queue +import re +from threading import Thread +import warnings + +from metarelate.fuseki import FusekiServer, WorkerThread, MAXTHREADS +import metarelate + +# known format identifier URIs +FORMAT_URIS = {'cff': '', + 'gribm': '', + 'umf': ''} + +CFName = namedtuple('CFName', 'standard_name long_name units') +DimensionCoordinate = namedtuple('DimensionCoordinate', + 'standard_name units points') +G1LocalParam = namedtuple('G1LocalParam', 'edition t2version centre iParam') +G2Param = namedtuple('G2Param', 'edition discipline category number') + + +class MappingEncodeWorker(WorkerThread): + """Worker thread class for handling EncodableMap instances""" + def dowork(self, resource): + resource.encode(self.fuseki_process) + + +class EncodableMap: + """ + A metarelate mapping able to encode itself as a string for use in Iris, + as defined by a translator Mappings subclass + + """ + def __init__(self, mapping, sourcemsg, targetmsg, sourceid, targetid): + """ + Args: + * mapping: + A :class:`metarelate.Mapping` instance representing a translation. + * sourcemsg: + The code snippet message for the source of the translation for + formatting + * targetmsg: + The code snippet message for the target of the translation for + formatting + * sourceid: + A dictionary of required key:value pairs required by the sourcemsg + * targetid: + A dictionary of required key:value pairs required by the targetmsg + + """ + self.mapping = mapping + self.sourcemsg = sourcemsg + self.targetmsg = targetmsg + self.sourceid = sourceid + self.targetid = targetid + self.encoding = None + + def encode(self, fuseki_process): + """ + Return a string of the Python source code required to represent an + entry in a dictionary mapping source to target. + + Args: + * fuseki_process: + A :class:`metarelate.fuseki.FusekiServer` instance. + + """ + sids, tids = self.mapping.get_identifiers(fuseki_process) + self.sourceid.update(sids) + self.targetid.update(tids) + self.encoding = '{}: {}'.format(self.sourcemsg.format(**self.sourceid), + self.targetmsg.format(**self.targetid)) + + +class Mappings(metaclass=ABCMeta): + """ + Abstract base class to support the encoding of specific metarelate + mapping translations. + + """ + + def __init__(self, mappings): + """ + Filter the given sequence of mappings for those member + :class:`metarelate.Mapping` translations containing a source + :class:`metarelate.Component` with a matching + :attribute:`Mapping.source_scheme` and a target + :class:`metarelate.Component` with a matching + :attribute:`Mapping.target_scheme`. + + Also see :method:`Mapping.valid_mapping` for further matching + criterion for candidate metarelate mapping translations. + + Args: + * mappings: + Iterator of :class:`metarelate.Mapping` instances. + + """ + temp = [] + # Filter the mappings for the required type of translations. + for mapping in mappings: + source = mapping.source + target = mapping.target + sourcemsg, targetmsg = self.msg_strings() + sourceid, targetid = self.get_initial_id_nones() + if source.com_type == self.source_scheme and \ + target.com_type == self.target_scheme and \ + self.valid_mapping(mapping): + temp.append(EncodableMap(mapping, sourcemsg, targetmsg, + sourceid, targetid)) + self.mappings = temp + if len(self) == 0: + msg = '{!r} contains no mappings.' + warnings.warn(msg.format(self.__class__.__name__)) + + def _sort_lines(self, payload): + """ + Return the payload, unsorted. + + """ + return payload + + def lines(self, fuseki_process): + """ + Provides an iterator generating the encoded string representation + of each member of this metarelate mapping translation. + + Returns: + An iterator of string. + + """ + msg = '\tGenerating phenomenon translation {!r}.' + print(msg.format(self.mapping_name)) + lines = ['\n%s = {\n' % self.mapping_name] + # Retrieve encodings for the collection of mapping instances. + # Retrieval is threaded as it is heavily bound by resource resolution + # over http. + # Queue for metarelate mapping instances + mapenc_queue = Queue() + for mapping in self.mappings: + mapenc_queue.put(mapping) + # deque to contain the results of the jobs processed from the queue + mapencs = deque() + # run worker threads + for i in range(MAXTHREADS): + MappingEncodeWorker(mapenc_queue, mapencs, fuseki_process).start() + # block progress until the queue is empty + mapenc_queue.join() + # end of threaded retrieval process. + + # now sort the payload + payload = [mapenc.encoding for mapenc in mapencs] + payload.sort(key=self._key) + lines.extend(payload) + lines.append(' }\n') + return iter(lines) + + def __len__(self): + return len(self.mappings) + + def _key(self, line): + """Method to provide the sort key of the mappings order.""" + return line + + @property + @abstractmethod + def mapping_name(self): + """ + Abstract property that specifies the name of the dictionary + to contain the encoding of this metarelate mapping translation. + + """ + + @property + @abstractmethod + def source_scheme(self): + """ + Abstract property that specifies the name of the scheme for + the source :class:`metarelate.Component` defining this metarelate + mapping translation. + + """ + + @property + @abstractmethod + def target_scheme(self): + """ + Abstract property that specifies the name of the scheme for + the target :class:`metarelate.Component` defining this metarelate + mapping translation. + + """ + + @abstractmethod + def valid_mapping(self, mapping): + """ + Abstract method that determines whether the provided + :class:`metarelate.Mapping` is a translation from the required + source :class:`metarelate.Component` to the required target + :class:`metarelate.Component`. + + """ + + def get_initial_id_nones(self): + """ + Return the identifier items which may not exist, in the translation + database, and are needed for a msg_string. These must exist, even + even if not written from the database. + + Returns two dictionaries to use as the start point for + population from the database. + + """ + sourceid = {} + targetid = {} + return sourceid, targetid + + def is_cf(self, comp): + """ + Determines whether the provided component from a mapping + represents a simple CF component of the given kind. + + Args: + * component: + A :class:`metarelate.Component` or + :class:`metarelate.Component` instance. + + Returns: + Boolean. + + """ + kind = FORMAT_URIS['cff'] + result = False + result = hasattr(comp, 'com_type') and \ + comp.com_type == kind and \ + hasattr(comp, 'units') and \ + len(comp) in [1, 2] + return result + + def is_cf_constrained(self, comp): + """ + Determines whether the provided component from a mapping + represents a compound CF component for a phenomenon and + one, single valued dimension coordinate. + + Args: + * component: + A :class:`metarelate.Component` instance. + + Returns: + Boolean. + + """ + ftype = FORMAT_URIS['cff'] + result = False + cffield = hasattr(comp, 'com_type') and comp.com_type == ftype and \ + hasattr(comp, 'units') and (hasattr(comp, 'standard_name') or + hasattr(comp, 'long_name')) + dimcoord = hasattr(comp, 'dim_coord') and \ + isinstance(comp.dim_coord, metarelate.ComponentProperty) and \ + comp.dim_coord.component.com_type.notation == 'DimCoord' + result = cffield and dimcoord + return result + + def is_cf_height_constrained(self, comp): + item_sn = metarelate.Item((''), + 'standard_name') + item_h = metarelate.Item((''), + 'height') + snprop = metarelate.StatementProperty(item_sn, item_h) + item_u = metarelate.Item((''), + 'units') + uprop = metarelate.StatementProperty(item_u, + metarelate.Item('"m"', 'm')) + pts_pred = metarelate.Item((''), + 'points') + result = False + if self.is_cf_constrained(comp): + props = comp.dim_coord.component.properties + if len(props) == 3: + if snprop in props and uprop in props: + preds = [prop.predicate for prop in props] + if pts_pred in preds: + result = True + return result + + def is_fieldcode(self, component): + """ + Determines whether the provided concept from a mapping + represents a simple UM concept for a field-code. + + Args: + * concept: + A :class:`metarelate.Component` instance. + + Returns: + Boolean. + + """ + result = False + result = hasattr(component, 'lbfc') and len(component) == 1 + return result + + def is_grib1_local_param(self, component): + """ + Determines whether the provided component from a mapping + represents a simple GRIB edition 1 component for a local + parameter. + + Args: + * component: + A :class:`metarelate.Component` instance. + + Returns: + Boolean. + + """ + result = len(component) == 1 and hasattr(component, 'grib1_parameter') + return result + + def is_grib2_param(self, component): + """ + Determines whether the provided component from a mapping + represents a simple GRIB edition 2 component for a parameter. + + Args: + * component: + A :class:`metarelate.Component` instance. + + Returns: + Boolean. + + """ + + result = len(component) == 1 and hasattr(component, 'grib2_parameter') + return result + + def is_stash(self, component): + """ + Determines whether the provided concept for a mapping + represents a simple UM concept for a stash-code. + + Args: + * concept: + A :class:`metarelate.Component` instance. + + Returns: + Boolean. + + """ + result = False + result = hasattr(component, 'stash') and len(component) == 1 + return result + + +def _cfn(line): + """ + Helper function to parse dictionary lines using the CFName named tuple. + Matches to the line ' CFName({standard_name}, {long_name}, {units}:*) + giving access to these named parts + + """ + match = re.match('^ CFName\((.+), (.+), (.+)\):.+,', line) + if match is None: + raise ValueError('encoding not sortable') + standard_name, long_name, units = match.groups() + if standard_name == 'None': + standard_name = None + if long_name == 'None': + long_name = None + return [standard_name, long_name, units] + + +class CFFieldcodeMappings(Mappings): + """ + Represents a container for CF phenomenon to UM field-code metarelate + mapping translations. + + Encoding support is provided to generate the Python dictionary source + code representation of these mappings from CF standard name, long name, + and units to UM field-code. + + """ + def _key(self, line): + """Provides the sort key of the mappings order.""" + return _cfn(line) + + def msg_strings(self): + return (' CFName({standard_name!r}, {long_name!r}, ' + '{units!r})', + '{lbfc},\n') + + def get_initial_id_nones(self): + sourceid = {'standard_name': None, 'long_name': None} + targetid = {} + return sourceid, targetid + + @property + def mapping_name(self): + """ + Property that specifies the name of the dictionary to contain the + encoding of this metarelate mapping translation. + + """ + return 'CF_TO_LBFC' + + @property + def source_scheme(self): + """ + Property that specifies the name of the scheme for the source + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['cff'] + + @property + def target_scheme(self): + """ + Property that specifies the name of the scheme for the target + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['umf'] + + def valid_mapping(self, mapping): + """ + Determine whether the provided :class:`metarelate.Mapping` represents a + CF to UM field-code translation. + + Args: + * mapping: + A :class:`metarelate.Mapping` instance. + + Returns: + Boolean. + + """ + return self.is_cf(mapping.source) and self.is_fieldcode(mapping.target) + + +class FieldcodeCFMappings(Mappings): + """ + Represents a container for UM field-code to CF phenomenon metarelate + mapping translations. + + Encoding support is provided to generate the Python dictionary source + code representation of these mappings from UM field-code to + CF standard name, long name, and units. + + """ + def _key(self, line): + """Provides the sort key of the mappings order.""" + return int(line.split(':')[0].strip()) + + def msg_strings(self): + return (' {lbfc}', + 'CFName({standard_name!r}, {long_name!r}, {units!r}),\n') + + def get_initial_id_nones(self): + sourceid = {} + targetid = {'standard_name': None, 'long_name': None} + return sourceid, targetid + + @property + def mapping_name(self): + """ + Property that specifies the name of the dictionary to contain the + encoding of this metarelate mapping translation. + + """ + return 'LBFC_TO_CF' + + @property + def source_scheme(self): + """ + Property that specifies the name of the scheme for the source + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['umf'] + + @property + def target_scheme(self): + """ + Property that specifies the name of the scheme for the target + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['cff'] + + def valid_mapping(self, mapping): + """ + Determine whether the provided :class:`metarelate.Mapping` represents a + UM field-code to CF translation. + + Args: + * mapping: + A :class:`metarelate.Mapping` instance. + + Returns: + Boolean. + + """ + return self.is_fieldcode(mapping.source) and self.is_cf(mapping.target) + + +class StashCFNameMappings(Mappings): + """ + Represents a container for UM stash-code to CF phenomenon metarelate + mapping translations. + + Encoding support is provided to generate the Python dictionary source + code representation of these mappings from UM stash-code to CF + standard name, long name, and units. + + """ + def _key(self, line): + """Provides the sort key of the mappings order.""" + return line.split(':')[0].strip() + + def msg_strings(self): + return(' {stash!r}', + 'CFName({standard_name!r}, ' + '{long_name!r}, {units!r}),\n') + + def get_initial_id_nones(self): + sourceid = {} + targetid = {'standard_name': None, 'long_name': None} + return sourceid, targetid + + @property + def mapping_name(self): + """ + Property that specifies the name of the dictionary to contain the + encoding of this metarelate mapping translation. + + """ + return 'STASH_TO_CF' + + @property + def source_scheme(self): + """ + Property that specifies the name of the scheme for the source + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['umf'] + + @property + def target_scheme(self): + """ + Property that specifies the name of the scheme for the target + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['cff'] + + def valid_mapping(self, mapping): + """ + Determine whether the provided :class:`metarelate.Mapping` represents a + UM stash-code to CF translation. + + Args: + * mapping: + A :class:`metarelate.Mapping` instance. + + Returns: + Boolean. + + """ + return (self.is_stash(mapping.source) and + (self.is_cf(mapping.target) or + self.is_cf_constrained(mapping.target))) + + +class StashCFHeightConstraintMappings(Mappings): + """ + Represents a container for UM stash-code to CF phenomenon metarelate + mapping translations where a singular height constraint is defined by + the STASH code. + + Encoding support is provided to generate the Python dictionary source + code representation of these mappings from UM stash-code to CF + standard name, long name, and units. + + """ + def _key(self, line): + """Provides the sort key of the mappings order.""" + return line.split(':')[0].strip() + + def msg_strings(self): + return(' {stash!r}', + '{dim_coord[points]},\n') + + def get_initial_id_nones(self): + sourceid = {} + targetid = {} + return sourceid, targetid + + @property + def mapping_name(self): + """ + Property that specifies the name of the dictionary to contain the + encoding of this metarelate mapping translation. + + """ + return 'STASHCODE_IMPLIED_HEIGHTS' + + @property + def source_scheme(self): + """ + Property that specifies the name of the scheme for the source + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['umf'] + + @property + def target_scheme(self): + """ + Property that specifies the name of the scheme for the target + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['cff'] + + def valid_mapping(self, mapping): + """ + Determine whether the provided :class:`metarelate.Mapping` represents a + UM stash-code to CF translation. + + Args: + * mapping: + A :class:`metarelate.Mapping` instance. + + Returns: + Boolean. + + """ + return (self.is_stash(mapping.source) and + self.is_cf_height_constrained(mapping.target)) + + +class GRIB1LocalParamCFMappings(Mappings): + """ + Represents a container for GRIB (edition 1) local parameter to + CF phenomenon metarelate mapping translations. + + Encoding support is provided to generate the Python dictionary source + code representation of these mappings from GRIB1 edition, table II version, + centre and indicator of parameter to CF standard name, long name and units. + + """ + def _key(self, line): + """Provides the sort key of the mappings order.""" + matchstr = ('^ G1LocalParam\(([0-9]+), ([0-9]+), ' + '([0-9]+), ([0-9]+)\):.*') + match = re.match(matchstr, line) + if match is None: + raise ValueError('encoding not sortable') + return [int(i) for i in match.groups()] + + def msg_strings(self): + return (' G1LocalParam({editionNumber}, {table2version}, ' + '{centre}, {indicatorOfParameter})', + 'CFName({standard_name!r}, ' + '{long_name!r}, {units!r}),\n') + + def get_initial_id_nones(self): + sourceid = {} + targetid = {'standard_name': None, 'long_name': None} + return sourceid, targetid + + @property + def mapping_name(self): + """ + Property that specifies the name of the dictionary to contain the + encoding of this metarelate mapping translation. + + """ + return 'GRIB1_LOCAL_TO_CF' + + @property + def source_scheme(self): + """ + Property that specifies the name of the scheme for the source + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['gribm'] + + @property + def target_scheme(self): + """ + Property that specifies the name of the scheme for the target + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['cff'] + + def valid_mapping(self, mapping): + """ + Determine whether the provided :class:`metarelate.Mapping` represents a + GRIB1 local parameter to CF phenomenon translation. + + Args: + * mapping: + A :class:`metarelate.Mapping` instance. + + Returns: + Boolean. + + """ + return self.is_grib1_local_param(mapping.source) and \ + self.is_cf(mapping.target) + + +class CFGRIB1LocalParamMappings(Mappings): + """ + Represents a container for CF phenomenon to GRIB (edition 1) local + parameter metarelate mapping translations. + + Encoding support is provided to generate the Python dictionary source + code representation of these mappings from CF standard name, long name + and units to GRIB1 edition, table II version, centre and indicator of + parameter. + + """ + def _key(self, line): + """Provides the sort key of the mappings order.""" + return _cfn(line) + + def msg_strings(self): + return (' CFName({standard_name!r}, {long_name!r}, ' + '{units!r})', + 'G1LocalParam({editionNumber}, {table2version}, ' + '{centre}, {indicatorOfParameter}),\n') + + def get_initial_id_nones(self): + sourceid = {'standard_name': None, 'long_name': None} + targetid = {} + return sourceid, targetid + + @property + def mapping_name(self): + """ + Property that specifies the name of the dictionary to contain the + encoding of this metarelate mapping translation. + + """ + return 'CF_TO_GRIB1_LOCAL' + + @property + def source_scheme(self): + """ + Property that specifies the name of the scheme for the source + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['cff'] + + @property + def target_scheme(self): + """ + Property that specifies the name of the scheme for the target + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['gribm'] + + def valid_mapping(self, mapping): + """ + Determine whether the provided :class:`metarelate.Mapping` represents a + CF phenomenon to GRIB1 local parameter translation. + + Args: + * mapping: + A :class:`metarelate.Mapping` instance. + + Returns: + Boolean. + + """ + return self.is_cf(mapping.source) and \ + self.is_grib1_local_param(mapping.target) + + +class GRIB1LocalParamCFConstrainedMappings(Mappings): + """ + Represents a container for GRIB (edition 1) local parameter to + CF phenomenon and dimension coordinate constraint metarelate mapping + translations. + + Encoding support is provided to generate the Python dictionary source + code representation of these mappings from GRIB1 edition, table II version, + centre and indicator of parameter to CF phenomenon standard name, long name + and units, and CF dimension coordinate standard name, units and points. + + """ + def _key(self, line): + """Provides the sort key of the mappings order.""" + return line.split(':')[0].strip() + + def msg_strings(self): + return (' G1LocalParam({editionNumber}, {table2version}, ' + '{centre}, {indicatorOfParameter})', + '(CFName({standard_name!r}, ' + '{long_name!r}, {units!r}), ' + 'DimensionCoordinate({dim_coord[standard_name]!r}, ' + '{dim_coord[units]!r}, {dim_coord[points]})),\n') + + def get_initial_id_nones(self): + sourceid = {} + targetid = {'standard_name': None, 'long_name': None} + return sourceid, targetid + + @property + def mapping_name(self): + """ + Property that specifies the name of the dictionary to contain the + encoding of this metarelate mapping translation. + + """ + return 'GRIB1_LOCAL_TO_CF_CONSTRAINED' + + @property + def source_scheme(self): + """ + Property that specifies the name of the scheme for the source + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['gribm'] + + @property + def target_scheme(self): + """ + Property that specifies the name of the scheme for the target + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['cff'] + + def valid_mapping(self, mapping): + """ + Determine whether the provided :class:`metarelate.Mapping` represents a + GRIB1 local parameter to CF phenomenon and dimension coordinate + translation. + + Args: + * mapping: + A :class:`metarelate.Mapping` instance. + + Returns: + Boolean. + + """ + return self.is_grib1_local_param(mapping.source) and \ + self.is_cf_constrained(mapping.target) + + +class CFConstrainedGRIB1LocalParamMappings(Mappings): + """ + Represents a container for CF phenomenon and dimension coordinate + constraint to GRIB (edition 1) local parameter metarelate mapping + translations. + + Encoding support is provided to generate the Python dictionary source + code representation of these mappings from CF phenomenon standard name, + long name and units, and CF dimension coordinate standard name, units and + points to GRIB1 edition, table II version, centre and indicator of + parameter. + + """ + def _key(self, line): + """Provides the sort key of the mappings order.""" + return line.split(':')[0].strip() + + def msg_strings(self): + return (' (CFName({standard_name!r}, ' + '{long_name!r}, {units!r}), ' + 'DimensionCoordinate({dim_coord[standard_name]!r}, ' + '{dim_coord[units]!r}, {dim_coord[points]}))', + 'G1LocalParam({editionNumber}, {table2version}, ' + '{centre}, {indicatorOfParameter}),\n') + + def get_initial_id_nones(self): + sourceid = {'standard_name': None, 'long_name': None} + targetid = {} + return sourceid, targetid + + @property + def mapping_name(self): + """ + Property that specifies the name of the dictionary to contain the + encoding of this metarelate mapping translation. + + """ + return 'CF_CONSTRAINED_TO_GRIB1_LOCAL' + + @property + def source_scheme(self): + """ + Property that specifies the name of the scheme for the source + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['cff'] + + @property + def target_scheme(self): + """ + Property that specifies the name of the scheme for the target + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['gribm'] + + def valid_mapping(self, mapping): + """ + Determine whether the provided :class:`metarelate.Mapping` represents a + CF phenomenon and dimension coordinate to GRIB1 local parameter + translation. + + Args: + * mapping: + A :class:`metarelate.Mapping` instance. + + Returns: + Boolean. + + """ + return self.is_cf_constrained(mapping.source) and \ + self.is_grib1_local_param(mapping.target) + + +class GRIB2ParamCFMappings(Mappings): + """ + Represents a container for GRIB (edition 2) parameter to CF phenomenon + metarelate mapping translations. + + Encoding support is provided to generate the Python dictionary source + code representation of these mappings from GRIB2 edition, discipline, + parameter category and indicator of parameter to CF standard name, + long name and units. + + """ + def _key(self, line): + """Provides the sort key of the mappings order.""" + matchstr = ('^ G2Param\(([0-9]+), ([0-9]+), ([0-9]+), ' + '([0-9]+)\):.*') + match = re.match(matchstr, line) + if match is None: + raise ValueError('encoding not sortable') + return [int(i) for i in match.groups()] + + def msg_strings(self): + return (' G2Param({editionNumber}, {discipline}, ' + '{parameterCategory}, {parameterNumber})', + 'CFName({standard_name!r}, {long_name!r}, ' + '{units!r}),\n') + + def get_initial_id_nones(self): + sourceid = {} + targetid = {'standard_name': None, 'long_name': None} + return sourceid, targetid + + @property + def mapping_name(self): + """ + Property that specifies the name of the dictionary to contain the + encoding of this metarelate mapping translation. + + """ + return 'GRIB2_TO_CF' + + @property + def source_scheme(self): + """ + Property that specifies the name of the scheme for the source + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['gribm'] + + @property + def target_scheme(self): + """ + Property that specifies the name of the scheme for the target + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['cff'] + + def valid_mapping(self, mapping): + """ + Determine whether the provided :class:`metarelate.Mapping` represents a + GRIB2 parameter to CF phenomenon translation. + + Args: + * mapping: + A :class:`metarelate.Mapping` instance. + + Returns: + Boolean. + + """ + return self.is_grib2_param(mapping.source) and \ + self.is_cf(mapping.target) + + +class CFGRIB2ParamMappings(Mappings): + """ + Represents a container for CF phenomenon to GRIB (edition 2) parameter + metarelate mapping translations. + + Encoding support is provided to generate the Python dictionary source + code representation of these mappings from CF standard name, long name + and units to GRIB2 edition, discipline, parameter category and indicator + of parameter. + + """ + def _key(self, line): + """Provides the sort key of the mappings order.""" + return _cfn(line) + + def msg_strings(self): + return (' CFName({standard_name!r}, {long_name!r}, ' + '{units!r})', + 'G2Param({editionNumber}, {discipline}, ' + '{parameterCategory}, {parameterNumber}),\n') + + def get_initial_id_nones(self): + sourceid = {'standard_name': None, 'long_name': None} + targetid = {} + return sourceid, targetid + + @property + def mapping_name(self): + """ + Property that specifies the name of the dictionary to contain the + encoding of this metarelate mapping translation. + + """ + return 'CF_TO_GRIB2' + + @property + def source_scheme(self): + """ + Property that specifies the name of the scheme for the source + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['cff'] + + @property + def target_scheme(self): + """ + Property that specifies the name of the scheme for the target + :class:`metarelate.Component` defining this metarelate mapping + translation. + + """ + return FORMAT_URIS['gribm'] + + def valid_mapping(self, mapping): + """ + Determine whether the provided :class:`metarelate.Mapping` represents a + CF phenomenon to GRIB2 parameter translation. + + Args: + * mapping: + A :class:`metarelate.Mapping` instance. + + Returns: + Boolean. + + """ + return self.is_cf(mapping.source) and \ + self.is_grib2_param(mapping.target) From 56ae140c905bf718f95c2db762f77772841dda28 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 10 Feb 2022 16:37:06 +0000 Subject: [PATCH 15/22] Revert "Revert "Merge remote-tracking branch 'upstream/main' into all_benchmarks"" This reverts commit fc9a1cb2d346ec0682148e2ae33524aea310a4cd. --- .cirrus.yml | 6 - .github/workflows/refresh-lockfiles.yml | 2 +- .pre-commit-config.yaml | 6 +- .readthedocs.yml | 22 +- benchmarks/benchmarks/plot.py | 2 +- docs/gallery_code/README.rst | 2 +- .../meteorology/plot_wind_barbs.py | 4 +- .../meteorology/plot_wind_speed.py | 2 +- docs/src/_templates/imagehash.html | 15 + docs/src/common_links.inc | 1 + docs/src/conf.py | 10 +- .../contributing_documentation.rst | 176 +- .../contributing_documentation_easy.rst | 102 + .../contributing_documentation_full.rst | 173 ++ .../contributing_graphics_tests.rst | 2 + .../contributing_testing_index.rst | 1 + .../documenting/whats_new_contributions.rst | 21 +- docs/src/developers_guide/edit_button.png | Bin 0 -> 92308 bytes docs/src/developers_guide/edit_on_github.png | Bin 0 -> 53986 bytes docs/src/developers_guide/find_main.png | Bin 0 -> 38567 bytes docs/src/developers_guide/fork_banner.png | Bin 0 -> 18871 bytes docs/src/developers_guide/imagehash_index.rst | 20 + docs/src/developers_guide/propose_changes.png | Bin 0 -> 24197 bytes docs/src/developers_guide/pull_request.png | Bin 0 -> 105614 bytes docs/src/developers_guide/release.rst | 32 +- docs/src/further_topics/index.rst | 4 +- docs/src/further_topics/lenient_maths.rst | 16 +- docs/src/further_topics/metadata.rst | 11 +- docs/src/further_topics/ugrid/data_model.rst | 566 +++++ .../ugrid/images/data_structured_grid.svg | 1 + .../ugrid/images/data_ugrid_mesh.svg | 1 + .../ugrid/images/geovistalogo.svg | 573 +++++ .../ugrid/images/iris-esmf-regrid.svg | 93 + .../ugrid/images/plotting_basic.png | Bin 0 -> 42338 bytes .../ugrid/images/plotting_global.png | Bin 0 -> 133531 bytes .../ugrid/images/ugrid_edge_data.svg | 1 + .../ugrid/images/ugrid_element_centres.svg | 1 + .../ugrid/images/ugrid_node_independence.svg | 1 + .../ugrid/images/ugrid_variable_faces.svg | 1 + docs/src/further_topics/ugrid/index.rst | 54 + docs/src/further_topics/ugrid/operations.rst | 995 ++++++++ .../further_topics/ugrid/partner_packages.rst | 100 + docs/src/index.rst | 13 +- docs/src/installing.rst | 10 +- docs/src/sphinxext/image_test_output.py | 78 + docs/src/userguide/cube_maths.rst | 6 +- docs/src/userguide/cube_statistics.rst | 14 +- .../interpolation_and_regridding.rst | 4 +- docs/src/userguide/iris_cubes.rst | 4 +- docs/src/userguide/loading_iris_cubes.rst | 91 +- docs/src/userguide/navigating_a_cube.rst | 8 +- docs/src/userguide/plotting_a_cube.rst | 11 +- docs/src/userguide/saving_iris_cubes.rst | 8 + docs/src/userguide/subsetting_a_cube.rst | 6 +- docs/src/whatsnew/1.4.rst | 3 +- docs/src/whatsnew/3.0.rst | 5 +- docs/src/whatsnew/3.1.rst | 5 +- docs/src/whatsnew/index.rst | 2 +- docs/src/whatsnew/latest.rst | 266 +-- docs/src/whatsnew/latest.rst.template | 112 - etc/cf-standard-name-table.xml | 2033 +++++++++-------- lib/iris/_merge.py | 7 +- lib/iris/_representation/cube_summary.py | 2 +- lib/iris/analysis/__init__.py | 104 +- lib/iris/analysis/_area_weighted.py | 1022 ++++++++- lib/iris/analysis/_grid_angles.py | 2 +- lib/iris/analysis/_scipy_interpolate.py | 2 +- lib/iris/analysis/calculus.py | 12 +- lib/iris/analysis/cartography.py | 6 +- lib/iris/analysis/maths.py | 32 +- lib/iris/analysis/stats.py | 4 +- lib/iris/common/metadata.py | 2 +- lib/iris/common/resolve.py | 40 +- lib/iris/coord_systems.py | 5 +- lib/iris/coords.py | 419 +++- lib/iris/cube.py | 103 +- lib/iris/experimental/equalise_cubes.py | 30 - lib/iris/experimental/raster.py | 24 + lib/iris/experimental/regrid.py | 1102 +-------- lib/iris/experimental/regrid_conservative.py | 44 + lib/iris/experimental/ugrid/load.py | 22 +- lib/iris/experimental/ugrid/mesh.py | 645 +++--- lib/iris/experimental/ugrid/metadata.py | 13 +- lib/iris/fileformats/__init__.py | 13 +- lib/iris/fileformats/_ff_cross_references.py | 6 +- .../fileformats/_nc_load_rules/actions.py | 21 +- lib/iris/fileformats/abf.py | 9 + lib/iris/fileformats/dot.py | 9 + lib/iris/fileformats/netcdf.py | 22 +- lib/iris/fileformats/pp.py | 4 +- lib/iris/fileformats/um_cf_map.py | 10 +- lib/iris/io/__init__.py | 16 +- lib/iris/plot.py | 14 +- lib/iris/tests/integration/test_netcdf.py | 6 +- lib/iris/tests/integration/test_trajectory.py | 16 +- .../0d_str.txt | 2 +- .../1d_str.txt | 2 +- .../2d_str.txt | 2 +- .../3d_str.txt | 2 +- .../4d_str.txt | 2 +- .../results/cdm/str_repr/0d_cube.__str__.txt | 2 +- .../cdm/str_repr/0d_cube.__unicode__.txt | 2 +- .../cdm/str_repr/cell_methods.__str__.txt | 2 +- .../cdm/str_repr/missing_coords_cube.str.txt | 2 +- .../results/cdm/str_repr/similar.__str__.txt | 2 +- .../unicode_attribute.__unicode__.txt | 2 +- .../coord_api/str_repr/aux_nontime_repr.txt | 12 +- .../coord_api/str_repr/aux_nontime_str.txt | 26 +- .../coord_api/str_repr/aux_time_repr.txt | 3 +- .../coord_api/str_repr/aux_time_str.txt | 7 +- .../coord_api/str_repr/dim_nontime_repr.txt | 12 +- .../coord_api/str_repr/dim_nontime_str.txt | 25 +- .../coord_api/str_repr/dim_time_repr.txt | 3 +- .../coord_api/str_repr/dim_time_str.txt | 9 +- .../tests/results/derived/no_orog.__str__.txt | 2 +- .../results/derived/removed_orog.__str__.txt | 2 +- .../results/derived/removed_sigma.__str__.txt | 2 +- .../trajectory/tri_polar_latitude_slice.cml | 45 +- .../util/as_compatible_shape_collapsed.cml | 144 -- lib/iris/tests/test_basic_maths.py | 38 +- lib/iris/tests/test_coord_api.py | 146 +- lib/iris/tests/test_coordsystem.py | 23 + lib/iris/tests/test_file_save.py | 18 + lib/iris/tests/test_io_init.py | 49 +- lib/iris/tests/test_load.py | 37 + lib/iris/tests/test_mapping.py | 5 +- lib/iris/tests/test_pickling.py | 11 +- lib/iris/tests/test_util.py | 98 - .../test_AreaWeightedRegridder.py | 17 +- .../unit/analysis/cartography/test_project.py | 2 +- .../cartography/test_rotate_grid_vectors.py | 6 +- .../analysis/cartography/test_rotate_winds.py | 8 +- .../regrid/test_RectilinearRegridder.py | 2 +- .../unit/common/mixin/test_CFVariableMixin.py | 6 +- .../test_add_categorised_coord.py | 2 +- .../unit/coords/test_AncillaryVariable.py | 49 +- .../tests/unit/coords/test_CellMeasure.py | 33 +- lib/iris/tests/unit/coords/test_Coord.py | 60 +- .../unit/coords/test__DimensionalMetadata.py | 1058 ++++++++- lib/iris/tests/unit/cube/test_Cube.py | 26 +- .../ugrid/mesh/test_Connectivity.py | 99 +- .../unit/experimental/ugrid/mesh/test_Mesh.py | 269 ++- .../experimental/ugrid/mesh/test_MeshCoord.py | 130 +- .../ugrid/mesh/test_Mesh__from_coords.py | 2 +- .../metadata/test_ConnectivityMetadata.py | 46 +- .../nc_load_rules/actions/__init__.py | 3 - .../actions/test__hybrid_formulae.py | 99 +- .../actions/test__latlon_dimcoords.py | 337 +++ .../fileformats/netcdf/test_Saver__ugrid.py | 4 +- ...__collapse_degenerate_points_and_bounds.py | 2 +- .../tests/unit/io/test__generate_cubes.py | 37 + lib/iris/tests/unit/io/test_save.py | 45 + .../cube_printout/test_CubePrintout.py | 6 +- .../cube_summary/test_CubeSummary.py | 6 +- lib/iris/util.py | 184 +- noxfile.py | 11 +- pyproject.toml | 2 +- requirements/ci/nox.lock/py37-linux-64.lock | 231 -- requirements/ci/py37.yml | 48 - requirements/ci/py38.yml | 1 - setup.cfg | 3 +- tools/gen_helpers.py | 36 - tools/gen_stash_refs.py | 126 - tools/gen_translations.py | 216 -- tools/generate_std_names.py | 4 +- tools/translator/__init__.py | 1116 --------- 166 files changed, 8738 insertions(+), 5817 deletions(-) create mode 100644 docs/src/_templates/imagehash.html create mode 100755 docs/src/developers_guide/contributing_documentation_easy.rst create mode 100755 docs/src/developers_guide/contributing_documentation_full.rst create mode 100755 docs/src/developers_guide/edit_button.png create mode 100755 docs/src/developers_guide/edit_on_github.png create mode 100755 docs/src/developers_guide/find_main.png create mode 100755 docs/src/developers_guide/fork_banner.png create mode 100644 docs/src/developers_guide/imagehash_index.rst create mode 100755 docs/src/developers_guide/propose_changes.png create mode 100755 docs/src/developers_guide/pull_request.png create mode 100644 docs/src/further_topics/ugrid/data_model.rst create mode 100644 docs/src/further_topics/ugrid/images/data_structured_grid.svg create mode 100644 docs/src/further_topics/ugrid/images/data_ugrid_mesh.svg create mode 100644 docs/src/further_topics/ugrid/images/geovistalogo.svg create mode 100644 docs/src/further_topics/ugrid/images/iris-esmf-regrid.svg create mode 100644 docs/src/further_topics/ugrid/images/plotting_basic.png create mode 100644 docs/src/further_topics/ugrid/images/plotting_global.png create mode 100644 docs/src/further_topics/ugrid/images/ugrid_edge_data.svg create mode 100644 docs/src/further_topics/ugrid/images/ugrid_element_centres.svg create mode 100644 docs/src/further_topics/ugrid/images/ugrid_node_independence.svg create mode 100644 docs/src/further_topics/ugrid/images/ugrid_variable_faces.svg create mode 100644 docs/src/further_topics/ugrid/index.rst create mode 100644 docs/src/further_topics/ugrid/operations.rst create mode 100644 docs/src/further_topics/ugrid/partner_packages.rst create mode 100644 docs/src/sphinxext/image_test_output.py mode change 100644 => 120000 docs/src/whatsnew/latest.rst delete mode 100644 docs/src/whatsnew/latest.rst.template delete mode 100644 lib/iris/experimental/equalise_cubes.py delete mode 100644 lib/iris/tests/results/util/as_compatible_shape_collapsed.cml create mode 100644 lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__latlon_dimcoords.py create mode 100755 lib/iris/tests/unit/io/test__generate_cubes.py create mode 100755 lib/iris/tests/unit/io/test_save.py delete mode 100644 requirements/ci/nox.lock/py37-linux-64.lock delete mode 100644 requirements/ci/py37.yml delete mode 100644 tools/gen_helpers.py delete mode 100644 tools/gen_stash_refs.py delete mode 100644 tools/gen_translations.py delete mode 100644 tools/translator/__init__.py diff --git a/.cirrus.yml b/.cirrus.yml index b3992de64a..92b8d788e6 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -60,7 +60,6 @@ linux_task_template: &LINUX_TASK_TEMPLATE - echo "$(date +%Y).$(expr $(date +%U) / ${CACHE_PERIOD}):${CONDA_CACHE_BUILD}" - uname -r populate_script: - - export CONDA_OVERRIDE_LINUX="$(uname -r | cut -d'+' -f1)" - bash miniconda.sh -b -p ${HOME}/miniconda - conda config --set always_yes yes --set changeps1 no - conda config --set show_channel_urls True @@ -141,8 +140,6 @@ task: only_if: ${SKIP_TEST_TASK} == "" << : *CREDITS_TEMPLATE matrix: - env: - PY_VER: 3.7 env: PY_VER: 3.8 name: "${CIRRUS_OS}: py${PY_VER} tests" @@ -153,7 +150,6 @@ task: << : *IRIS_TEST_DATA_TEMPLATE << : *LINUX_TASK_TEMPLATE tests_script: - - export CONDA_OVERRIDE_LINUX="$(uname -r | cut -d'+' -f1)" - echo "[Resources]" > ${SITE_CFG} - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG} @@ -174,7 +170,6 @@ task: << : *IRIS_TEST_DATA_TEMPLATE << : *LINUX_TASK_TEMPLATE tests_script: - - export CONDA_OVERRIDE_LINUX="$(uname -r | cut -d'+' -f1)" - echo "[Resources]" > ${SITE_CFG} - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG} @@ -197,7 +192,6 @@ task: name: "${CIRRUS_OS}: py${PY_VER} link check" << : *LINUX_TASK_TEMPLATE tests_script: - - export CONDA_OVERRIDE_LINUX="$(uname -r | cut -d'+' -f1)" - mkdir -p ${MPL_RC_DIR} - echo "backend : agg" > ${MPL_RC_FILE} - echo "image.cmap : viridis" >> ${MPL_RC_FILE} diff --git a/.github/workflows/refresh-lockfiles.yml b/.github/workflows/refresh-lockfiles.yml index 32c347320c..f7fa10069f 100644 --- a/.github/workflows/refresh-lockfiles.yml +++ b/.github/workflows/refresh-lockfiles.yml @@ -71,7 +71,7 @@ jobs: strategy: matrix: - python: ['37', '38'] + python: ['38'] steps: - uses: actions/checkout@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9af8d08a70..228970bee2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ minimum_pre_commit_version: 1.21.0 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 + rev: v4.1.0 hooks: # Prevent giant files from being committed. - id: check-added-large-files @@ -29,7 +29,7 @@ repos: - id: no-commit-to-branch - repo: https://github.com/psf/black - rev: 21.11b1 + rev: 22.1.0 hooks: - id: black pass_filenames: false @@ -50,7 +50,7 @@ repos: args: [--filter-files] - repo: https://github.com/asottile/blacken-docs - rev: v1.12.0 + rev: v1.12.1 hooks: - id: blacken-docs types: [file, rst] diff --git a/.readthedocs.yml b/.readthedocs.yml index b54b0f065b..63c4798050 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,20 +1,20 @@ version: 2 build: - image: latest + os: ubuntu-20.04 + tools: + python: mambaforge-4.10 conda: - environment: requirements/ci/readthedocs.yml + environment: requirements/ci/readthedocs.yml sphinx: - configuration: docs/src/conf.py - fail_on_warning: false + configuration: docs/src/conf.py + fail_on_warning: false python: - install: - - method: setuptools - path: . - -formats: - - htmlzip - - pdf + install: + - method: pip + path: . + extra_requirements: + - docs diff --git a/benchmarks/benchmarks/plot.py b/benchmarks/benchmarks/plot.py index 45905abd2f..24899776dc 100644 --- a/benchmarks/benchmarks/plot.py +++ b/benchmarks/benchmarks/plot.py @@ -22,7 +22,7 @@ def setup(self): # Should generate 10 distinct contours, regardless of dim size. dim_size = int(ARTIFICIAL_DIM_SIZE / 5) repeat_number = int(dim_size / 10) - repeat_range = range(int((dim_size ** 2) / repeat_number)) + repeat_range = range(int((dim_size**2) / repeat_number)) data = np.repeat(repeat_range, repeat_number) data = data.reshape((dim_size,) * 2) diff --git a/docs/gallery_code/README.rst b/docs/gallery_code/README.rst index 02263dc5e5..720fd1e6f6 100644 --- a/docs/gallery_code/README.rst +++ b/docs/gallery_code/README.rst @@ -23,4 +23,4 @@ you may start the jupyter notebook via:: If you wish to contribute to the gallery see the :ref:`contributing.documentation.gallery` section of the -:ref:`contributing.documentation`. +:ref:`contributing.documentation_full`. diff --git a/docs/gallery_code/meteorology/plot_wind_barbs.py b/docs/gallery_code/meteorology/plot_wind_barbs.py index c3c056eb4a..b09040c64e 100644 --- a/docs/gallery_code/meteorology/plot_wind_barbs.py +++ b/docs/gallery_code/meteorology/plot_wind_barbs.py @@ -30,7 +30,7 @@ def main(): # To illustrate the full range of barbs, scale the wind speed up to pretend # that a storm is passing over - magnitude = (uwind ** 2 + vwind ** 2) ** 0.5 + magnitude = (uwind**2 + vwind**2) ** 0.5 magnitude.convert_units("knot") max_speed = magnitude.collapsed( ("latitude", "longitude"), iris.analysis.MAX @@ -41,7 +41,7 @@ def main(): vwind = vwind / max_speed * max_desired # Create a cube containing the wind speed - windspeed = (uwind ** 2 + vwind ** 2) ** 0.5 + windspeed = (uwind**2 + vwind**2) ** 0.5 windspeed.rename("windspeed") windspeed.convert_units("knot") diff --git a/docs/gallery_code/meteorology/plot_wind_speed.py b/docs/gallery_code/meteorology/plot_wind_speed.py index fd03f54205..40d9d0da00 100644 --- a/docs/gallery_code/meteorology/plot_wind_speed.py +++ b/docs/gallery_code/meteorology/plot_wind_speed.py @@ -27,7 +27,7 @@ def main(): vwind = iris.load_cube(infile, "y_wind") # Create a cube containing the wind speed. - windspeed = (uwind ** 2 + vwind ** 2) ** 0.5 + windspeed = (uwind**2 + vwind**2) ** 0.5 windspeed.rename("windspeed") # Plot the wind speed as a contour plot. diff --git a/docs/src/_templates/imagehash.html b/docs/src/_templates/imagehash.html new file mode 100644 index 0000000000..8b0dac0cce --- /dev/null +++ b/docs/src/_templates/imagehash.html @@ -0,0 +1,15 @@ +{% extends "!layout.html" %} + +{% block body %} + +

Test: {{ test }}

+ + +{% for hash, file in hashfiles %} +
+

{{hash}}

+ +
+{% endfor %} + +{% endblock %} \ No newline at end of file diff --git a/docs/src/common_links.inc b/docs/src/common_links.inc index eb1ea60b7a..67fc493e3e 100644 --- a/docs/src/common_links.inc +++ b/docs/src/common_links.inc @@ -37,6 +37,7 @@ .. _test-iris-imagehash: https://github.com/SciTools/test-iris-imagehash .. _using git: https://docs.github.com/en/github/using-git .. _requirements/ci/: https://github.com/SciTools/iris/tree/main/requirements/ci +.. _CF-UGRID: https://ugrid-conventions.github.io/ugrid-conventions/ .. comment diff --git a/docs/src/conf.py b/docs/src/conf.py index e13f12a13a..19f22e808f 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -158,6 +158,7 @@ def _dotv(version): "sphinx_gallery.gen_gallery", "matplotlib.sphinxext.mathmpl", "matplotlib.sphinxext.plot_directive", + "image_test_output", ] if skip_api == "1": @@ -199,7 +200,9 @@ def _dotv(version): # -- copybutton extension ----------------------------------------------------- # See https://sphinx-copybutton.readthedocs.io/en/latest/ -copybutton_prompt_text = ">>> " +copybutton_prompt_text = r">>> |\.\.\. " +copybutton_prompt_is_regexp = True +copybutton_line_continuation_character = "\\" # sphinx.ext.todo configuration ----------------------------------------------- # See https://www.sphinx-doc.org/en/master/usage/extensions/todo.html @@ -208,6 +211,7 @@ def _dotv(version): # api generation configuration autodoc_member_order = "groupwise" autodoc_default_flags = ["show-inheritance"] +autodoc_typehints = "none" autosummary_generate = True autosummary_imported_members = True autopackage_name = ["iris"] @@ -327,8 +331,10 @@ def _dotv(version): "gallery_dirs": ["generated/gallery"], # filename pattern for the files in the gallery "filename_pattern": "/plot_", - # filename patternt to ignore in the gallery + # filename pattern to ignore in the gallery "ignore_pattern": r"__init__\.py", + # force gallery building, unless overridden (see src/Makefile) + "plot_gallery": "'True'", } # ----------------------------------------------------------------------------- diff --git a/docs/src/developers_guide/contributing_documentation.rst b/docs/src/developers_guide/contributing_documentation.rst index efd31d4f20..e289b1548d 100644 --- a/docs/src/developers_guide/contributing_documentation.rst +++ b/docs/src/developers_guide/contributing_documentation.rst @@ -1,173 +1,21 @@ -.. _contributing.documentation: - -Contributing to the Documentation ---------------------------------- +How to Contribute to the Documentation +-------------------------------------- Documentation is important and we encourage any improvements that can be made. If you believe the documentation is not clear please contribute a change to improve the documentation for all users. -Any change to the Iris project whether it is a bugfix, new feature or -documentation update must use the :ref:`development-workflow`. - - -Requirements -~~~~~~~~~~~~ - -The documentation uses specific packages that need to be present. Please see -:ref:`installing_iris` for instructions. - - -.. _contributing.documentation.building: - -Building -~~~~~~~~ - -This documentation was built using the latest Python version that Iris -supports. For more information see :ref:`installing_iris`. - -The build can be run from the documentation directory ``docs/src``. - -The build output for the html is found in the ``_build/html`` sub directory. -When updating the documentation ensure the html build has *no errors* or -*warnings* otherwise it may fail the automated `cirrus-ci`_ build. - -Once the build is complete, if it is rerun it will only rebuild the impacted -build artefacts so should take less time. - -There is an option to perform a build but skip the -:ref:`contributing.documentation.gallery` creation completely. This can be -achieved via:: - - make html-noplot - -Another option is to skip the :ref:`iris` documentation creation. This can be -useful as it reduces the time to build the documentation, however you may have -some build warnings as there maybe references to the API documentation. -This can be achieved via:: - - make html-noapi - -You can combine both the above and skip the -:ref:`contributing.documentation.gallery` and :ref:`iris` documentation -completely. This can be achieved via:: - - make html-quick - -If you wish to run a full clean build you can run:: - - make clean - make html - -This is useful for a final test before committing your changes. - -.. note:: In order to preserve a clean build for the html, all **warnings** - have been promoted to be **errors** to ensure they are addressed. - This **only** applies when ``make html`` is run. - -.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris - -.. _contributing.documentation.testing: - -Testing -~~~~~~~ - -There are a ways to test various aspects of the documentation. The -``make`` commands shown below can be run in the ``docs`` or -``docs/src`` directory. - -Each :ref:`contributing.documentation.gallery` entry has a corresponding test. -To run the tests:: - - make gallerytest - -Many documentation pages includes python code itself that can be run to ensure -it is still valid or to demonstrate examples. To ensure these tests pass -run:: - - make doctest - -See :data:`iris.cube.Cube.data` for an example of using the `doctest`_ -approach. - -.. _doctest: http://www.sphinx-doc.org/en/stable/ext/doctest.html - -The hyperlinks in the documentation can be checked automatically. -If there is a link that is known to work it can be excluded from the checks by -adding it to the ``linkcheck_ignore`` array that is defined in the -`conf.py`_. The hyperlink check can be run via:: - - make linkcheck - -If this fails check the output for the text **broken** and then correct -or ignore the url. - -.. comment - Finally, the spelling in the documentation can be checked automatically via the - command:: - - make spelling - - The spelling check may pull up many technical abbreviations and acronyms. This - can be managed by using an **allow** list in the form of a file. This file, - or list of files is set in the `conf.py`_ using the string list - ``spelling_word_list_filename``. - - -.. note:: In addition to the automated `cirrus-ci`_ build of all the - documentation build options above, the - https://readthedocs.org/ service is also used. The configuration - of this held in a file in the root of the - `github Iris project `_ named - ``.readthedocs.yml``. - - -.. _conf.py: https://github.com/SciTools/iris/blob/main/docs/src/conf.py - - -.. _contributing.documentation.api: - -Generating API Documentation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In order to auto generate the API documentation based upon the docstrings a -custom set of python scripts are used, these are located in the directory -``docs/src/sphinxext``. Once the ``make html`` command has been run, -the output of these scripts can be found in -``docs/src/generated/api``. - -If there is a particularly troublesome module that breaks the ``make html`` you -can exclude the module from the API documentation. Add the entry to the -``exclude_modules`` tuple list in the -``docs/src/sphinxext/generate_package_rst.py`` file. - - -.. _contributing.documentation.gallery: - -Gallery -~~~~~~~ - -The Iris :ref:`sphx_glr_generated_gallery` uses a sphinx extension named -`sphinx-gallery `_ -that auto generates reStructuredText (rst) files based upon a gallery source -directory that abides directory and filename convention. - -The code for the gallery entries are in ``docs/gallery_code``. -Each sub directory in this directory is a sub section of the gallery. The -respective ``README.rst`` in each folder is included in the gallery output. - -For each gallery entry there must be a corresponding test script located in -``docs/gallery_tests``. +If you're confident diving right in, please head for +:ref:`contributing.documentation_full`. -To add an entry to the gallery simple place your python code into the -appropriate sub directory and name it with a prefix of ``plot_``. If your -gallery entry does not fit into any existing sub directories then create a new -directory and place it in there. +If you're not then we've got a step-by-step guide here to walk you through it: +:ref:`contributing.documentation_easy` -The reStructuredText (rst) output of the gallery is located in -``docs/src/generated/gallery``. +.. toctree:: + :maxdepth: 1 + :hidden: -For more information on the directory structure and options please see the -`sphinx-gallery getting started -`_ documentation. + contributing_documentation_easy + contributing_documentation_full + \ No newline at end of file diff --git a/docs/src/developers_guide/contributing_documentation_easy.rst b/docs/src/developers_guide/contributing_documentation_easy.rst new file mode 100755 index 0000000000..f54de628bf --- /dev/null +++ b/docs/src/developers_guide/contributing_documentation_easy.rst @@ -0,0 +1,102 @@ + +.. _contributing.documentation_easy: + +Contributing to the Documentation (the easy way) +------------------------------------------------ + +Documentation is important and we encourage any improvements that can be made. +If you believe the documentation is not clear please contribute a change to +improve the documentation for all users. + +The guide below is designed to be accessible to those with little-to-no +knowledge of programming and GitHub. If you find that something doesn't work as +described or could use more explanation then please let us know (or contribute +the improvement yourself)! + +First Time Only Steps +^^^^^^^^^^^^^^^^^^^^^ + +1. Create a `GitHub `_ account. + +2. Complete the Scitools Contributor License Agreement (`link to Google Form + `_). + This is a one-off requirement for anyone who wishes to contribute to a + Scitools repository - including the documentation. + +Steps to Complete Each Time You Propose Changes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +1. Navigate to the documentation page that you want to edit (on this site). + +2. Click the ``Edit on GitHub`` button at the **top right** of the page. + +.. image:: edit_on_github.png + +3. In the resulting GitHub page select **main** from the ``Switch + branches/tags`` drop-down menu near the **top left** of the page (to the left + of the ``iris / docs / src / ...`` links) if it isn't already. This changes + the branch to **main**. + +.. image:: find_main.png + +4. Click the pencil symbol near the **top right** (to the right of the ``Raw`` + and ``Blame`` buttons). + +.. image:: edit_button.png + +5. Make your edits! Try to strike a balance between informing the audience + enough that they understand and overwhelming them with information. + +.. note:: + + You may see the following message at the top of the edit page, informing you + that GitHub has created you your own ``fork`` (or copy) of the project as a + precursor to allowing you to edit the page. Your changes will be merged into + the main version of the documentation later. + + .. image:: fork_banner.png + +6. Scroll to the bottom of the edit page and enter some appropriate information + in the two boxes under ``Propose changes``. You can just keep the default text + if you like or enter something more specific - a short sentence explaining + what's changed is fine. Then click the ``Propose changes`` button. + +.. image:: propose_changes.png + +7. In the resulting page titled ``Pull Request``, write a brief description of + what you've changed underneath the following three lines: + +.. code:: + + ### Description + + + +Describing what you've changed and why will help the person who reviews your changes. + +.. image:: pull_request.png + +8. Click the ``Create pull request`` button. + +.. tip:: + + If you're not sure that you're making your pull request right, or have a + question, then make it anyway! You can then comment on it tagging + ``@SciTools/iris-devs`` to ask your question (then edit your pull request if + you need to). + +What Happens Next? +^^^^^^^^^^^^^^^^^^ + +Another Iris contributor will review your changes (this happens for everyone who +makes changes to Iris or its documentation). The reviewer might make comments or +ask questions (don't worry about missing these, GitHub will email you to let you +know). You can respond to these comments underneath where they appear in GitHub. + +Once you've worked everything out together, the reviewer will merge your changes +into the main version of the documentation so that they're accessible for +everyone to benefit from. + +**You've now contributed to the Iris documentation!** If you've caught the bug +and want to get more involved (or you're just interested what that would mean) +then chat to the person reviewing your code or another Iris contributor. \ No newline at end of file diff --git a/docs/src/developers_guide/contributing_documentation_full.rst b/docs/src/developers_guide/contributing_documentation_full.rst new file mode 100755 index 0000000000..77b898c0f3 --- /dev/null +++ b/docs/src/developers_guide/contributing_documentation_full.rst @@ -0,0 +1,173 @@ + +.. _contributing.documentation_full: + +Contributing to the Documentation +--------------------------------- + +This guide is for those comfortable with the development process, looking for +the specifics of how to apply that knowledge to Iris. You may instead find it +easier to use the :ref:`contributing.documentation_easy`. + +Any change to the Iris project whether it is a bugfix, new feature or +documentation update must use the :ref:`development-workflow`. + + +Requirements +~~~~~~~~~~~~ + +The documentation uses specific packages that need to be present. Please see +:ref:`installing_iris` for instructions. + + +.. _contributing.documentation.building: + +Building +~~~~~~~~ + +This documentation was built using the latest Python version that Iris +supports. For more information see :ref:`installing_iris`. + +The build can be run from the documentation directory ``docs/src``. + +The build output for the html is found in the ``_build/html`` sub directory. +When updating the documentation ensure the html build has *no errors* or +*warnings* otherwise it may fail the automated `cirrus-ci`_ build. + +Once the build is complete, if it is rerun it will only rebuild the impacted +build artefacts so should take less time. + +There is an option to perform a build but skip the +:ref:`contributing.documentation.gallery` creation completely. This can be +achieved via:: + + make html-noplot + +Another option is to skip the :ref:`iris` documentation creation. This can be +useful as it reduces the time to build the documentation, however you may have +some build warnings as there maybe references to the API documentation. +This can be achieved via:: + + make html-noapi + +You can combine both the above and skip the +:ref:`contributing.documentation.gallery` and :ref:`iris` documentation +completely. This can be achieved via:: + + make html-quick + +If you wish to run a full clean build you can run:: + + make clean + make html + +This is useful for a final test before committing your changes. + +.. note:: In order to preserve a clean build for the html, all **warnings** + have been promoted to be **errors** to ensure they are addressed. + This **only** applies when ``make html`` is run. + +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris + +.. _contributing.documentation.testing: + +Testing +~~~~~~~ + +There are a ways to test various aspects of the documentation. The +``make`` commands shown below can be run in the ``docs`` or +``docs/src`` directory. + +Each :ref:`contributing.documentation.gallery` entry has a corresponding test. +To run the tests:: + + make gallerytest + +Many documentation pages includes python code itself that can be run to ensure +it is still valid or to demonstrate examples. To ensure these tests pass +run:: + + make doctest + +See :data:`iris.cube.Cube.data` for an example of using the `doctest`_ +approach. + +.. _doctest: http://www.sphinx-doc.org/en/stable/ext/doctest.html + +The hyperlinks in the documentation can be checked automatically. +If there is a link that is known to work it can be excluded from the checks by +adding it to the ``linkcheck_ignore`` array that is defined in the +`conf.py`_. The hyperlink check can be run via:: + + make linkcheck + +If this fails check the output for the text **broken** and then correct +or ignore the url. + +.. comment + Finally, the spelling in the documentation can be checked automatically via the + command:: + + make spelling + + The spelling check may pull up many technical abbreviations and acronyms. This + can be managed by using an **allow** list in the form of a file. This file, + or list of files is set in the `conf.py`_ using the string list + ``spelling_word_list_filename``. + + +.. note:: In addition to the automated `cirrus-ci`_ build of all the + documentation build options above, the + https://readthedocs.org/ service is also used. The configuration + of this held in a file in the root of the + `github Iris project `_ named + ``.readthedocs.yml``. + + +.. _conf.py: https://github.com/SciTools/iris/blob/main/docs/src/conf.py + + +.. _contributing.documentation.api: + +Generating API Documentation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to auto generate the API documentation based upon the docstrings a +custom set of python scripts are used, these are located in the directory +``docs/src/sphinxext``. Once the ``make html`` command has been run, +the output of these scripts can be found in +``docs/src/generated/api``. + +If there is a particularly troublesome module that breaks the ``make html`` you +can exclude the module from the API documentation. Add the entry to the +``exclude_modules`` tuple list in the +``docs/src/sphinxext/generate_package_rst.py`` file. + + +.. _contributing.documentation.gallery: + +Gallery +~~~~~~~ + +The Iris :ref:`sphx_glr_generated_gallery` uses a sphinx extension named +`sphinx-gallery `_ +that auto generates reStructuredText (rst) files based upon a gallery source +directory that abides directory and filename convention. + +The code for the gallery entries are in ``docs/gallery_code``. +Each sub directory in this directory is a sub section of the gallery. The +respective ``README.rst`` in each folder is included in the gallery output. + +For each gallery entry there must be a corresponding test script located in +``docs/gallery_tests``. + +To add an entry to the gallery simple place your python code into the +appropriate sub directory and name it with a prefix of ``plot_``. If your +gallery entry does not fit into any existing sub directories then create a new +directory and place it in there. + +The reStructuredText (rst) output of the gallery is located in +``docs/src/generated/gallery``. + +For more information on the directory structure and options please see the +`sphinx-gallery getting started +`_ documentation. diff --git a/docs/src/developers_guide/contributing_graphics_tests.rst b/docs/src/developers_guide/contributing_graphics_tests.rst index 53e895f440..1268aa2686 100644 --- a/docs/src/developers_guide/contributing_graphics_tests.rst +++ b/docs/src/developers_guide/contributing_graphics_tests.rst @@ -63,6 +63,8 @@ This consists of: developer to easily compare proposed new **acceptable** result images against the existing accepted reference images, for each failing test. +The acceptable images for each test can be viewed online. The :ref:`testing.imagehash_index` lists all the graphical tests in the test suite and +shows the known acceptable result images for comparison. Reviewing Failing Tests ======================= diff --git a/docs/src/developers_guide/contributing_testing_index.rst b/docs/src/developers_guide/contributing_testing_index.rst index 517111507b..c5cf1b997b 100644 --- a/docs/src/developers_guide/contributing_testing_index.rst +++ b/docs/src/developers_guide/contributing_testing_index.rst @@ -8,5 +8,6 @@ Testing contributing_testing contributing_graphics_tests + imagehash_index contributing_running_tests contributing_ci_tests diff --git a/docs/src/developers_guide/documenting/whats_new_contributions.rst b/docs/src/developers_guide/documenting/whats_new_contributions.rst index ebb553024b..576fc5f6a6 100644 --- a/docs/src/developers_guide/documenting/whats_new_contributions.rst +++ b/docs/src/developers_guide/documenting/whats_new_contributions.rst @@ -4,16 +4,21 @@ Contributing a "What's New" Entry ================================= -Iris uses a file named ``latest.rst`` to keep a draft of upcoming changes -that will form the next release. Contributions to the :ref:`iris_whatsnew` +Iris uses a file named ``dev.rst`` to keep a draft of upcoming development changes +that will form the next stable release. Contributions to the :ref:`iris_whatsnew` document are written by the developer most familiar with the change made. The contribution should be included as part of the Iris Pull Request that introduces the change. -The ``latest.rst`` and the past release notes are kept in -``docs/src/whatsnew/``. If you are writing the first contribution after -an Iris release: **create the new** ``latest.rst`` by copying the content from -``latest.rst.template`` in the same directory. +The ``dev.rst`` and the past release notes are kept in the +``docs/src/whatsnew/`` directory. If you are writing the first contribution after +an Iris release: **create the new** ``dev.rst`` by copying the content from +``dev.rst.template`` in the same directory. + +.. note:: + + Ensure that the symbolic link ``latest.rst`` references the ``dev.rst`` file + within the ``docs/src/whatsnew`` directory. Since the `Contribution categories`_ include Internal changes, **all** Iris Pull Requests should be accompanied by a "What's New" contribution. @@ -22,7 +27,7 @@ Pull Requests should be accompanied by a "What's New" contribution. Git Conflicts ============= -If changes to ``latest.rst`` are being suggested in several simultaneous +If changes to ``dev.rst`` are being suggested in several simultaneous Iris Pull Requests, Git will likely encounter merge conflicts. If this situation is thought likely (large PR, high repo activity etc.): @@ -43,7 +48,7 @@ situation is thought likely (large PR, high repo activity etc.): * PR reviewer: review the "What's New" PR, merge once acceptable -These measures should mean the suggested ``latest.rst`` changes are outstanding +These measures should mean the suggested ``dev.rst`` changes are outstanding for the minimum time, minimising conflicts and minimising the need to rebase or merge from trunk. diff --git a/docs/src/developers_guide/edit_button.png b/docs/src/developers_guide/edit_button.png new file mode 100755 index 0000000000000000000000000000000000000000..ee2e7858f051aaa63af6ac8f23a8954603ec8251 GIT binary patch literal 92308 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 literal 0 HcmV?d00001 diff --git a/docs/src/developers_guide/edit_on_github.png b/docs/src/developers_guide/edit_on_github.png new file mode 100755 index 0000000000000000000000000000000000000000..f802ebd9d63bccd37dfd5f50bfed425d2aa2e096 GIT binary patch literal 53986 zcmc$_cTkf}^goIP5s{*ZNEekRAib9eB1jVj=~WSt8hQ^=dXI|qCLkasbV5(0*HEN~ zCM|SA3nh@`7hm7^e(!JQ_s5+(cjoR4!#yQq8hBB=#k1LGn6yWp;?rc72kz`9O)xNNJWtwcsv0iim7O-_2Iba`g% zPDXZ{>(Ac>b=?QMWMsd*)m4;U`B-cD z-o)uy?HUD*=seYRRi!3Qjmun7D&#iQIhSqXFSfiXr&GG8@#@)Kx(h9LF1Jdw&^cca z3DVBXf0|qPgXsNk&V8U?E=i0>CaEWV^;DjtJ4I~5drr)D4wK@`gLy(4*`M-3KuP$* z-zu4Lu+^VQ{VA1KS}y;sk-eo3qWrho&3u{u-|8ocE4Q?ohy)4{*aws0eKxlPS{58C+;|y;waeTVBiSEYV5-(bBwTP!x^l3b1v$-AdZ^4fMh4)%0 zIGPT28hhJQj*elR|NbJKjAhUY*lh_1Vt<8R{+E6JWP3~d8Arfl@0o|# znU$rW#~fn$mN@_GVHQpr`|-S?sx8+f6e;-p!^cQUfSq_y3a|^=AgnR&kB+!Vd3`tq*>sw%&JA>dAX?k3wC3_ z4#R<_H$5*s>pGC}2sbp7p5$~G?aU!k=Ho1~N=SZFZbE*&c_G_lGd;)EdeymxT57uf z%|>0t(*w!kHuwJd9V218nG-t)nul#r=ghR!O4+*oxr(PtXvde~1#@lK<6E(fwl)O; z>p(HRLIfAPW3nQAEslJ8{#BUmsNjKUwVnr7)&&#Qwy`UoQvWcVw79{R;a2y!AXh;! zXRQ9j>>a(PSE}9LF4IrNTeTh~T0+YB1epy{F3N0~H%Fuov@X+6vu8*jjfYMsPi=2%av=Nzf2!>nSMRd>LO_);ES)&V-UoTqN;!yxWgccv5^c|X?56Ic`w|D z@5;kmo59@Tf?1EfNpXpFJvyxrHHl>DdTfte>1m6j!$OE~@}0Ufc7(s<@D^iWF^x`S zplx8?=>o+L95CK`XoCxmoR>^ujBVW9yR@4Db@Yx!rRABNL(65qJMC6E z2cf4n-U=@OyLTi1XHZ9YC8k^sg8|DI0cRO#PN}%G-QupqH7>J$&R(N3H08)Dr}#B)GH1OwuDGpjgDfbLzBoUYMb1e(iSWhhUc?oV z4j0xAd2F1$fgL z#(SSquC5)jn!Iy(g0OvM(-T0p_#PjV6y@ zMsM=NjOPRNLFim|xqFgcM>cPktMa=H^rz3W2%8f%NaAxsP1S^*rih*3oWny&)V0<2 zaV~GOqhQS1eKWVVHWpg%_FziuT!G&1yUZL-!!XBQEbe?z22_ z`)9>gW1?Tj*OkV3m+RbdrT!<}^D=M{_I9oIxMA9&jmVBVmw|F{rnl?Eq4?H_qP~?h}^OVYa#Bngq_+l`0fw0 z*A=4f%qU9;Wa*C9T2=E{@^(n$!wqj43;GJsYuDj+^63@LF4ab--@qdUgm6PEeN;gr zv@%4Ly-$K+EB82#@qRzV{M|l!I6t(aa3`hFc^(LjjImxaV_O@#7_gLpCLh@uO62Nw z+Gx@jB6hxJX@2;`ERTn#z#wNbs^eHEgO|~Jvv~!Ak!A)P8g6xMRQl(bn#emXP?Z^} z(UI4=!B`j2aywIsm~gXNbRo# zlvEFaA zP4uTcti`jBl;r5$&?2ez9HB9*#p6qxr*0@bZKrIL$03{o2cB(T;86qN`B%-Py>Qml zEE5X#E{nUEWR}2ferMza%0HJ|$hgtDtApKUz@h+Mt;T?s z@5+K5CoQk^22;mL`UZJScwB8o#iQ5|FdJ3_(QaICt@^r2&gwzK%{=~mbh=u#B5Gef$`^~bl zQO!W};?Mj{do91ZR0F$`y{6i7z7E{doH$^#%NdiK64#!#T&0=t&azN|E4qy`pwhKl z+8tPUqGq!@ZfhELp$`(=OCwfjQh90(-kUPc_Bn9)pTzM$JWM&6PqPBg}~}8#DB0XlJVZb{x(u@xU>#b>Qj6gn$JdoRpdMD>r@u9leURZb?5CwZw{FW1bHo?!8_wyH*7HSj7qEz9}UxQRWu_ zgAr;6rf1k#T6U3)4?ANs%yQLuQWb+DrD~BAt0iT-VF{2KK+mt+ShLcXtb@CcW zO2Lx2d&wjhv?Z?jB8L&pc04%It2WC2(zkOC*A4T*Jji&X7G3BJwsI};i6-$h{mh#@ zWFJwXXP_T#!jq^A8^86m8E#`yafjC-U-M<%MqRr$i^|II?YGX3UDw(hGwb|)W`j{` zj}16qGNI*#{SyeT5t3-ZrQ%DmR*OBjUun>U^7o5vXbTzIi6ndD$5Z!S&gb|Si5s-y z8m)N}6j+OI=4;rD8{u*i4x(al2La8W`PvoJ%!Q(LaEoU(^~e#$-`dD&8i(T7TGF-z zk6f6F_tX}3V@D28qJhKN`(bgYECT>#Z_e8C$0oz+QgeT;jURi)(WKA4=QMqBIG@cq zj&jZqTeAk)fSQO+Gv8~ko>#PT`n3sfSj;agrXH=O0q|YP^xexH{wP>=@YEq7s$PO% z;N9owG#oFfN?~$)V%8-*kd>oMa%G8Q-$k3INi5%vSIC}lu(rbetAPZM;$yOR@TG$ zoZRPk#b;CkdHzv7$nq+dy5c#1ulCpTPzbDtlhSbT^+dX3>i&}o4?SVm#gqz-bWZsO zt?TE#TvR!iVxRK+%N!-nHy2)}hug=E9(703J8%r8cAaT~^w|S+)AzdEC~`A^P8skL za=If^_OB_3y>mzC%Fr#21@A7McH2b{{;rl)d&#CZ_M+=lqBW|As^5|~dM@QZ%^I2x zsW?*`waVG77aiX6RVH3A@H^of4wI5{6L+GeS7+5a_lkd?1<9CpWL{7)6vsTPHUlZ} z%=-_1pJF*gr+6GI55I%GyC7P0$4Gd%;jy?Y)HTL2tE*Pzwaq7#|KbaiNE|GUxwb)# zrTQaU+IqT|`5=$KY?A>^SY3YPlB&KMW%nkOo}7NXFlH(>akGF|-os>Z)L*k8(kT+7 zruorloZfr7h9z2{#mp>M1jCd3f`7pkWV3i7-GI5nJx!ZY=^RZCh4=6lZ12gnhi_7j z1kFhYDxdylVNP=tG_t0D2aK5D%{vrd3DL`nq89f`DL+4*>|pCGS(NM6ZT(};XVSo| z7IRJ;#}-nx^;D+!aW=!MmHL2|_K)NMg@c%^N7XCLxZUX68&nGQ&&4|i8>};M^tFoT z{N5#rZEQi2X)N*_=XN6^$`QE)Xfjl^Be zchJe%XyX~HE^Ny+k!-QyMr}(*22tAF7&rTw$2%(DXI{XClGUoY6=A60YDoN0r{HCn zg($OXouh}!I1Fh%$$)&0WBQz-Rl6Fm@N;26(UlH01cxS~_XZ#;>;)GOX^(Q!T+iE$+a8mZL@?dQS$$>&qDWL~DUcc-j_mXrS zs-~JV>o#GTVYqV;Iep?4gk4>QvtLKVj!kjpr&v>&>P9gb^Ra1tq0p?G(^HRcGKwy| zj|wQ+j&dtKVKVngO1&XxULP@3&bPGo=)j&XQEHxzgJ~MC>;NUw8 zYtq`piAd%@-Atef!sZ|Hf1PwOehd^EFSGEIzV@a`=irAwn)J~;T z5!<^`-zLwep^h$51}S+CPe|^&)HM5;oY_!APx|rhd8&RV+q%a6p_yl1cbv_on%P*k zdz32_yiOaI+EfBA(aW1q40R?@ur0H*Pje69r*Xfc)YwZ)=ZA!KHqCxcYSpIrIOl6#;5NUNi8Ib=p4!sY znZa6cIH)&V7bmB?t=)#ri@;WF@4cs>Q-)&tjcC8RPgK{>ge<}}(4H?ebVlry!*%sl ztB_12_jy=fBKgkmCFQ@;3G}J+U0+^6=`rlD=p5!-HnAPjYY8wSUv}|xAD)rNL~olX z2h^&DJoTG*0q)H;OU+5PiZ6-2( zJHcA!wkGa<8XMcEK1p;UdbhM5m?(gj)g7E>6^zi12zkz;z^B0?_5RxB$u2;XU`nt= z*J=1pE`LFE{8>2k5)?b_umJrtjcpmYpW$a^L-=Kh^P|sP5VK62Z zu-p2wfG44@`nq{)G!Mz;IU%SoLfV{)`PkS-{)IG+yKuV>tpT58$*Q$ZymN9Gr)yT$Cw$h)Z)$jlR<_=A;n8%X^0EHCKhAZ#z@He!pK!o^ zzQ5~C@O$jA*sErq>GQ7SWIB)*k^MwCTgn5e?bmF#G2Ruzx&ynFM{yy^O1aj*?GH?^&S2@MCISGR2d3mpZrE-c1OX(~I z-SfzXw3@IVOH~Xx6V6u!qc;PQxHQwvRw{(sJI65Rqx;D(Zh!Y4S@ZGyU@iPwUa#$l zH!3c#FQ+~jCg8qf_DR=W_ zHXA$l3R&~1I8YqT?H2Uc<0sP+ty(RSuBX|iFwU_|qE1t}n_l(U^8G*ZLHQ%*p4yy; zznRuq^wVscc@v`;t88~UDn&fGG(^g;vd(kv-IR$z)up9Jek+tW%(byBIg!EJ>Rh_H z2Lt|Ectz$ZjQ})4Vc5nlXH!8%zI@=lCXY#}bxUtp?NTnU*0wlEsdm7hGw>y7l^$i# zSUZUYeV2zGd<%>WDJ;2Za3wrb`g(#v&Ytd+*HR&C#kFs>ImnDJKhg@flX4>6Vac*O z3C)Sud6RbyZ?ThfQ_W-f&B$UuU?s}JtLeQmQJi7WDbD*xA>g2cT`NZLp{dQaiI`Q$ zt-;xcfHc)^8<1)bCeYvNY4A*68AokEDV8>LBsXYW_k{UGPpfq4USwxXZh~z%H598j&=_8$bQoX=DoJsT5*tMcYs)`1mbtY zDnb)$zKFDYKT4|>mK~6hdev{x;3!<0-HotW*r4vifJP&(-=v_AG|*vDf^fDo+e@h2 z!jfG1cuNt}4EL^m2e>Di$riuKpqE%We%wtBV5q7=^X;X66LM%nb+N;jqOLh{v-|7u zz2!>Y)pMT-T)Pd2uiU_MN6YPGSt7CveaDunF!fV2I~uH7KKQ7>15#ke0WaWabo#9D z21olepc6Ri4VW2fTG;Dwc4sURbS58;S{-FPqH;E!XCqCObD9fw!v5&<_Zy>_DJnGrMm+Rwc$dt|zb z^z9aR7c~9skOSQncX)#GuOA>Urk$8xtp$`D&Hq%}$r4{U+6dK<)0X_slOSkM=WZd; z$(JFAc~o}WKj2GvZigHxYD`s@T&zhv~^2={rz)@ony^+q^=QN=DTp3`gG9l zZ1~?>dhnlHTAO5Vkl>N8pU>sC2^2`C5Oa|r(MX_o`KGOLl2-<@tK zJMp!Fe`psAuHbrfcFHaCUpK$Wsd3(~0o>4jA+rBZxK_&;F7c9rzBTYNeVJt1MA2?o zxRh$zp<*!QLL^jk$37tTWJV(<}YJK`v3do}ipdHC9EyzFDOaz^2|!%EKPZ)GL`LcFk$s z(t7VV&N{)-KwUGiO>%7`T@Kjt+$Xj~iE$ZslU*`SS%k{cmSjzl6PBeyS>L^jybrdh zai*wGJ)NH-^B|HOo(`Ao`Q=|`9fT~}9T+q_IxkHHQ7!;2K?}p{m76U&l(o_7UVXIr zZEY(3Cbz41Lk&x$?w-Ry+h(g%8na28y5$YHSDp0go%6Dw=TFWyX|}89tD)(jB$3rL z%p=Pwf?v=8V+fGl$;8Odq1p#)aeez(ug>twDrxs+m_+^)!<9HHoBY&c0)c4EGv#9= zfk8H6H3NDWNrTShwx4IoLs&EttH_j$8@X=$dx z^A9-Ht^|D|%;~c_dqE-X(vA4x&G4NIY2zscmYDF!%c>5T%&B20Pu!>X9&1+ev@5lU zb=kny;fW0AFy;?HxU*?UwOq*sI+7dk<~Au}sd#7dDSbqQ%(5${`)_AD#eX#3{q0EuCy0Nq}Ob$Cs_4;k8(|j@vc=_KdY?X{X@0zBZiw zC!p}B%=wYi2OR@Wejfu1NB#u_@8!2&CcPn;R#Cb`f$c9{MDi7S3i>NR#6g@W*Poe? z$|4z-Me8bIcg`PCtDW@{3}gxVmpFLpK@cSgw+xs(BLSBJ{{fF@wr(vUE$br~ciaEa zMw%JIWqOf0e~bqS_<>`+Fom^q1O?jLPyfu6B&`P|JMlj(R+#Q8_rFlzY5sZqUwQ;z!VTZ!w;H^jo|T z8vmNtrTePi(C%lQ4W-~+dS73TzP_^&9nh90_^c3oiUAN?I>5feyMuTKg6)9(<_P#S z1#lYQ1c~q_KGS&=!ZCQ(ZL4_D1;$S41niV4^4U^ke|$nm_Soo|yYOD%e52D7fP6qt zIEB#Z?u(Db6jX}xFLoCQnu_v%Q;oC5HIx7!oh-$!Z*@OuN9k#K)2~1I_(b(Y#bPci zVEKO5iP0QSC|%J#?hRjl)ffm3 zK1*oOA`qQ<3?JTGkQv+}bk4dXBppW?!_5gRS(d)zNN?gu25qJsexa%_DMP<`A)Ns_ zkJ?-C=CC-9E#|=o|Ib*qTU3tGRP(dogK$J9v1QLPz(+Yir0yYHMp52q*K6XYwK53D zdOxJY^Uc^K6&&DD8z6CKQf@N{vr5%>%v`2Z(xmV2+XCf(e&s2-u`7q#RJ={lfJ?8JxkCIgoumI!tHz&Np}yQt}`HD|2e_0Afy=xxxa5h~RMPP*EL% zOLake`&Yw++8+$*^tAIKCli3(oP19dyfs!U$?leXKQo8}oTKxi=hp8C!G-6*P-UvB zi~yp$u+-+j%8TFNxgp9^TPEhLU1u3bNL>BOUQU) zx#Kqs!w}pUX_@2q@N;PDZ*4TdKvmY7pK0|<8i8q)3TZL&3mo^QrS&B(b7Zk`Ny6qU z(XdN`Sus-ItGq^|G6?wzfk_41ybr3!=&&Cy?F6jE41%^@ z^f_z;qg)QtiKbrYq`{M72-s}%_kQ>jxcE^i!oM7i;V}0fNMD(601^PPX?~pyOYq&L z*1_{>1QDBJN$9r)oZ87WhTK`bx#Qn;a&mlD#SLMnp@=OBu**-?xInlxq+}w9@$-=??i*Rq(>UbZ=*5Sw=M7Y1bSBd*^9b&c!CnE^!F4N z4qp5K=O*Wm(Y7-wAQ<2Ns2dgHS(+ACm}g2+>=G6A?}J4ERS^8&B$Y=^ZqSM1Dt-<1 zzTT4FjLe7I&OU$fHd|&3WKEH`7oX5W``PyZq%T%r;2bqLiJSejBV;UVzs|r%S~Jo@ zfs{e?C1)rv$*h~zPYYIk9t|7F1*>@m6VDd_G4)F&Q~Ypz{{XN|<^&lQY{?Im-<%KJ zP4e~i1@Eh6J*?Zu6iWqc4u#udq_Ic_r`5R3L+>`#^I38LVc*6u5RaQ4NIm%}_3jlI zANHl}d~FdCU#0o1S_G~8#f0+=@O3@>MPHmG+$#f(ahddJQ=II2;7l|`TG`9V=s~^Y7c)g+@}zav_6yYoQ8^>1E8QBxd-DKXZ%u6mh`0y@R4>o? zgHN%P$9Vkd6Zmch7~d~NJk=@7It;BJ4D`fT02PM=#%CF9&D_Qq!*2yFR5}oj8m0&8 zJaH4;3NGW@$627Rk}`8wd=l|KG1|?Gau~2w+W-PToyQtWRpWQ)w7wFR9hUekMbrcA zP_`_$s@qI>gCoD{M+$rj5YD!xqbYa{TT0$`QM-A841e!IOUND@;M0}wZLHO0DgBZf zRbEk9aC+o}fr={{*Wg#jgeE>t6jy($I1mnAqfkGvA-PYG6wh({;jqefwV6aACdlp@cGE+z76dy+b^n$r&JwzWfAzTW zs`kXM(oeRO`w4Me$wM@a6?P+@)kqW{+~mV}kgmIH#cBSH(O&D!qj82tr@nm@)qa2> zaAt#9Oo{=1ww@|!%2Dv%S+kag(E;Al)P=O(KyW@wNVS?hYGQTv}+TGK6I`p2mWUFejaY$APC?K!T^mTo=K=dzC*^)*nMLkHYTMZ(7K#qKSBL z66ADW=h4|&v9tUcPtrL8OoRyaOmV49>sH96ALX8z(u7)-3a9QbvC3W904D>M9_cH{ zQedV?LriiiP@AvZ5z@>PEsNAsxFO9J_tr*IsH5`m)t3sP_P50$aEO47!xWoa64%Qj zSAnzcrmMA(dS7oUaD}2PK2rasS_y-+BLCUTXQm4NFDK*7-O6ozP#f3DE;DRLQR44@ z$s7b#RrF8+#-aohfL#2&j%tB z!bG81o5M5-c*$jdB`_08Ex*RtwqNka=vSuac0CNXnQk`l z{P{=3Px#~4NZ-vEXU6sz!Y+Eu^KFC_-q#DIS6~q{gxtY(Js@zR@KnZE?a4>V+2CL8 z?F4uggB0O~ubld#gjyTw#M$a9Jw_`XGq6kVP9JdNMjeDYlsZj5JX$FXSca-yV%QAf z?pI>~tq;q*H{~2}->)xsjCOnV(!Mn^0r3fF7Y*BS0Eb#VnloR?PnhUyKP;7m+gKV! zxB6LWF%Uh68OyRxR#KwjUj9=lwakCOIeykaCWXa3qH16vH6-;V~+PtlFIm{>pM+nHFniEk2bC)%ct|xTH z?|$H+w+d}&@}Ej0A7bC`|6rJR!ROU$36MKK3{lDQb272HT)SUAVQVvQ?1Gw*Mbmz^ zL1OETxhs(K;?T2jP0MN3{vF;`ToH{b0Ed=7>Y!k?#$Q@5a#P` zV+CanHY#kd^PMyvz=@$k0Xu^TyiivC;RYgbr@LNm{*+{z4rGe1Vu*MI;Oujg4&nII zU>trP0sR3us9+p;=(|Rw=6fca{KDHNpui~H|8|h>hl_m`qm`Rbf61PyR9BEx3f^XJ z0yRJGwhUNMT?zm67;_d+@0g4b)hq_G$(lGJLJT)x$2GOdB5%fge< zON|DKu=|0`-CC~3aE3z!o0Rt1cw~ZViBC;V`EAdpeQ8jSG5q7r9dggh)0v*cd=Gq4 z^k|e2NoIInPUcm|yp4J_M}Ly6Lb6?Q)@&9%qi%Pt3p#w3ef2=8 zscwto@yGgfWb_chi{Mf4NsGNkOYMPPb7hix)8m_g8yTuE`d$#A+kAPyQoCNcfHckh z9KEL%%`DUyrn|XEFYf7IJuw?5a$wy`6&YFm-hp_o8bxFe*DI^*QHdSp-hDar2q`fCWW+#i|?cB%_VnL@}chGGsS-dPwdHh&AOLcxE^ z!};g~B5_6y&G(GwV<#>YqpqTUjV&nWF~#2#eWmmEbXv04WMI!-;;d_^zMtHr#c#Xh z!l#PXif_Qeil?Qv<`?~M6fVgbE*Wvk4bGSi^xj)c7`I8dqU9B5!aZYOC|P;%%A$*v z+MOwtAerhbImY(T_0PfLGV!W~aVFjmn|RqVs#=*Xh^lTp)4Pgm-1|6wKU*}gDb6?+ zl68|na$zy{BWw_%iSSygyYnh>sQuAPlZ--F)+C;be8d42CvO z|3p*CSB+dna+bXE3c_0NY755 zc`l;XCjUgw*WSN(^Qv`+uNtp|gUq)nG{%hSdXs^w;HU3YEndGRAObHsz0<+Hr@bj) z5U7Vq2ZKWsXa}cey?4T_=PjD{(rC-1k6XIno|Y@MQ9JSulL^G#8sIq=kwpw8xv}f@ zhHzN!@zyN(q$|qC*3zSg5%A@>%l8Z5OOqyytwvowU23PuoJm1_ zg{i5AZ-ZhDG8PBbY@IcqbEC_(e~ed4AK~nZU-8|y?mB-^9O4_VaP$?S>#0b#p_!!V zF#}B;b3ZrpALS@B&xbyrYBpzu5^_?L30ioNq`efF*GriisJ-+Kdtk-)gs_$QrVv7DI08v0mQ1Z=AC0GxlkkB6q%m!9K5)Pf5&C;%T|e?@MKhm3B0} zCl(Z)qpo9umMMsh^z8z;p7?1A#&S1wFc7Vyi0{feKhOzW1S%f)V2Bfzmi`ARHdKhl z2D~M#$=70@130u4*0&w5=g2~_~%5(K};0XkpeI6YL80Gz&DO7Ei3SlAQ-dIDp7 z8H0{WvAb*1a0`~2y!I$5lDz247%UE66{kz;JKnHY*gIL9;;%9()K%+@J5@KJ?Bgyb zXX!F5Y)GrGxGLTHdV(8xRyO9`y}=jknBUOxwf}6 zeb(uXNS<#b!fK~W8^3j|J{xA+>=Uf=CP@{h z#}WM2nQUzMnDCqJK;i7$m3zOM{+N^gb`~Pj$3G;4CXRo=L^lF&rE#yOSgjvrY@Td8 z7+*N1seq1g=e^C(OfCuuYG90kWVUttr5M?#j?icbGV!s#6cM1f(45m5ekUvSB#ic) z1c!>5ob@{it=toJ|J}Rzl0VGi64JCl@xxENUKv6hIaTOm?LK}S>T8=}B+<9+@|NGl zdSJXQX(ji2>^E_9dC0?FgRc3?qy&g^c?_`dRi{B(mk#=8vS?|_?wDyohVP&d90vuT zMhl%Ic>sjZkU#;9H7U@y#sfGS79t=E22KkQCxOdl;g-i8EEwGMmlQC#?Yoeh3#xWiiJC_%&~(oS5w!Q5mg3rIeA@e4Y9*CHWd)YAQ8S;aI6n zH2+KDZcA<2vzMI5FE#KyAtFf5^+$W=Z7v$Q}z6og>Q{0hC z6FS+AeF_S9rjL)HTw(pvCLNYq!#aAnWcTWzwsO-rC~lAIhJ+xw&8O_eUz+Nt_o@T( zQlegd{j$RHwD@77kYvq%viHZ64L762E{hk4Y^V8L)YWnIEft;Ir6Tu;=DYz7{9Wm> zYX?_kq2MjS+6{3RQC*!wC)cUV_^p;#MJDw!v5tcL<_;_Bmq>!e$mUyrjodFKDNM%% zGph_lvz4G&?Z8lVL_cd?rSGWdLVX2$mfyCZuL5kT8fKnvNJ!qnPE|rY*qF&szpOWN zor3zL_fm}bgKdhnM>-9Dy?R4Qr34oiMjAW9x1ZgaZkT*rcF9_!4wqjA-melHJlg>* zuYe9Aiu-#y0q5~qgp)1+eh+Xu+6CYYBmMF~2m@*7AT<{dkIDdNg#g?E#NN1Fjd*Ac zK40Y^o>&H)TZ0L%+YrFnWY_Z6&76d68}Ij_R(xEhv%3#NCQaQkVfPZci{2Z*xLGA( zT{;H4F|%q{w2w0XOlj$@AVW ztw30WP|S!MjSP7QdEmE9pT`Stla(xfU#1TZopxqNcvpt7`F9IGBO_c6;<1COuksvZf8A34I-$zen_Y1dX6xOTh}7Q9nyzoogL)3ak3d}( zzIphSW>X9M9fIfa;>Oeagw<6yFFIyv^d+*8P!CHH*M{R79>2!KCcyxSkIm+g=wZ?W ze8T_L^Gt{TS(WUcocGNF*Ym7Po6)dE$_^=r5PSt~$}{6OxZD$usarZS1>$SLgw+M| zq_0{{+XTD3+8F+atbtQVDF*F3fSqwu=M;}=C+|e9)BrX0Dg3de`${+XZ1rGy>#n#9 zDcA8TmB-~j*^D32q${(2kg%Qw+F#;0YgYr{x;T310}o27;5b_1ZpR>gEQxreX?api zx@bqTa1)ro)iT23;KBYrVSkWtD5Qw(0h}%K5SDYnhk1iUEFcq$Ed#H_2oaHt0OAzl zYtkpP!l2XfXw<-O~A2wtk ztm1iye;}|wrCa*{mPGk~X8QjeK(_!e6P-NVV;zs!Xn_ZKSBg|u9jyZ*PKNu>6a zB1!nF72q$j_~GYFCT$Ac`^kS3Kd@ zFYV)_m#F?^CQhp5mCakXZaGd>t~4Ld#RvZ#+Fb`;s7-(JC-VJwgP97>Gj$w<-EbWM zzMA##Dw0hp+3{G1g$jI2IP=FePmGMT&9eS|sd4L!WT(J2;}33s-_(5%Z6NZ_y!)c> zQfI6wA`m|fL$C2{q-56h8(>J7mdOG9O!tKg5BpX0018S@e*;01TOzaL#1JTQ)KoEn_x1p zpdfN^5OFXdw{G`0t@f@HFY;--7PHxY;89i9%Id16&qAmW{3PeU3G%7haou~@_cPlw z&=)B&QC(f#bUf$(e~s>Do=rru=OvXnQ(!X}@o#?ahtXW|t2ctJ?_TZo`#a2#Tkdjn zBJba2Judl6^7xrZE!mAr4w60li+@*IIjVjvT$;C^#@JN&pH7c|h}0erx!mQgYCTM` zt1N%^PwR(rYiIOI#JZ|6YyMcwm-Usuy_d<*k`~?lw|nia8j!3^=6@IOv9p!)Z)(*` z5tU)dOkzr5w@Ig%R6c1Yr+Edp{?08Ce%D_eA8GSXE7|f*w5s_KYZEU^P>PiKch~X1 z%`%bof=N%GgjISb3+m~Zy!(@NP2$m(Z0c$(@|AZw!dSfHdtlsO;`-Qn$|Uu+*vg7z zb3OH_e=uee@j>!5&`y-%r3P8BmHo4+n7_o2yJp-p>t&*KY3|VJJ+?>xo9O(q9|^ma zy2P9JvBmY1%WrZzcZOJ>9$-Y0&u~raTwn_eWVx zh8L8t{mqkpOPSX)4v5diEEhW!*NzRESt?||^u~`swkF5-{D4`kuXl65>RiJ{iZVPr z;s~O=DtMXRb?$Vw(3^0Vc)RJKQOZ-`(ufvNM`y4nGt=T!RF2hNpo~|@67}o8WtQUX zn6Bac3==pbP(>-iZP%F2&Zv1VJ_D+aM7D0Tnd9d;j=ua6HkC#&l!Lo1t*lA9v0`I? zY`~%lo}D*sDZ97BwDerh`igZ!eQUDN>LT3K{+etZjTKnO-iBd zlP2{y3nUX&t1o=!i)C`K@)i`TED5G4G;fBNqRF{(!`_RD(=+Moj(t6U{!-&6X?6m< z(3-c}aeCXPi7}!bbk5J`UBz8ZagUot6LOBHmUfURX>Sa!W^W%F$Iuytw1+%pUa~EW>xPX&7C#B=hx___mBPEWjpNd|3a6{|6J+^_RVC{m+cNYz(Mv29>&29T4R)q%$D~n>* zMf!g-|AoHhr-I{`u?jkOVwb0-fDR~qfW+LVIJu&|LK`P1Y+7I*qQt`G_t*5v8%$cG zn8dR9CzRaJvlKl`KqF$?WpF$AXC>fs3ZCROe|$$_<+n@=d~ceD%g8Z$VJxjk4Qf$& zt}pYTKDPI<@as8a4bR>ve2CRWWzFXl^t0R8rIj173M^8}d&_U{H)d{f=0RK$>SwYH zht}K7??I8ThGwPJ5j$^u>K(%A)Xn){KmPL#&F|1(uMX@g(@_ehnWK37NLS7_jyZY1 zwOtWt*TsyTTxj^)vw9&fNM|Z;mc91ncj;u8#TUWS_o+#MX?UCFp}6fRjuBGtowYYG z9r-+IM)F}>OUOsvGEqkWXYz-kFI6q=g~4)fy#hz_ow}YkH=yba-XKRROP(dYC`WB|cHJ1mtCqPX6?yxd{NFaI)mw;3I&+dRkec|eR_ngIT(Pkg^BmOoiowx||ZC_BTf zb(rCIFnObZ+R)ylVVmdA@o^JP_BkG-_A^XsEtHFcpLXtq3xc#>3CZuGd!EF4tAxw5 zmRl{pRofm_&b1`(`SdccPwuNF&L8DaVAL6P>p67uRa(0OlqxDLwKFQF(B6)BgdiGv zQLw7|LVk_?F!Rj)BDu4MwAC0%9do1M8EbQu!K0#_*NJ0yJZ)2sO0Erd=}(f<#-FX@ z<}!~`7vC4quK2CgK&Ipg9TvVjF$U3 zn-Pp)h%R=;puDW~;*Y`$LCzjd+-WO7*o4me5_aoUm5JivQmlIYN~D9p648Dm5#~JY zbSnvn!jBi`;+z=wJ{Ms8#+D4@M+(5re$A4oWI+mfHN9u=a&h!p-T#ZdxBiRz`Tjsr zF_4z-lCA}51eBHzsijN08w4Z-mPY9mknY$e7o;2M4(SG|Mf$$b_xtnqKJNV!zW#>0 zJ9B2{oH^$_PYks3+TWan9F^{b?n5Q}SfoN6I6?Rlr3E=KJ{6TtPFGi5WbURjMQZt8 z;PlqrmS4cMj#BzJF{(t`F&3L5SAfdb^ms>jKxcPi<&5N>M|?zIpX3huslV2ttIuSw zQOPwiBa4<&p3re+MW^|YqzrFzas`|f@8L{r%&=)6i8jKmnrB>1K9uJ@&Lf<235gYI zBb2y466m-lQ%Zk)oL$ivKIm($rB$9Q<7*A6c{Jl<>6XBeBQx;4&ygD{T`2xoEIQ5$ z76wvfO}?yJb-S9PqDf=tU@efTg_9IePII$9lO!QV^KeaD*Ekw*aQVaa-hrJIKcUh| z;^Lu4;VKjsj)1WK=<3*zTKPtxQ!HYk0kD&hulf1JrkjkS)!tP-hLdl!WHd(8=A;3RogHajB`>vjUhBw@==Zmq0|A>!1%q7QqwT|}}Yv}(V! zxicQlxH&rY^7KujykSf1W>+eW+63p?&5v`ZIer*!lMqi^%CxKqFp7FD@6!M%?E8_x zEnL2sXgiQNzrRB#J&iQsY)9WW&6dPSGDSDLJTF4*7Na?GucaWn?dRPM_e=;%wSVKCwIg5ryh_;-yE)nN>{JvA{W)Y<_{gw)&Fd-k6LVbaMwv#c@-FO!5I{&Mi%ej>oeS&Ko2gSa0A<>jT z*()nVSZ6Nz(|im62M~YAe!bD&^+*J4hu1`QVJ9X@DB^Rqa`-F-a_2Rw*v3>km*I|a zD{W%=M^wOeA#PJ7EK7Uw#MriC+`gSPaYzMY7RFp;7GA*f10LNHc*c|j>=Vf5qd5N9 zCNa&k?ZWX1cTLkGFg*NzY)a~X9J&5V+77g|y6(y!*%-E}F%FAlwf2@nqfGqO=4WZc zIG)>OD1Cu3W@T1xMZ8uQV4X2rdgSghifrQ^u+!GH8aS9Ue1>2wkqRbk4m-^mk1}Wt zeoz-D))aZT?RZ`PY=ilAtW2Wg<(qnch;dis-fT@79Ikss_4uAi{9L`!GE_)$zR6cnuqH-vMDzx^l zsRlE*MGw`#HcB+PeoKDRZOX4hH*{biO14l`(;Tc0Zh92%hp=F+eT4HOdCdxEdzQMq9c=DC}wxS0MDl{|X3xe@@fy zeUPJX(BcKAGkMkx+x&TiUpC0}3>l}=LAa0cJ;=v}cd>wKb8ml2PeWH(Paw10 zUfj@c0Z#(xkpW)$I366HK&OH)m%2Z%$2Kz(v%{nvUiP(Vp|n%QCWL&#{RgoepG|`h z-KHhz6r11VZI6Gaf>T9)w-LJzE*wgYUGS7HG<`VqwQVx<;gOuxqnE!)7rjf{A?I2AMqm$F%g` zWe0keb2b%kP%tjAO-ONc1KK`MI;ny zl;N-7$i14NN{n8Z(iD1qCOND9;0axOX1S}*iD(dlX_$^(XVrnRso9`)tI*FJgbg3f zEMvZC4DTLl9j1K&%`=EK*Of8hO2i6*Ro@8urgqzKqfk8O_+}uQUDl9YQSU73XWd<- zYoQsfxEA7e5&hxqL!8d0pHp0qKTWgUp)J{5<8!1~Wu+jY_cn!REA%%%*CPjxT5OWZ z=h=|i6W!&Tc#QjbJQJjD15l=^HuH1m?SOs&`Pe;QfYLjHNORZUv zJjNi!TIo;aSG}Urd-i)}pm2)9aGKa@KV%%w+m8>A!5vg^(+Q>NwLoSRf0XGVcxmU- z9!#dhhShE@I*l>1)O??lc2=^A-<3J|kRfza7Kagy?xNn(E=;^rjn|HMU*3Fc6XAj> z`qUpnNH18}Wl{1gM5=Fb^YL4~*-we_Pi=yLEd(>44$3gko@mVWL8l{%TyB|MTfhI7 z4q`y7t$zb~erYVQQ1u@DWTmd5Th(OP7PHb^gK(R2+a53u)_X5*H`JLrzMm}n>nYuw zU8S@s2;0U*P-pHicNjkN!ETMu`+{{^aTQ1JTK}B@aAWSh3V`tPZt=Q(H+Nbbc8%?w zw!3`O>yPr#AEjFW87JsaOpPf3QVJe+=CWNJ5_nQB&JLo#JuTv;u zXk34T6pnK-jzd!zw|HC966jvQZ53>p3Hg}b$vER)@Py8jxcW3gSe+V0g-Gs6I0j!M z_~s~yR*!&^)S`HnmA73pN%~_wr%wO`!8A=3a+p29XuPzL8HWR@K}ABM>}HVX#(dm5 zyN7zRPelMDvz?)44VJq%HfV*1 z4b%vzn_pXT*$TXCB!2~RxHut^B)K`_K5dw{l^t{jQ7O*k?R$td#s9*fQ`k$4GtV4k zh^z8Nd~QT1;n|&U;58(OLkU3P2zDhiR<{L~rh$fyYGY#~fS8V=e!r4Pth(Q`xj!^LeOMb=y8m~oGz&HYiWv_Y@b=r{8 z6!v3cZ&{R0bU_~Dz_t5M!&q4452wr-QRqCg2d@25a@O!>_MWeW?Of^ba(pq!%B#wm z2cqTTtcWL_qEQp71(ILwdymxnaNbfutQ#G}n$1q($kGDuz6ngN1e0;y?d5OMX$qgtY zD_w*w$K>KMV`vRZm}K+XLRsa@m+2Gv<{P}K5P`Bg0WTHF<_-ggDe^K5jKS)XMIE8V=az^$2xkCpZ?h+ z9Ivx|xM-I&HPH2;!njMlOwW7TG=WGK7Y#VHX|s$7Q|e$$il*C}OZM6y_gChRzl+?K zHtc(zuO&pd-TU;>pcRDPA}%%l^D6U2b*`12s@C&(Vt1+l6oQjkE`^?VHYS?N1;b

W|5KNl8Q-RubXV^-P&qb#KQTi^WUbZZPjMRNE@DK zIodS7MaIc`v#435G>Ny;d&MDDgD}heeiH`?4lj$qk*(qd} zk=oIXE*w^QACw)A@w(Rx08Ba?2Wo}iA~iyoq3e)oFlG#MZfODW_*A)0vwfRjVl?KZ zj;@P_YFleAeW{je^Q`A31n8v0MWo@+lEf&9k80^@*(kgxpEA2rvJdpk@mH17unQHk zz13JDDazyK?)Pc(6m4u6D%@(&i$I9Q{PrM2Olh-;Gd#ta7cP<_)Dp@(>gNtaA)WN% zOHsZ}K3k=~0XC2)`C%Q*}l^kd}(NFHF)sh8>y zLB!kr+n3wFv1-HQlDXcXVYhWObVZV-a9d5BZ)Au#t@jIgA*!KJ;Pc%XtFlu~S|mHi z7m0lJ{mw|^r%&h#6V7VfTJ{IoRtQc3b*wQLRpkIQ2Jmm%Uw>FRUWh~F>0)8iaOw)t zAeNPq&>lOFQFc@3&cIs41RtctE6}zVn@}VX$8eZNEL)wJtl)AmJZuqHl$9`i?g@W& z(34JXs39fmtybY;q@~WsoxVYbo zca+k7heyHOY&>9RY)?CE84!oFo|U4P&6NXjOs0fWGMQ*nQ+niDPVRg%1tSAn(ZMDp zfz&xK&%c!zu0w)j4i&(E$I-l;rm3s@gh?4$qMX}K7=E4V>4%?$(;#^w<Ql9BgItV3kh*@OFUeHs!`LyA+%4xv@mUGjba_?nHN=**7~)Lz1v! zdtYVC0>w+nuCdW%sV-3S@I6luIJx0iC#91ZaoO<~-Y?B=L0Qr5DF-3SbfO1@a1Dwsv*GhOGKeI} z&~F|41kkZve#4XVbj0IXhP#AvUyE}rvk&$FfW$+l?z#o@B5?bZjGS~e(@?UYXaP`Dec4w)D4m?gdMch(+oWhP6kG1Cy81r!+0LHI z)gJJNni{0X_hK}o_oD>1*r3Sm6(AKM4)|<33^z?b{84S;rxSS0>ZHQ&HPMhad699V z*%XN8-$+V+FSc1&%+b}dugt3|DYg9Msv*`=j6X@j570wNuas;l{O}2jjP7Z9#%c*y z#k+yz!9j)T!Ya;h!gS=Gn^YXJ@+~K9B+`>70PFoWW&$z=zsyO}ZNi(8HOD z@3%&BE^1Nu?05rR}`n>*oi6a?*uaCk>yyTTm6K} zR{LYW;h=Q9?iw-M(YI6Dpb&2SvdpypA3%*5u@k$b`;+sv2L#D}k*Z z;8HR(oDN0(#(h!$7gB-p*x%!Xs9ayo{)V>y`T-+0a>1brR0LcHJqemvmi3sM~ltcfE?4Jw+FWX<6^iuw}hkjBPTT~d-s=(d& z?*S1$XV?>J|B)i|AEn>Eh+6!jdNexr*Ks`}kwL}c;LFASk3P^7WpF^qJ&uUK*77=R zgigG_bPM34e@R1o7aLDyv_A0Hjv7Q~S3kpkVX^)fX#o^DB_Ti@6bKLgOWG3xnNe(x zwReyIn-fbQ_TBU4Zv(=AtuvgVM5f(`1oyuMyvkQ5P73isqyFn#yh`BokkHq`{}HzO zOhy~mX|4tNH}?CKPH)Q-{jS@FGB2i+S`Hl_a|+gL|GTei{4OSbWt)8!7};_+P2)f1 z#hVPQhW+6`2>zoFi6^%61%?hG!+$d)%_)N?H(9^_wR1>4U${D)l)5qg`qo9*h!MrE z=$F6vDEd(>k1s6nClH_cmq(E(fBm=HfN}EIJ|H3a|0Q<-Tm5x<|M#;$lD2{;M=Io5 z4TpO#HhBXU?dJfbN0|R6m$Y;-Cz{*ZHvL)-v#VQ+7YoLKB#e)bi(Cvx_4f8gk_%ae zBVn2Rn}6aT1@xK&wD8%SlcJB$4W5Qhh1-E1|D#tQ{wS)u!+E2_aX2E&9zh}EGhJyq zH+?VQ{y&|;Km7PC4!Q@b{Wovri7?PK2N(@d4Z3j8(=hfX_tkD}+C(g|_67h<0ZH=I zcj7+iSTt&#n3w=O=@2T!4r4nOM`J~WG8b;d;XO{If7*|~`UyC{fJ4nY@2u(*YuFYf zboH}s-N5K{d)&y~!lHnq30r&4_x2j_{c4YTnIj62J32bpo3AQ4B7Xp&UH!#&(VEx! zd5rYe^75aKX&dQY3X#askN#UW8XaIic1Do60cOIY+oE?C|K!9349H9Y<^nP_ydL9- zTyH6|!%s$n$vvgzPOpx?FS@Tp7^P}ey(5~G4>{NIJ{?ccafBoyelE2E3r^#A%GK-* z!KPXNp3!W#C~T~bwDsYS9s>Awl?bKv6Rqsg@$o^!f$(wmtI0Bb-(d*?kH%^z+w5U~2;A#TFVk3O9n1JfOFLjcQOAUwi=qvyH?Dq?%~eRVY~p%AN%qf;frOlK z0`#wqB4=f0zFfle?z#xa8KUxk~y7IE4AP58kuyaZq8|OBSa40|jZ(0i0L@fE*U}DE&)ch1{$d}Xn17AaVsYQ1K6Zu?sr?Mo%e15f2grW@G*-Taw#T<0^ zq=7E;ck&0oB2s`y8y_DJkU01RBM5`6-hQbS*grtHJSkhm#l+eG7QC1;jsgI8#r_iD zUq!`Thx>4G>X#h2BLK#LF{CCq!Dr5|v|oGMU>UZ0soXi?+H*$9!CGZf(P7WFLE5ed zU5>5kj=7ufo8QanpfE=Oe||e}S>bzoLt@wHF!J?{R3vFkfOC8Q0gy~3%MP#3$&tXg zFZ}=+haag$JeMlb{*N8>r8&Pv{ONR!-6C{SPw-to;aYFVe1lsQDZk@#J0vYF%@OMr zF=b*mut}WgErHB`mU&GZ(GvKjzW%}=FhF-J`v6n^bhqj|Ah-%CJT?F=3HmJU;JT!O?H4qBFO|N#>U>Nr~nCuw^MwAB*O210p0>8`Adc_Bxw?499Plv z6<0e&jV?s}k+LI+F`wOTPkeIdkzKtlgRoHX=_ks=1Wm(Le_iFF6mK0jTt z2tTmC6XgtMX)|fLZ!Pesn~VI_|Wn4 zJvNc*bSlEA+?NoA?(P^G@AUj9;}&tNHLU4Jj`S*Z+L2_!j)IAsr72*(vEyXF?($Z~ z(|N}e`=QCvm&^r9)G}Yo-&=vpKK&5|X^cA+TQ_ySR**gjtFOeaGuf=s8(5 z&QRNoRUVoh9p%%t^$8#8OkWV@7Y*68aFC^?7DJW7#ZQP&_|8HklvVzXznqz2F}+C< z(D?veuN?1`X-|iNXq%6kuQJ<9SAT<6N~zw=oh0hZhtx5xjNIA{yNq&YH=Vs4+nZaB z6@U1GlK+pbAx@RpO{Job0dHxT$Ze=-$CnlF9C!BAAsm?;)wE0s`hAH}^8L}+A)G$| z(5lGPLv0IrRP8V(^Fo!7MDYC?c<55bYJR!$6URhSI0Kum&g4btVy@i*^T2pc$MDAM z(5=+w0}M>fZ;s;r>wo zP*Va`GfGJE1X?sL@n=?BdSu^TmktS-<)>nVxd-1Uquo@l7cF{9j2_* zlWH)!P34L_V~dKu_K&7J4INEIbqW}uZg)_oT3qScM8orH^aDlEa24FrISWIJCR@e| zMpqY+#{*{f#=?CI>*0k@>Di{UjkbvEYh=$Jiv@+3&g|*BR0l`73t@4q@R@o9cd?8> z+3$!}xibs#{UHq3=yaZ7=CWgRv?M|moy*YFZc#<6xV;4*4eD&>`D8P1U1*KRAic_4 z6D$5azQmB&nnsnKp$b1pw+p!&POaMn10%!YOg@XyN{jd%oaw@92IW&LlSB?TLGFkRv*Jvbx!EeqFvmbWY=vc{ z<`01n|0VG0U0|f^+;;FuP=a7}=*wj!DQemX1^NC`Q`-_zb{6#Y`MATKNxb(!y1~PT zZEO1EwGt>oJ2oQ6mz45A%aVz0#~s>ha$L4;T-C9SgG+bH(j!lz$S){i0NV|=WGc&LpZx|#p3mzWo!CFMrTTXU1}32BoLr~K42DqS}mmE0F4zKU{wLto4oU|C?35>V)2yD$eC3!^%t%X&U}$KTu2IBWcRtvcxru7*NjBU zAT~voa=AIQ(5+#0BdXM)5lm0y=>;wv3*Hqd`9x^f1or*}j1bp#NR^A|U976>pGXT@ zV2A9;#f~fNG~MxtG|?XeKI{47h<&ZSkV|m#Wxh4x45S2r{$7?NHeZ?;7 zOE?+E-s=8^(fPYnf0Ytd2!pro03L?72hh|mWKMnDu^r0X-hM2SbMk|cfnm`aPWYt( zI^pQ$f6HWPcbvECN8lsEjd*rUA~(gPSk;z;l|!P~bH;rkP*fIgCtl%$miEr*4ev<9 zA9niV6G9l?)Nlz96o{Z=rVU%$44CuH8FCa1b0z~*#sap_$_Z3c;Q5%+1PrPIpP*D3 zF=$xKE`yG^W=IqUKw1l3&(k4;#L%tHou~fFGuPNpg$7=X>33CO$%U63?i*1aUfIx+ z4to}qJLJ#2?Rl@E>?N!1W#0YmjT?xgS`P968KW-X6c$?o!wiO(h9;N2f7So+)5ITs z68MLo-oSE(wMz6RgbUY0-hcORJI0nFmRH=|oY&Ff%K~l33IuQ1o^UCAQkAS&Cny^W zk8jHkaZOO|o3;x8T7m^Sali&;W9j&=EOmFMw6mKf?e0G!lzXmYw0y38nTU&)wn0di z_=0g!ZR}uQHs1L$&R~Me6nhTGo?BUYUF(k$JjdF^k67RewM-)~u+pYIHUp(y-r+%%F8^qePbQ#>dDnY&E0|D$ z2X?Yvyw2C(Mao+Og|s1W`D#=i>m~XlA>%~q@<;|nGMALvZEX^3Hn^29^e#D$&NWfSQp9IdBR#2ozt zOtYrj=cXxZ+E4w^>8^Nh#Bk)E2qX|R9GG82n8HW|Q}@l@1m5!as+a(oIBuEG$VPpZ zKH0?zeS-ZtJkEa)4`?Aup=Fbowws>@P6UO6j8W*`^H!nIq1}HbXYS0c5$WWGRruOu zv$l7Am4|~h3Kg$vKm}x|Z+p^0iU{2%2^RVk|Nj{>=0_+*!!j(md+COQx9ADos#g<= z63B39ez>u*(j>eqj`N`^@uw*{?S$wFdM)?i`QfoSQs^4q@%5V)6uREzh*@S85;?-J zp89uk{q^Y|k6szb_uEYay4gjc{SpOA`Lo$&>j_V$gb1JGYtWJz(l6 zjn+qF_eeYx@tcVX|6!sIp+vs%aid#wen_5Us%<)z6a?EY*H&#-C+1BA;?Y-N_5}PE zpMO?TTm2mV1PoegaQ|$9=EfVJ@1f?i3Il()&XTGd5}j7Lbd3OS+uKAodz)a=g{FFL zzSVeR&_tO(c>_CspLOEmRy59Z{YTjF{uMSLpuKJwMoKLaCg3TD-kd(L1`Dn z8ja|#C3YIEIE!09+OcM3hi1+;IMmyuF6Bh5vu^KLrs`2Ha$+X}85;yQ(lKk)v3Kz$ z)!o@>5M4P=t$}ED4rYOFth2CcR=o5T{v&;7A>)R7uW>DxVv|dBx6x6(;q#ZkyF>Ze z%-WU*o#U{m`s9sp82ea3`)*5Mezod$@!F7<-QvoG?B@$Gb8sK;G8$v4MNJMEuFDD} zBw|+RtHeNl%L&N-PV`pIK@Nr4VDI$hp)8`cH9Um{ewoNDV_PHWpp*Ib1|}mzsuEZa ztg|^*=4+Cgv$j9mS zj?GLPTS87x5!PiYzDMNB<3{9N-!ciMG!AMaCMIVqG~vp|C(9U6n_aAvCxA`fK^L2# zz`$bf;reC-INgKJEkO6%>Lwu4cjx$2S;JeCNLXoe?F)-+LrGtB#(L7CmYWHj#~6P6 zXe`q^$IG*PC=E9A1i#}~Q}$z+#Lkv|7jHFg9jJW@s6pHLejp|7JU&U<4>pt2QF`1% zAyBGP@-DI^P~q#V-J_#QK`8=(6M?OvFthLLoO!)vad`;-5sm&Mn1B(=t!dg3c0ZjOdDVG`LDU%^gdNUYWVDk``~j&LYRW@JybsM6sj(xR$b4 zGs35vVod+cfvBeu1!(z5uW-iTvC7VdJDDO9oHX!ChRE9ccB8vG{%N%vK??9|tqVTK27yb+g)QiJSCP z_n>$$(r1r_V*HmZZDJ9=cA@+7 zE#6;w{k6Q`ks_K%d0@9jArU(QG4BG?7Dj9IhqOM7Ksjr=Y}&Ud_<7EcEBp-im<0rb*-pN_R-!n)yhiK3MDUS@TsQGo!8^W(&*W+K|z0*pAU!tV- z@BO~DeEzvWwRBQiz>}V8IXF*O&iFQK9>ubiT6uTCMf`!BBocxI{HTxx3(DLyB`}#C z^-P8WQ~5(20YI*cWW%zHGUwTpE5FpqmWpab%rFPOHteR8oAxv~zdkrwQSL-o|j$|f%Vb{(CC0XHMJaBYAL zl-ED&)$LhXiU2z9bLW_zw3LRaQrcWDec{z z?T6GYcG{IaPH66w=qtW|pzM3uRTtZt5-+T@t*^;W$O}`9yvW5(E+qFa??(8b17Ll3 z!y<>3&bUAR1U|_8*paig_Ci*&>yn!5Md3wsD#qP-lyiSB0pUN+wai1v2&rtpUKK%dRgl_UOXL!)G%05u`_k`zv8`n*q<#9!2 z@&E`Fd}smU$3a?J1AsSO`M5B*u-ud<;~}Z1HcksXAVLZn7+rIq@ORTuV`khtLs(sKFLymq_@MBoBR{Is^|f8iAxxC`e_ldfY;g&hJsmd zpjZNd7>)xOu#R^uV9q63BM{HxAp##)CV!$38O=jsRR(aHG=cwJpVU0sPRM#XTE zZ$|_#B>lqcycuQHD46|%Ti+#!}tI3lZkmFPc)5G_7 z)FK^@ct)F0nZ}9a35!p0HFKs_TUGz1wd4#?%rViJ&r@Xcs)V{Qg3SY0UX=_0aPfh3 zo#rTQN&A;C)2&&CCs{aiIGjp$?vZZ<1RQqo@-#dqL6QbyH-CuUuP^9~`lgH;yuAUA z$y`=E{&`+O0r~u#AOMmXXX7Ys`Nje?Jjz-0s-M+S*W(!bjK>%7jWdrgRAZ{qzu)j@ zu_l@qkun8~%ka|0q*T8(r>F!l-}x{5ozHAaAF9T~oYQw5zB>TE8gaVwQp( zOe*lmTEzJdpVD3V%yIf1FC7AjQ7aAaJvqB_?{Su|y#D}j2ch-9%rX4EQPl&HA=}cv z)wCbKHV)Mub>aLBByIBrkg8AYUR!~;UG>ipGuvydy4Q8>8g{=Pf`DAa(Ru^t=OUbi z)f`EC+l6%sDYBLTNHd6;_PJarS{xL!f06neK)X+(9<+!@{=k;|byI7Kbk=69VdbQ1 z)r?VhZHCl!P6=p7&=h6jHxP4?2ZksJ+dzd-?u2V|@_f)dzHGj`bH8d>g&gG_Xqf#X zx$mUzD^DV5TXO3nP$ZHLM-Dk`7pP+UZvGem-$+|%5`SJh^da5je;EW^)*o5`l;Tbz zoT9~n$3S}q)l16C6q@G^J*%x1Pn}`hyu47?^b-O2&fahWgXD3o4&rUX0!k7oJ;VUu z5i4I=Csn7u8#ev5NCe|Q9E@bsu~`!eBX9j8JZB@v^40oG}ft7fkD0zVx;cb658{* z;j2I$+zykjUa&fuaaZXgcz17NA`ghv6@8u%8fkzRDrNL-c94169J-OYXJLjN4D|wq zp!IFxrC*!YS;^)vRCZ6NU&Yp_+oA~9J*^C#VeV&yW9>Sr1Zx9%=VUs%MMzs!F_=|MiQ`2XP-{O&qZ>>9La|2{`3<%-(*jZ#`jih+ zSR_Nij8b_1i(K)@sxRC#l~+2qwc&Jd=jB{is<$_Ps2$}V67zcY0g$r%-)>`_0S~nn zIG$kb!5LyQSMG3HH(dwoX;;8qA3pGncm^0zby|B@&3SOwP^eR9Yk^DJw#OR{<1L0! z=^4HOS#JRO)di4WZFQsW&B8rMJ&NG8N>1@Pp(<7PNZ4(bZS}3}uIb#hWeb zy+}nRa7h<7EJ2HDJsKE>+dCRhhqSgDu1i?7mvL|IY-!q-(9G12(fLSA`XZWy63a2M zp|I?A9@CE!X=54|4T#a)2Mc-45MdU{5+#SUJfXy);PCwYT>y}#0mpZ)!Y){zHcB*j zb+kthBNR(}V>&raaoQCu6LM^do3AghXMdW54nr$3vfOQIMt&7!0X1^L1e3|574FVU zA@}=u%n506ie@cs!spbj1oa?sK>6-8P_lQbrwAuwimUwC$TuWM6=RYg;MaX9CiZEU*o)#7^xY8RI` z(^nCl_euRB*W)vuVK|jJ17x~FLb_xFvU%p-U<`IQ?;$NWpxo=I(!sMiB3mYFI(R>v z+B?61%5u?rosy3R^&2}Tip%Kz&NmmS?fs~svdq0Q zr96Y~VV2@cqy-!`c`NS7IKsj#LVGT1cx$wtc}`+oaXq?Q7heSH1}#A``s!73W8>y! zHFyIv2SKolxjJcWxz+lkdYFRuq()qLC=hP)oLAYeM#&Be;n7TsF?&uVhl?M`q=N!kT5Ajs+ylr+b-VWft1`=JmPyhSAQ9Caugk!>Y2IqzF4F zN32VOfxBORXinJaiW=Pd20d-u=z>hAIB8*JH&F(T-CH&~*$+GC0O|w01vhkQ zL!C8y3UM4m>@HkyjtWDj4rq%~XU?vvE`fsIG(69TwOiA_w7to~$|ld;CBhc1TYs}I zEs>l#H^1^n2Z44Ai1zEXeZ87C<1qj^oB6tafSR$m;?oQxRf;+m+By9`{gaVfez~Zm zDEp1RZ`n%KA%`NV-1FZIyb%8s9!LX5so6a+kBEa%`j|`{Bw&ln=|>`AJ^z=1`5xf} z7oM(%%pC2GfH~6#nEXa6BVc>uIENaRV{3>IMAa$LZY8Vo2qD>&6`InZ<3&A*>?r~& z1)@^-@m6#BY)Td`nV{je+{wPs3oKrq6L_u4+ScXd%n)7c$vG{$0T@ZVlx&O|9?{DIYi|oq^rJ3|h_IXI(S n5|ZakA)C5SEMyR>p`op~Y2Qa$Mf$f;5{Bsp8#&TA| zXKGTjg#^AQ2^)N!J`PN)goDiQw0&L9h-lD?tku4=L>v!~M3jo;P7i*H;GdOgaD%&c zIp5TRRaF-b`x_eed2Gakfww=^+rJ-p$Jif8k#y!BRiMX&O)x|HdTLlaw2M0ZOoUk? zS!wB41yYz&NR0+MoX0KNR(}V?ftQZ-~0N^Jr)_t6l96-wHf?c+A!Td$rSx$ z2lJSjavrqs7jeTg5=ZKDgGJANcm@$j2sa2f8;Gi{+}H+}!p4G97gg0(0u}l1jX(C( zGfZ0RCb{}s^%7X~l_lC-$Ia894F?uRY(GmWf;yRBPMe_3*3|;9wt?mx zn!C-k`Ph{Co6n?L;p{spuPpLYg1S}TM@SpKSvz_M)#%sk2~$(VrG9>D=wQvrPjx2@ zKvPuM1r}V{)ZIZXdzWIA#2DwLHfO1oEs1S$UK=^D#=B;<>Jk;bZ$jCywU9+b(9ykc zqoUboJNDV1Td$S@NKR`d>^gv?buNKdu$ z%XIZB{;|x~fo+A!6Z#q!hPu}ZB*V8=$T)dtgQNTUwU!Qfq+doZTe)lsGmpV*NSK`D za+<+i7!C_${B`8Du{qBVh4#I~cJosv1lV1(#luF>%TaA9oLp+pQm?)Fd!eke#frwA zdpjsd{s3t4+giq&#^uPsuZ2BE?mSPLG{u;RFVUtEZzFWl%rVu2&y&pp1u(s`^yW29 zBxXHFU!_V5+saOa2Q!|-rNATrF7hXPqFm_!w3j_Q0l>p zmmHazh@Xru!6qaH4JBX-qw^&V3j!i`T}W(A?XBEiqY>;>!!;wd7jHu*%UF=WP4Ku1 zlXpUfXtb%&VYTulJO6~V;{gc;@@wl%1XZ3FEPiLB&w<*wmsnn?v!i#Ygo>D^05fc| z8y^s-8Tx$+doQZ9>SO`m2sAPc^@edPh|ff?4e6}?|_BsY$CkOf8Q(d+Y@`fv67h z$_?uGT7irl+a6wvy~235x~Wuh+9_JnPyJ_y2`Tj!DJS(UKHV!`bcfDBfo4-(qM4cb3DfBM#FtR4l@Y8tjE`<^unz`ow^T zvvn13*j%I0Eni360r+1;Zd*4cSeELf_3b7nW_kssBrXqm<~u9}<4$~oCaW4$wum@X zG-1OR+DXcu6;8q{Ic`PY(Kqm_GM;L?Wn}ofw79lzyRMs2?7OeCYCwypmq<0(`u1kr zUpFI~4qs!n_jU=3=QTiWBPW`TQ$q@btYb5y$P(IvX(7Foll)CIt5|vXVQ}Hilco_B zW**Q5AChd>XZ5S%xLH2fQt6IGX?%~`aMONd=k^H7aF@-LZL;7!d$p1qrHc}4Q;pKYqzz10HbNpRIBI|Gw@GR8i63$5QXV5J(q(_YSI z1M^`eqqMDziP0KdtBWT^aC*hC1p2cZnFGcbDixzX{GD4(aJ7UmUel1Fs(o9VG-_V8 zol&1(OYxV`Hv=v03;LEM?jF!7O}B7)l8Py(=MkVsI89Am-gY5m3Gz4@RUJ7;Ohgr^ z@cKTN%ftpWx1_3B<&IDeMZiM>z4{Gt{W-Vho3(M36V9fx{ z;TI|Y*kfkhhzl_~(!BA*sX)RbDcjlPSy;D=&|1WD=K=h>C<`dEJ>9qze~CdntfoSu zivSJ`igt=FZ*STZaqltC`S~P^@U{a%kTH;eqfllDL~D2lisv1Fm}HQqBP+DD_g$3s zX4KKbgyHORqd{m9`uM}74O%oZb@3r5LS<2W)(Z=p)i7bYi*S#DIcj}Dr)44`)Zu;m zL%L}IwV=G@o4G6n%`k5;P(|+e$tI<|-`en+JD+Xwu0^Sl|9n!tqj4W>r(FH)I&muU zacM9knd#t76Y0F29gsryA1-odQA{>lJyy;OmtttYXmb;_m?qXm6L6NF1j2X1?Aecn zSGFF(Y;TLvA9!GxG#fP=`?>lSbe(@5##ixGR-EI48qV>Odj%$zP;H!{DYC~0>K+#C zQD;jXB7e7Y>h$Q?MOQXG>k-=bblr^H3v~I?5@$SXlu#h0{sS2FPlzWck z0Ry#9QnfWaL{yHD`qx$YCLb&|HG0QdfWQrSB4qM0-pamn8JhGg@|iaks4xqTVmM2k zaJsvb&smSl0n1-hJ5hqVj{TJ58pHh3Uc!-!mf$teC0q#kQF9@Q_!6 zJrrB~>4DzHfg!$fDv)Df+7rhmIeW=m!ik<`IjBBGuga*GJLUG(v<%08ha;ec*%vi8L+_-8F@N4^Ho~SFGP6Q zjkzV?x+Er>3}=Wh(+eHF?785A?w@v+)#`01c3R}DZQTcav$#HrT_93xeNb&}?G@+B z#KkB;qb)XgK$Px)A4dVy6++&VUSG{f&br{hT$PJKDXeaL2p4X4wz7E}E!Bs>O~)%f zK2P638Wvu(Rn1b7m)%ro3GdQM-3#QNHlUs`m9(zw{D|4Rc6L|ZtGJjwXUD$1vEUX$ zWi19ISsk#D>ViqU`fWBivj;!raA3g0<_ENgFUdbppEamRc)=#)C#8(}vTv^pO$T!M-OAWs*g`8F;>2W`#tSja z{@UKw{CJ`!;U%Cg_uU?kP#(K%-*NXN%f`V3VHJARgeGmzo<&(va`W)sGQ#gM4hr!$ zTT9^U#hwS+mS%c(a|x@Qk*EqBehwrb&pXjQI@;Gf-Qte+Qqo_}>4AGhYgr5m0TPe&)G z`I_tb)jj4hq7XU1ZphCrp=%%Nhtg?S$>L=LS2y}s-so|%BpM2khpZfD1dx}Ya-}}}vt!yDqAr(HTWcBxyl>h37*N_fhu$~t? z!Vd&Wa6&6TDaTwh1 zw-J4PCy^l3-WsXdR9H~z?Tf}p2~RFV8WlozLv0YyvIE>3(MqcR*77-Vj)dd}!9yo- zNdmIwrW+^|3O|kat?uvZGwZ#5Ee}A6T5Yg5^F86+_ zPIJee`Zm|JECfZ7ET3@#mF--ZE`0Wl+`V9uH;-|a?TdMjB8aoR!3Vex--1jFn?_fp zlyC9qc*Gn+iVAIti{gW8r4M^IIog08;^nviT5!`0v(_Y-nbo0tS9bQS#5ZbE4d}PJ z=bqnuAh#QmX%nN_Gm>Q()9s>~Yo3QZteBs{e|g~;d;U?cb%yVvC9nLwa-C}-_N_pO zuq~sj%X1`Tv_EM71|=X}U-ICka_`M{76*yNo`!C4d2!u7N5q$W$;-CQeJL>n!IjJZ zf7*NNpt`oLPm~CUI|O%li=3Nt z&v)PJS6yB8`gL_Rf20C??X~6_W6Uv!{)VM${H3%}o^5|IncTPh#I9g>_c-p7^5l^- zhl%cnjR*jB$(n~&@`T@e$6N{kQx+M+)R*wLy7i zveE?6*JY|cx^Znz@q0p0boYT%W~{8i1}?R}?5}d~e&mj=FItdqO)u7Tq05QNIdf=C zN%AdKc|zz%oL+o3`*`P}uB|2Q1cwd@ax^dD@O+g?$F$_I`A(*4N+i@!$lWf5J$I26N?an@KVLXEb?6q8;7N_x&nU%qf>A= z(TKi?1llVJnMFP*^DzQ}-MbuTq(b@fs022Iz(KYqvJ-?jvtP@n@522;a8_TFQBS`> zdlzBAkEvokd}P|Kwa9>1Otdl77NBlr6=o6dN(q)MkR^KW_8AzFHH|)S5${#XdH@L{ z5sxWg{u*waVO-plPKJ6o#YD7w)al^jP~a8Z(xZ$6L8wop8ww5ee&2|$g|)P{MS0!K zL6m(vI5U!9q~~0jWQB5t{*pp4!kfr}z}D_vhN(?CVNVlpfTl;WvvpJ=V{Dk=q(%6Q;UTzr^Dk z*bg)~bS9EAmsQ2!qZl1@kI{C+IWHj)tRD6axaox!jnv`W%W&IeyvpDI_)do7<>aDs z_>dfcurVO9wd@$v6dfU-o$g+=Q3a#`G}^Ph*TMC4)!$p^=-GZZX>4Fuj!j%2xKm zbYmaK3!+gjj9`U2V>)=j!7O*NL1E&|NWKh2(6NLJA5m91Q;h2}8Di1#tM0qP%Q6t@JZlanf`J3dPVUlgK7LuHqfY=}AFo7aWbc(h zPk`cwR(+s={ZRsdEcc8*kXd=`szJv2E4&EQCPWD!P?ZDK*#CnUin2$tX!UynH*W(f zEdKQz;5T%Yzn}1!UVr^BF?{_0|FjQZmRPU%8;E{-HN?~Tfu`WGR~`6y9@hxq25=PW zdcO8*9?K9#_AO8R3%piXp-=06LK)~+I(}HRkNnTi8r+Z9fOelLdY$~rEp*Ye-y{WY zh^}%B^cIbej+SMozcwEw3l#pH^aeg5&0QP%BB14>Wn1=9$U#VU(%wqV2hi}X=0;J)8Pflh@e}Y048%SsfjRiPC`WK#hX8QrAG3|Prl6+9d@zg_y;V$gD}D#FF|wdK0tq6lC0Mnoax_Y1IK z|5=jH2RLvGV}nck3s-=eKRZ6wISj>^60mE5e)}2iFE79}Co z$1)w*zbIf+U8=6$&NVms&Aw-he>Te-Q|6FRjr)H(?klJcyQ8Bl^8&@_a zr13;#TQu9*QN}q@Limx>$kqQKnV5<|U@1Jcyq|IcGJdqz3`3wVMIn z(=PlZvFAuyn><(@C@?waqqkY9yel#TIQoc$U8rjrG;?a|I; z%iV|VcN7#ZLc9tMF0bLhnOp;*q+U18(Rv;>nmy+fOxk%v`_1J#qX5dsmghrOllyx4 zk_A&{g1DFoNq9sWv{`?p`K_MLWus(I{BWDt$jNG$xhZ>W@j~r>xK+IKU)*8(`>DXd zA7k`U{dQIbIIyM=h8?C6yH${<^=Ml9&uBdhCSzung4;Ki&%4;+zy$;6J8A;8TZDj5 zV^`_85XV+&+5W5Gp{BZez(Vl1s`LG<|CD9?aeqI535}|;xz#@(-6tB^whvvbO{Xn^ zez!Rdf(37^iLivXiFLHEX8UsL_<=g&!8VQRE!9VxZn)5$+rO4b|7VH4*37nAp@R1f zBdQ~J&77b5E-W#ajFFG-<~RvFA^bPJ{(9g;`~|I$BTslRY(;~m%@xafo7issZmO%G z8@%K|3a@AK4MEc2>Tit$r~XG9Ge_x*Y+&TiqTmYpg9^$vts5(yqEnQiO;tam&B&v; zfuTxy=FXZ_)41;mWJh}`Ec<&*UEsj1kqY4vd+eLHv$%uyf5~9^9~mTVXzNU%zR%?B zD_XiQ(Qszm4RH#f_3$2#uj10>*J!@UoVnn?NxHNH=Et`eOHz$o-zQXwTbS@bUAzZU zHXK)9sS=F5xH}NFkmh11@?2ttf0Xe~QW<7nsQIH7gu{d&3%hS7{H8XZHi!zHt>~X> zHueww|JJYn?MvR#{!4qdQU0Oze_Nl@{C|9j4;CyqJQ8=kr#8Ks<$fwyZ$1qrsgahm zlT|xs3->10*?u&z929y?&7X%8yHb+cWYC^bt~&P%tDtBQ#e!=lkgvgfG~@OrRrXHK zVNTuCrP-lxwWYenn67(@I&+n&UvEO+bs+U-iV#RL==##_{&R;wwN0cJFaT#;*pNdlef%;5OO%(U?G z{(~&Kwm;}3Lx&9`QeR|lZAQk7NMrti1;j#ROOx5*@QCsEOsB>=Sxp65Xle@zl-XmFK@m1zCFWRH#xZJL7Vo>(&{50>4)F9WvW`8!N#3V3ky3}uW;^Ot zem539?dl@OY*Oj_O(BNA4`Fs)7K|#Awg9WNcno!WL<`L2vZvSB=-mxRpflfVhRp4z z9a;rRI3U3&4j^3ZDN1W0KhPXyO4p-QKx6)Xv@jgF+NU)LEd!S+H^!yJXM5o?;Ha`4 z3-ZG#NGvHHm@C#_jZ@WAgeb=nV9IQun`}h*Ga94z+q3H(+76 zDYfzK53Naq+5!Yc;XUBNvk0m{eM5{{OKOjT)I*9ET& zfNKAoo^E3$NeJhFcrvG#_$Wyy{w2Xwe}u1z@7H9Cg1sk#(8V43k@WS|5789KokEG-(jO7h2(HDi_x{`#obR}`>)bPJ(NW8X`?L2u31b}k&*bPV}*qV}EX;Wt)AugOF>-canE_jrbFWCJ#Ec3eEp?Vf2_;JiPZ>A_r$%}^qTHoGsG!lO|M zxGLFiM^Z?^x_VK>RlBHTcAmbQ-H+m9hy$4D>FjIPbd1&jZi=;ab_M9sp?&mL+NRd0 z{BvhORF#*cM7UU#Khz&jpTUgRXjF^w z(dYgz#yoJzBU~B7YEKWo9T0q>`cL~G08VoRo&Q3})!XDV`vio$`QL;63DOu#eglu68E4r9pS3^%c=i|lT!;QWc5cE@xS9B?NwziD>=`z1YZ2{AA6aK;X zdIYS!e!)-RSsu5HZ^GZ_AvyHBH)ld;QeL%c0jFl+I*uooU{R6cCcjqRB5K&m7^hs> z@xe19_avTr$80$g$dKKk`Nxg$eM$d$0q8GAn(^7$rOAtE5C^n$PvPyfKDmM7Ry`cJ zIL5-FpeWrv+-^?Y>T*rCJgxKB7CHU;wg!P?UHUWNWbF9ctj05AHef$rk*%ff&+w*3 zr3xtysnI#R>YOW^T1uUyN%lL!6F3{H3EyBTszZ)xrfubcGJpQ4 zp10FwrdgjgA2#LE7!}9rTx&@Fnui8z;a7aJrs##!8ap4pM zDwX3?zsd@U%@Mdv1=cicmzY>-I$A64T4~mBdP5@3o05(|4v18PR)@K|=6>cjyoW>s z0TGHpeG~9tR2lY?IA>2~juKrDjiaw-h*X%9-JKne_In5EAJYU+0Z{1G5Kd?iT6CR1 zicE|==Y}KwiB!X|7Lm=k+qR~mTaxOE=C=c0j^MBH#?sz~ObpMtq0EQ2;AVeNO*jVw zT{HNsZu^<%qy!La0bT+bs2=*4^Q^80>6OTy{dTHk@fMN3!j5C1#*Wf5)~%-dWBH3c zo(TWG=it%!#zh6LaQ)dN-~_^?2imDzEep*x2wLv9uO^sEj{Klw* zo5Bv#%-RyUByDVPU!bkBFHSl#e#$jRpn4~`qSPxLUf%sYg@8a$)uVtxJSXx{m0)!z z)76UgQB&y0|JkU>%+T*)oF@+SPka~a+0Z+xx(yl~P1#wEet3h_BuY7z&p4g1BA51+ zbL$0pkHp>Kz@Tgn+}Kv7K&1paD*AQwu6fStK@A2P&#x$*TG!IuIpHN0s>ib)$yCl( z#dP)+sA|DOJc_S1rZlLl5je5Z|s=qgb46e&o@tSpHjG{dc%b|L2nbVQ=&QMmk~U$lp*LFz7TuZ11-l z>%-mr`>Z9WQsE@?10ZSs)eiPMGX7UU_D=x&U$uU}u`+?M`TzOw|0g0mCx_OO3O(f0 zl>h^Jew*Wq_+lY(xJjg(VwMrdst}=7eZ;tb=N(A>g8Q5os+?@n|2wE|

iC-%XgT+onB_=QlF*Td3-=Uu&{p)CNhTYtV&5a(=kP65qL z=9~Tz^^n9dz>l9T{qGzMa<+bZGLi@Ir6$X6WEQc?9kpWZFTjAB1A1DF0=2VV3Sy)k zNoJPDH2wWVjG(G4Q`DBP=CSDO~8l^4>zaj#$6{h;m9aexZTI&bqS-up~S(}$~aOAqqyhn z*RcGk{$y20yo}Yb+WKW!;J=sNSD;mA2LR?t$iwgN??(=%9y-#3;lH@^#Z8Nibq^q2w5L!bHWmNXn>4J83EgMbSnFK`B6e+SQn zglcJNIUmfsPR6N)BpAxxAEti-(qt5-MI()N2lGGhLnd9Ww{wi<=+I+AXAmE`d)gz! z$!Rd0P2z(G87cDKLdpV0r`i(eDRjn7;T22dv_?g|z-)TwM>9 zl~q2dw*XL?yH)%E=)F(|jtyEYSehy6!knXG=zhvVL-^J0&ZwkOX1c;Zl4*0*R!MxO z!?hV1IVm3HzUvtsex|m*V0dFSU?qlUO?NURUmj z^J%bu2HuRe8{P!d@>oVr5ylZD&B=Zzr1HDA)oY z|M=lVs4HrwU|tdh%5nDNE{Y`=oqIj5_UDAEFY{9Y#jV0s4n;=zM=@d!!Nc7JQ`Kf zYMpMGqvAB%{aN=MIqam;qpQs>NN0;EW1~VCMMy4SdlXjC7#v*y{jz1lD_K8m0YGVV zW5(__=s$#lEwMplG|FpZJ4d`igWbVspSJ=?5y{grFkH@z7>b555{vEXK`5# z5*YymccWE)R_|s!Jc1mzQS#NSEBug~9C=`(jyxV@Z?Q>>eL{R!hvpt#*17DIh|dY{ z!gikBsSwWqfI%1MTEt8G^+-YnzXV-;U4>sl9iKvl0O&$%ZCwMi!V#udufh_#se)#@ z%aKK2LTf!&I;*QWjWs9`k!q~wsHVmdh;GAH@gPB2NhQY$R2U7yv2Q(fbS$5j!E;&7 zQdZwx?-sKK>O=m$snvN~Dwj%s7THpZ(IQ z(magR#9D&==dNmT80^cIWG;DRkNRp(&bVbT2Zuz#J)kM)>+EyNv5aC8=yidLSc!AFb@-!;!cW9ITU!c<8eyaxu98y0 zuB3?n)_P+j`t4iDN|gbCUL~^Fp$5kH;4g024t5yfQvj0-?f_w?+Z^^Z7^f;r3!E$% zO3>j~RHlFn84VNfL>Cp4(XHI<;swCnPgd3^8V)(Bt^h?eIX&5Qdpz*qJ|fVK-Xn|t z>ZOq!qT#D_|5V%9r?zw?Cxsz4b;iIXUPG!f>M&hE#Q^RzS1}^Xe<2$sE+h!*buV;OZvAL_O_+u~q_33YL&f_eGBSn~}@@iT- zRk=0oT?{jq+F4--*R&rg5I-vKz@IriPl;iebdF$5R+#Y+LH(t7Ty#=38t}GZJ<4mm zqDi8l66Nc$qr$f3cR}s&Pir!tm?DtBPcM`==I1wO_KYN0q8jWWoh*Vd`jVqgfyqI| zxz-1tnPPq_80`oJHt7_dP0m_=JR#td?`!K@cJ3-ygTJoV+}TZxy!;{5UH!rOh2<@P zCCpJ|eK2)M;8BWz`|Rv2fS=b{nNdV60}lwD7@u~dMM|VjkG_c6 zpe*~DI9R8V{ooLtX}Uz~V!-tz002kDWCA421j21{YBrjWOJ^<-_DkC+bJYIZi2jP- ztw-6FVUYUEGLWHY=f>VV&|ooDRQA1Xgj*Ujy)n!jl`3GxXmW9pF%SxzDwhR%U2gNj zk1?>1@y8zF$3@3g@Bpm9_9~7_)d1Oxm5J`Lpz5E!vE5tFqH6o7DC*IW>fI@WBS}YbAnnHurmQ!v zG7tGtOV|UdL_x%Fx;~91EAk$Y8fhsxScA;Tg9WLITMp_IIBhr++cH>M+$Qq@859H{ zs->QepVd3x>B3Q6OzP)xJ1q9i`=kQ9X`&>k|4-+FoN^{DNH2uRgG%X50r2? zACbST$OO&>qXS>-xJm__5g8FwKqUWkV&ejcp>rQYK2qL>Pq+U zrP>=_f6$$KuP!OjZI`nj#wq?thHF~)R=;3?3?Ig)m8_F-QRpmCyRb8W_g{(Yru`V* zTbhx-P`-!`y#J`a$OEd~Jd%#p(1W-8^uz2_^!`KDZFvGN^E(W@=LUC(qev5YoKy-G z`s&{?gOrZ3rV4)fl~Wy|;$aABE?OgBgTw_UQZ`x9n zxd^L-e5+O8j|@|gc43|$R`_n`UgS>R%oxV2p&yqgLoZ9qesy-*TwJXjdV2#{Knf z-bM}vlb|oIc6P1>oAu!aA#0MHc8Uerp~egc=H9$Up~p#o;6i0rYu%5P_F7e!gWNWh zCPRDMN_bgpDX9pA&~F~nJj{Kdp`0v7#D)hva**WnwTMhjH^w<{!P{UE+!I&^YAP?J zT46Bw!Et}~;?pOgyHjx3a?9;X=+~Be$BTjzU`&=f%~ekf^=5g-vYN~N&A~zflU}pS z(TaD=&4Ks9lFjYD%>pn3=l+%_cK+_xKIizDjID)6pNgu}7P`}!IrHv^&=wlC9ZLQW zA1>qbpF`QJYImpl%>4ot`kW}q<)DYQ_!gG_^_KAc2Gc|+G~73jT>w9#&i|`~UObWS z8~yKyrj?|GGkZV31@&B6IwYU#g%`cApvB7Wq3{kZk4j>UYmt3TVI(o}q+cUUQ%!s! zxzoUj7vCq==UeY;2|w|24Nl}0IpQcuId2O;#D_wZRZVY-taqwDa_7vw=%CFf2AC{{ zu2wP)o^89w^x4v5(!$oq{!l<&$J`qebr)f=?-1*!Bpg^wBqu4^&OAS2ebT28n{zi^ zHz>W*b(6MHAeH@<%V<@?CE$B zZZ%nuAZ(I9%L{A-WkS{2S$~p#Op;b?1Z`>7$PEwt_OKCM(JMY_K~uvFbzIKmV9+VH zT+6VKs>Z#|XlPnwDh%K2$S1wd%196hmMe8T!FFz)KJ~?Qmn;ODf_VSRgGmBj0mbG! za){AuC;RJg5vkEjbGT|@*nsD2JcW9*W5~}er33nDBFwOcyv5*2*eSmWo zBBDfS&ynndJ>fNcNpgl1G7y~uqP*1PYxj70d?N#8?r(lm?Hdd-<0H?iR1yQTa_RSH zElLrWM-VwZLoBDJDwV8-Qx3RIwhyjYeJTrSRldGEbZ)uc0f5@^P(QgGbO2c&>$`QP zmLfI^u7}H>!{(cVu29_8r!eI8SnYeQ}?yVno6%hr7004PTOY32=p|zg6DgwW0UszF81n5j&WcUs72gzpqLdIA{>Zs7 zZm>BbXCZYpLR1d&Tnw?NpCPkCVHq~Sz?;{L&RVuN*vPbY99p$$I8ovh6*+gq*?2FJ zFc{d=U_PC_#I7PD7>SvU2@;+%st5@`na1*ot+MlG7I@G5)$k;E-*P^JetBG5OjL07 zOEPW(zYq&XIugmL(g%EModQXn;w=FxeFq1eglu^ zNz~@`1_z74Gk%!0@ib9(FedU}z-qjnEVpoHRF|JD%c_~-N#}iD;yxD5er#~aN)rLQ zT4}p*?L8Fq5qvR3NAT9e=1!N155+fcY9b+J4O29|#S3paUOe02;$XmHQs4vv*U@)q zogbn*5v6eK{pmv(YFp}$lY!Vd{N3T z0r55fxi3^eVCQFbXQ>F%ULj@0a$~z}n@nFF-s_AClTOmtluoOUqtr=77hi=g*e3tP z6!sdf>vUZ^O8>bfV@qgaSF|WV zEGsYR-?TBM`PtH8>sh9Ms+OV2igX7euG^XsS}rMG5JFl65?^F+u!grJF_%l7lKVVZ zkLZF%ztB_aYRIJpq%j@Ozhb{HAA6ezAF4KGQ-JA+jkBgJ{ObA4dD{mMUZNK1xw#eb ztcIC8B`C<38GLrmXd=YLcXJ`L($i4>YdM>e#Wy+FODb9Qz4P>$&3MyBHb0|GD6eo` zw4e#zp^(o*M!l8_n-TmQ?0}c+MW1wziv6v@<45mw)~pOjBX`l}s_019J~u+6j`u_` z)<+LDh}!7Tz``&3t|dT~dY+V5O0#-(b7&C}i^F4levZN4o}-x5$(Xozv&36yIqyso zsMA(zIL{ZTLJ1LMoo7o{Uz>eD6P(FkC{TMFlIKt)zhAzA&x{>|zFqj#>*?pO5&NG{ zY)wTU6j*a5FjgEDUOUU@n0}#uW_!BkV_?uW-Y+h@l5p03+9`*Xy!EQt|8bj@YDGqFg;!x|B+xGVNYMCN^%JKIoW_`G2KiiQwvT!*${lDD5R0dJx!YwUfx zR?425ctPJ;;2?W;=Bo8wqDxsSCVNY2bskDyCthq&ixm#wQagOJ2ccD@^Fz!{nA9ot-a!TJS-TKdLHe^xU=A@W^?5@(C^?K6$dsS= zlBLauFkWUk?`plj=BJ_}rJl19lrBQJu$KuS=ksx`qbHAwCgd9AhZwwSK`^<8q`I$zv?R-s~YQKzMwS1j;F-km~p&9 zsh=g>Nf06SW-tHYDBQ_ME*=A;?jNx2`=%-#-BC6*IBo7-UwHioxmIS-S$yY}T^94q zTo5z%vE1F?N*3uH`^CleG@(yZLpXjoSA7Cid zPbxDM7_bSSjrY~Cr|QrAs8xH}v_%{C6d&=$w4JW@@k9$ueyI4iA8ZqQ9{a>KLa@1; z)~yP|$5z|3tSTKoq#jV{FQVt;Id*TOc3BpOPwRKhxYp~im_CTPQs0gfyFJ->Ar2E8 zOe-bQs;qJSbWoDP_F5n3O#Sr`>MrSuLgds>PuiyT6n~gTYZ*V*vY#m0)hjgm##Y1d zC@gT*E4VO|YHrX4&ld=gGV2$734FZby5+mXWgN@*1@c#Dd-#+O&Y&J&_EsjzEG=s` zbo)3Dm#DV3Kh? zvGwUVN%iWzMy;UH0kO^g+cIHAyi8I=rwE;B`Cj~Y7zy@_wodfaRPiG{x4(7rh7~1> z%8E%043~q!(Cr7)CpiRR@#{Gibm7f}7RI0#D3#?Cl<0LQCq8&TLksySqQuE6tR?*P z@)ta5V8SBY7~o7qI{r0rhKZB0tVu~j+T24Ej_IHl@3HK_yj!`6d$;UNh z@_{Uewj+sM^g?z%kViZ(@~V;Tx~dx>Eq;HBGZuprR`aVfit*O2hrx)$#1myK6E15) zbMc-OR`*_U8Skbf366O^EU`2Kw>Q=Uc>pBUjdUIH@u$p}Gut0&Bri~0R9`JCVG;}x zwfAD*2=tnhh*t_r2eS;T)H|SRHHj_h14cNJxzD1Z#VpM zIAXoC^Y{u%!g><=^t8(aq4Tr!oZjVTAmcTuleuHXOehbFYiQXei;e);j@W2Gla~lPi}n6FU*6) z2^TpxEyD0UC-{uIjRwMBKMm;&L1-Nxp@u+QePD#&1RBh2_|}Jg=I0pXx;qH*$6f%Qw~wc0?2b>raR(Yra`vpKJ>U zDa>X?5e1GsXUXFrBjP7@Zs+=DOuoULGaOl(zuZFJOBX-%M^mC$p61z!vtzLw4DhHP z=X*ct{b{rx{-kNb*6e9SY9}qhD#0PaReJOn0TpP@SV@tyUx5+M2jvu**H@-%;;z)E zmEsiWvB3qfzZq>mz=Fz4;jM9LO-HpBK3zC9uZUBRWqM1CRG}ePs9e8bEAITg2$W}X z3-=U!_R-HSQE<-kfX_y1oatkO^3xo>$&|hpyS4$LSA={UMayxkXhl<{5u4AOwW=CTYH{8+`P=&PbnrM zbga{~%sZ$wK^(}!xs5QrcEybG>VyNq!Rse1Lhm+kVYfnu(D4wPw6_vb6KBkY`}!q{ z?4gI&MOo;__T<9`tLic+m2138Y;=fX2eubrUDh89{C1_ySFS_le4zM{9I@0~= ztBK?Id_7Ydw&Ywj8|8Y@l;VLT3Hjob;460c{PHHOr!nGnKZ6qEJf*FwP`pImnSVgf z&wMkf^EtpK7M)%BhX{RSw-ZQ2scxK(H}7D5KcTHwYR+^O|TT3L>cBh-(9P2jVx0>cfjZt(KeqCq) zBeE_S@}CyIUY(dc+ge#{}r8m%+;TEM*iOs~*BLkc%eIsTZyHI15)xl$P*pwbK zj3+MF&r~-P;1OT~-r>a;bxS!X$-zJWsZqM4F0O1_VSAnX3bkWEA)n-9%O(1e6Gg6q zPqXNR>#3xPrWPZ-zC$Oe+&PA%h-@uy->v$-@OYHHIH3%+aCCkHC1{C;%q;uE6BYhQ zL;4A;+djdgCGP&wF85I^O{P{Bq0ekDJ9$3eir*#7h+v@b{DY<94$GwT(~bA{*?PEw zPDKn6>0Pi`b?)SiUi$`59?xTyWGbe9zK>t3d?vLSVE7|MP*~T;UyCLmn~~B^hYTyg zJaUvy&wf6G1&`oGwh-rY&<@DOWIajT-PX1S}R&o{j7l+l`l-im|< zn8;xwUiN5+jej4m;2pt#_8dlp6}dm&XJGhA!Q9u?;QE^6dSnaBioP_8NE%!OIgr`P zzJx=1i`5+Le#a+d4)Nkic{8n^oUd33Rt0b0Ae#+&SQEv|!6X)szLdI@0EsXhC~>*KOtH&Av4V>qQ~_1ch+5IoarC$u+JI z@;#A5I{KbPr3fV-F@g`>&(2Nh?b}efC&FJ&PEawCYH3HZe%e$KM4rf*?em;CW72>n zk)Bd+8p^LH$fND9e^Xi5y|8V&Ez{&xd_I++bTeqeWz6s;PVVNSzwdpO`$SLGwLe(R z+(p5{iChBDw-swU-gaGWc#2ponvDw}?khq%L+zKDqZ`ze>ODyBqy+=lpS#-n3yu}O zXdBW%N_8V${=G*X*qsqhs=vO%GGlm^)fuKZn78doh;X$@y^nElSZheq)RxPaMmog| zlWrHj^%!L_;$bQC5Zw4Qc$?wFYD2;7LZ<4ErbG21)W&+goiuvFoxBs@6vIFcwC+w0 z%m~mSwcFf$!pUsXFe3z}e5!%PgwtPlQk;zS$kveg@f7~;?^r56b~ql$-DmWRus_{? zeo7T^4JhA#gYg8B@{lq+g(DCFVSrhs*axxvV{*8NXm>Upp1lNPkV_7RXW&Yr&~3_$ zJ$&b0){yMxH(`8`va3rkN}3|!X?>B+zKRoP=27Hgu)e9|JCDG3SORh*Z^1O*%cOdd zewq1I_h=D!wBC$i)k)Um{M>ux6A?b)T${*+gt#^E`b$LshB!O-~70A+-7>_%1dRfwydI#2Z5zU9Stcnz4Kd;g51syK$ zTQ%Q{tlE(N7wMp=C#I}DB}3gq_{blM;vHLPBYlh_9R|b*DAbX}hv#9K9$?$cQkPw} zp8(r2JhBcZHnL4W2$yM#Vki-#+?z`#*v~WzlksKQ=sZ~%e$56Q>_$iS{3pCAO)+Vy zX6vSLu!Kc&dPIa_(@7DHbEL7s1d#%|1)h&WorD7rk(en~yz+Z_kN&6g59SI4%(f&0o3tpoEd1yN<-<40<%) zF!nFMi!d{pRFAl5(t9g@TI2VAdGpCmUFyTz9zOm4uWcDV!Vx^W<9U>Fx&N+IFd@GW&Z?qbQh3zl6Jher$!;X^wYq ze>p%sSE#yVZ}{Sl!zJ=_DJIt<3S;AS_Q%D8eFoyXeQV3S-FS(KD{H#mE4ZKLYN0cu z;Gx9no0ud2I^Ysk%InxA^>t$-=Et#7E-;l6%jj-$pxk*pN%kY<+w=pTM|-Py{TB6e&pqsSxE7zLnY08=Tt>ga@%pGRK0YJJ{3VwUb1MN0 z)63l{u~v@>_wUk*i=)f3j;oc6iOxB-I z`Q_K+#s?McjIDN2Q)FiMPBW%tpL@^}F>V!xn`VHl&}CGC_{ef}E*h_Z^Fo02P>8I} z=IW_plhHgy(&3NL1y(1<9%7%pgoO=%Pe)v%Kf2)=lOK>o<^JaVLCQVRXW>}n539EmGXo&I*{nd0UA_mVUrfJt)%>$aX|L}RP84mel!?TpQoLI4l HzTf`^Bu^YP literal 0 HcmV?d00001 diff --git a/docs/src/developers_guide/find_main.png b/docs/src/developers_guide/find_main.png new file mode 100755 index 0000000000000000000000000000000000000000..8a7af306cda6ef7b4035fd2ce522fa84429edf80 GIT binary patch literal 38567 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 literal 0 HcmV?d00001 diff --git a/docs/src/developers_guide/fork_banner.png b/docs/src/developers_guide/fork_banner.png new file mode 100755 index 0000000000000000000000000000000000000000..0d140c9fc6d8f10daffe136df1915e868a30d1a2 GIT binary patch literal 18871 zcmeEuWmuG3`>!Y}f`p(Ttw?t_gGjfuv`BY%Dka@HgtT-szdh9deW@OT|F;2j|9`-r z^_l*EavgH_AA!B^g+1=h1KaPMs;;KQ;Pxny`@1<{l-=dSE5`3DjH@zBfB^^g_kwdb ze}-lUZr<}>fM7U&Ufzep{{KvP@Sx-E;gl}9yYF30;C(~syXy8^CXA6}m3}l>n?J=( z%DwC1X#o)9zvKD!vcS_vbH{4FLRc4fXKW&w7ye+!>_2GvyS1(OgC|tx<4=50Bkb<3 zI|-e=(f+ykVENts?SUY#t`s@C&-tC_p`kmJ?!Q;xS6)`OC+f`Q1O^Lo-RHj(uFxNX{~7n-f$u{!!+(1LfUf~;|MuQz zzwaRWxA#N&%?j4PyAhkrE1f7%wo|9{+{mH0m_=6T~$9e|x*_?p{Qcm+AMX)L~( z41w|Y!??=lJAZ%GVRF5htDP##dY#sKdz0yW^JyxDZRUkgRNB|hv;zI}3(BrIxskWu zfSpgfcM6n4HRPcYQdP(=#%Bwc1c;45V^DUB|6U}tp20UegQEA~^S$fI{?dGm)XZ%t z05<1%mmZ}az^H*lPG^7dgBmnyWZUmIWWuG&uY&m3R8fh4>f=j`fY- zo2&3X1czdv89oF%Q1ef6`4qrJ(GCo9B1eJS)J}i%Z!Ui}tyn215SV4%Wyb zWKQSru=d^NenaM%;-PthIjDZ)3NE_$g`;aOuKp>smd^TwuaGGeFMmg+<9rVS89j7G zYF|YhvTo^j@Hy0S)dD?Xn05Zk267q?O&@~yiXsT!b<+W0{A5C8f|WWan^I4eG_?Gb zZj0rY0d^J1#WZWyK*Ug%_f4u=$@a~gIjCU1Am;K@bf4SPk*I8upIN_PoJv}DEMpL! zTGV1>BDr9j4`vs!_lpqw;?Yl9?*r`9mm_QCk_~lA^LpZNF7A#Cd4wuLez@-F2DcqB zz3VovpZ$HCcz_(zfOTA0eCQyG7W9`XJ!{$0j;!Yq<f+l`A`%p$)weSpbMt}n-vNr|pngH!$_fI_?jq8zj2VGloE zIh?zh9Xq(9I`qZ)+y-6oE7@Nz;)o$p+VDDQhMaEX& zQm-)84y(z(3VUi$snI{v4g;;ipY9!4F=B_Y(H&bXlTcij?2!rIk$vIkX?%qS!*|Wp zeTH|4d|OY0W_W%P`c!<&_%WKHsE8B1yL#@Lti!9N^Hv?&rff{XlQDwaaIM@P6_@|d zCaB}w>`!LXh9=fZO--z8!;4%%-dlOwR(a5&YeJ6J@!{F7D9g6qp@Duicik)e+O)8x z&7Oh<)nN+bgLdEPGDASo`O@pxWFnVgzTXzJ+E9Q^%juk`HLN!ss-03Akk*(^P!`s2 zyQS8+F=$BHC{m72r5!fxG$B;0#-wbze|;$?nn>AE*B2Hkq$ts%Zf#gn zDC;K*Zgb08s?>{jR?2L@S#P)=EteC|Gjj_e=I7CGf>;fbxGBA=t@UiDT1-X9t$|r@ zURoi;=buQo_wE&?wSRE^z-3CX9h=>2nX3SUg30Y5Tz=)?FZxM)pydxa|JD8wyc8K#Ifb_tal9ypG+?rv z8@QN=D+^>@SPV>LvTr|GtA{L_I_Jx((T4!iTW{89xvZaNF#; zbaAX)0t>rs!PR6e#!Zwi;p0J0X&tEz!R(s%6SP zY&^{FCu5ygm2`&gYkex~*1L7BQdwJSR#|Feds74V-wGmxH#`%(+q0dHVRc44kMD{< zvKsLS8rHX#m$=^~pBaHvN9`2y08%aWXY)vO9dAY3`Pe! zN*rBFdmJ;9_iYkWx@Z@Eb-f!)({N)b;D_lY($7so612^mJ# zB*uAFj{>9%=v$o-yOJSCoODxQqLXpQ4OqJ=PW>w>(X{&I_g;W^s@kI(xjv}Au}DgMZ)th>ae!M^=IZb(6JKOJ?kZsC{j|& z(7qd)I}0GZ6lWT;_Pkj>!xQM%cuBA<98PMznJ3vlF87*O#fWwht3B{B2vMfI?8%krucMN* z-&ss8AF7Dy)2Gf47A!v@6F`t%22_p+s|vie=KpRgZ574dy&zhF=nP|=bLUG3E;5~4 zHIH+6_?=mC(TV`J7(J+rj*DHZZHa!D+1$=uKJ zZYf`Bxx0RzX`yNSu3)AtGlhkxfi&t$JIT+xt496Ym}pZ=ix&hOy-rasB{0*Z%Ero^ z^HEo<;Z&j`*0jV0_<8vk^9nfnP6$t=4x~@m^m)8XNV?K7cD61C@Nqc#ArK}~95|jc z;M*|HiByEvc0NF_g-evcj)e8V&)RkyG1udI3rT80WSvE%4$*s9ScFeSOdOLJ^0ZC; zRL%ebwRmx{ErPGl&y!Eb*zFtHvrhN6QVWHa0GgJtF7ebGP8t z@HZW?rNF1w-8nQ?p;CKk;z*LP4J%X3RK;%K)mlTG94*`v`?Y}dXU=h1pBwl)s7Kv& zV?k3?*K`%K5BEkLp+AAEKPJ^GM|LE?>6GtnY}^hrq7fmk_IN=MeNTlUq6D-mb=egZ`%9U>SomNkjv1KOF`WF-T?({ zibg&(5Qt#;b_{T622+3t;}ZOBYB*h-?hkMmL_yQOW^6ypikDvubL%cIJM5~%ix?ML zZ8zKD*ZH5jd$NnZ_wa>AFpcf8&)u<+n9ge@mz|He8$WSSh+@ zm(6oem&+kZ^MTU9{3pRO4X?=g3;G!2VoaZ3F7K9?3c*zO`Ft+NS6=~J#2`5mgDmee zs0EHwEL#O}n5tJy&g$)B=)7|;*|PG3YF!;bc(v#yj#-`636p`ijCJgZn&mU5NuPq} z-24xA*wo!DA{6yfGccOt!^Yk_eD1u&?5D&nw__H3^1ziB_4Db7^I=0pyZwW5 z9|~H_HF!cF<;%fW-sIgw8jtTRiFsY98RFxSD(Dh5cgnC(Ngk>6)IFgRTWnxzl`4y! zLLi_|e4bHTbN~_}*Yj(Se9->q_%aq_fx+pZbNU#5%t;)CD$(vrXn)b&U86?+{;XPV zFS`^`5~h3Q3j~&%XX(4homR?mvo6aNIDZ@(7@2ez>|wTpXW6qC$)@zJC0Y__Jd9_l zKl)nXT@`%J7FTzRYujlp>^xWK4Jg)wUqrd-NNM3O+aV>L~#YB~PlH>jjz_Q4doRGn%#)jnm*+MX9%=f5ySME2g7?f!>#Dqa6Kg#vE ze^kBH9Z-5bC_rFF7+epLWRLhj@q0AS2@MvVZMwgqnL2nzWu7r(ncWIBqd%#u2;o(A zXvPGtT`2fv&N@buxkih1@VV3@RK=zJ^0G|XFCj-NGj1h0W@Nmal2q+Z?(C*cW(Wi# zuW{Mu8i`afFG@6r(cceiV}01?@-RD--D16N@p@f`W1!qtEPThblZsU5;)hlTBFSNEm ziR1YcE0S98KJ#2=#tXb%$Q!SV(7Ypg)R$Hok*%x!G|8_k-%GQx&2^;I7D|T^-e#l0 z-Ijbq(vFeN2=*D=)Nq(zTT;mK(0(FqBZsn5n3}LQ{rDJzo*wg-c7nxwnym?)ISW zw!22(fiqLMB2O=4b(?Og_=$DYK(Sl9p+~BP@yIhHA?gk6&Cetzv&#G}MNE^0bH(cRWguW{1eiiquXi4&h=7MO9#zq5M@kXHc2r{%%QwY_=~12W+e+E!F9%o zQJ9T}##Msh*?zmNY_jF`Pd;CK3awTj;3-XLqUp;5IlCA802?oo%1e5*V*cyXg?C5& z--tjAP7P!Gw5JH>jpz1Rf#Yc=v&MNR?YibO+2>Y`%^LoXp zcYuD0(&*)~61VuUDNNq&^zrHDn1174tPcux$*52;wmPy&}{))3tc+ud2WT$fhsWkG^_=Dy7+v&dv#<8MoGiN%uT5WY>ib3NPg zPo}dOc=w;^*p-?^iBsZ^+g8O%Wj&#d>WUf?E%hZuE?XV2TeI9{90s-f)LcQ~B%iH2 zT@IU}9Y9p+X_X^cRG`GyV)$5X6Qc@m)S^%~jiKwN&ImQ9`-9b^%Qxs5y7PpQqf@B_ z8C|d9)5eY`0(QUQnTPg!PEG`LsqVfJGNRs4_!){5<15G&#~h%nzR(qea-O&#K*-xn z08=AS&8SeM|Ku0z)tWxHILWxBK8|hkbKu&Qg4o}Xm+a62rFZIO^?c85E4B)nKosic z3?I=mvVr$CZI=UI9BY{ajYYoIUn^ri+cS*p`^xG>>1|HR#VqS*j3-UfjTJxA*T42+#;DD0~TnEU@)#&g9>L#RrAq|e=bebi@9ZsZPbX7U%Onn}>HOO|P zK?_c!;7EtnNfpQ%OwBhllB3rhy24dccV$>@b3D!zX<)@=ajEl7RKMBB_Z9oKXFY8T zn+*pYs+d1YjgSLpFaed zjMgoWuOfPSHt{&L?hY_nRFfHWov^C?6Z7Fbze#1=h|+bC_xP8(r>i)ZW3$`Pmo-7( zNgiFd`(cP%hRJ+Ag=?^lZhNE=()|YEjOtzi!ondJQeOgUw?M$DLv=51&O6 zWD#c(EE(Rk`Up|zbhEbZh4QUubkcbD7MgjB5t`HQg>lb9$UKY z?7b}J*Dvnl9;jrW%M50i7|s>(lb1BkGzov zr8Ev&f9mzUOZgpR$pYIR5=Yf2$Bfm_QRJMFHZf>e*Jhbf`95}f6@`p}1}Wl&_PEhX zH-neuUTIg-0KrF}dSgln!jx+Is|pXjh8lt7P0-K5Ma}Zs3al<8U$EbE`YhKeRy=l! zldst!leJyYLjxGQy~bug*gy4Fw8X~x5OVTqB4AZD;{yV3YD#Tu*5Bo9umSN%;(S{2 zjbJBG+7Xqb5zOs2Pitv{r_^ocQ3 z0xBQK#b{oJg7sFqjRalYNR$9C+lktUt63S_l`R?&&7r>|Rw|(2({3iOZRz2M-jTS{g zq*2!=pbgd#QOziY-?d55PjZXNdYWtWhM>-SYUly|A`yQ}R=FRGSi&;L{ zpWP{;kii+yCqhz04sd?=P5RWqSKzYwRgk5*wJd>7hOdu;nu8|8>V6MiM2dy%MbvLS z?V4Z;!q)g~kZGzCPzGFJCdNr#2|ku8M*kKUzdzmAwCz7S&6IDep{H|el0rMRs8lX( zIe;|rG!?>ru`x5E_e>-%|0qvnnp>0|G8|$faweU?GMT1Zvqt#Ncm4-7@wc3M6gA1m zqh$K)aKojy;x}T#)(6Wdt_>nmyAC;Eosd#sRZPK`q{pp<~CY3nZtJWjkQg24X zx*kx?#5S&yjBBF2ggw?&0&P;)8;ewm3Uu?3TYU!W?K>Bfk*nMh38pI+pLrCL)HDgD z(LHUoQai=2bv6~w$Eg6){Vfa_o z*MnDA<_>;i>TxS~gF0Oiq(w}S)btxC?06|D(<93(W3n}bcDCMcOSK`9F&dEM7I>S; ze(>OrQ;X=8UspG{FEN3u0#QIR@-%@+ zdwGk;SfW#A==3mihxK(m6 zy+qi{)&ALa^hi+4Fnz}Hx(GdGLPRP}OlZ;Nb0#{EArBL&;p1<5Us~EgC@_tjFkjng z-|FtL)C>>RHP2@1dYaS*1>q{wuNotbv+3omgE})Dw~byK2SNeIB0aZ|5MM{ zoA&b{?yUBqDLQlH0yhIE;pzxD21!^v^{v3cq$S|W$Je#DtOLKP{$l6XcdL7VlCrJL zEBVV*-FFx9iSA3W_YmP)>9A)hbyE>7<7{IveQWL=g0RxonAkugV@s`dj#)>~t<7_& z-c(O_SLWzCLv1*$IJ}mDkR#+R`%cMO8lj2IoZM;fr9i)} zs?5rTF=fqo_d%hD4v%td>QR~%qTvL2kZoGT%b%sMoGhc9u8!F@`wN8+cLFVCt&-pC zlX1!OQNQpM4-Ne#729@0jv<^MD&--!l~7-LvYr;!5&Z2uD4Q66oDuy zKxgagECZ%^XULyvgzsXpTze?hMiQ@N%tAGaiBb|hxTdLE{MWsIie0jn2Krk51Kp!;FrYMpjMB@!jdR$LayOTr(2fI$B*rcUC7>i`eI-oUr16c z6va|Y^WG)t{Dl)ce^C(f<(H0blpnX$Xregtb}yOvgq#gy2Z_GLB{0EH?yulwK_e-3*lP1dJRcr}xsDcP8NDb{ez#)6`W z<&?SIXeukg>!)x*9>DW_^dq8fx5h?en*oQx=1YMz zbAn-!Yw|nkOgP!$=+&e$7+gi$;IWflHZ**$b&1b!{-Klz=8LI|0hO=S!r`Qou(Qp$ z2rE?j??8D`A5{PuYzHOBR(RQ}Yn;fxzozBQ;wA zwd5|iO57}rWL;4@YJ$|H$epXI8vi40oF!&0e-vl~6YLwlG--UYYZG7D(1jP1x$+_6 zmEQ}kQ$aLAY+5;bwdeTl3WHfW?_j^4s8!TNW$%HmDtoyUYYaXr4_N=?J)3Z&^S{%=Ef) zLHcqu$pQv+nnkcsKN-__mN?`6ni9b}$@jAn> zIk?!hP2Z2w9J*Af?lqaON}u*K+C;Gl+uxrtxXX`6(PYE{R1s-<@nHzSYEe5h<643I z7I;=ambUDCs=zn%9Y~(mCQABzph{{*Qj7)RFHulVf=|8lXc2hzpO z{+NYT!T5t8Y_}9ON(8u(X1dqAQ)0Nh_jfeTywo!e5sM~@&mrGUyC&Bt+(%!>-G2K9 zQrE$GW7shlK@`EWTH4wtoAvIV4Z;LyjZ5s0lAl%2G@eB~!(Mf)ofbO~5bMQvHS-BU zURL?)OrlJ{>KUc*jQ-Gql6p#QpH6ft!jNUkzBYNd+T2It8Eai? z#)>vJ%ZFo{P1g^n-vX`xPU5AxDPMr~sVT@|2|fqysxub``*AO^6RyE6kLrY?UtC7| zAZ3+ZbvHUG_9ip^0>vD0`-Rlx6BjjLTB($IHsT?^h>m+t5hQE7DR=N~9cV!|C893- z!vTZIF*LAsutOQ-<~~4B~AeI$`}wSwvL6C_D*`hofOe%ElV8RLyY(%o}W6MUiYT z2?e}?s&f{M_PN@fSZZxHi^F6&f!Bfo{TA_#M;gn*oP=0o46whXeLQM-MqU4NTD8LM zJ55Dbo%}Chz8&UstDJV)25@rLODRlEFq6-EFWK{bT0U97YddzuByS^y-*du}ntjBW zQ`)OxG?*X4{v2GKD+9MjN+% z(Tv_#r->2TsWv+ltmx!hsGd11`zPn-*m0i1$0E{kW;fw2SOPckOK4`N5KI>#c4lNV zWKbP-6Krn2Z@7H3ATAKK2?%Bh%81?N4$`%fvdeV&G7qvv<#%Y5lGK>j^K_|qofTqw z+CgOr)6|nsbmiaC9L-8Nu`5KXABBlIHD(vuV)CImv9os9K$6>be}n}tYP6O(E``bO z)hE6Acu`?Gio^?j7V2Y}fu1OvA9%yV7cnJMh#NUVBLt(3!GC05IZ9Kh-=2icL7;|{ ziB6==ymcN4iPz1fjSEe-RC0MGs^eRwDU01fExWwuMUX&mx-dOgZb5^_@L~u`2Z*@UPNh7kMZ1+77? z&dD2);es)pUlnffEB?px%W9ZQ>tQgzUAlf~ci7HYoaLQTfZ8H%-Sq84VV$LI% z@)vhw6}Km|lj6mbnIClmF3;q(ZLZEv=Elk?X93r>iN97#CJ@~#mE+(zk)fB4FL?8q zMQrB6oOqufV*A##QZ&UH48b?Z;R(O}OvSYz6hNo8b`Vv&a-F@M8l#A157RDA9Rp_x z`Ar$I5~1I`0eh5X>vj9IywbR?5h>Rkh{c@-<`jdq$ZY;HrORg~$NS+kbM{xk&bAKh z%#bmG7)XN9Nsw>r2-xE@_HCi^!)uVi0Gq#>O8zO+chQI<5o{4#_fkd*p_jokdTcDm z<68}Zo29tjuh?O`Jm4^d32{TQRlCI(C9gwfHr44=TA}2}11)+w`YMm!n#aOQ0Zn~V zF3Dbc;Wvaii-m@fyZ^B}rr|zR;=T29zJ|d0cE*{;)Pv@D*!$E67MvX5Q*Vs$$%1w1 zunf+6#ayZ5xgrzQ;iYzku??OjZ+q!O&nR0RbwdnlT0#%%l z0~#s$zNV3%?2bu%F_FFBoQII%xbD>JuEw$&DvC+NO3s&KYAsP}BD(r*5`m+f$e2$W zB6VqvNyIv|XHn&i@B!tL1LRY??xUHmZ#?I3=Yk-zRC9nEZQ+}CIeT3aNEapNFkU_Tk ztMxE>=aU8V#Pyw|%L^fAOE2m1S-x~!t%I^n_3vKzopX)1qz>Y-ArlE&JA&3-+;bWy zJFlQDzvRmqaP^vm_#Caw&NOLY_V4V8?>|thZa7=-8IV*qA|E( z880v2#z6t6JpFI=jozq5dzP7*h;a>JwPRONgUYeAtL2uH1%*7)rnre56Ns3y0x3Dk zQT7 z&*7|?^(Pncv~&94dA)tLOEgHp-ASe#>}T;yB19Z^xPY~38r;v1_WP%S^J?PRa!x67 z-clgGBKDD70i9%Qk{cV>?zL5)Qi*Qby)F!0zHQAQmAA0?)AY_F)G>lH25X^gNhb8o zl;GOPL;_`0nn`MSR}(M)>tyRqGGdPOr5DBs(adcBtmjTN&&oI)U8USVB!YtMIZkEC zYR_oJ39zMKG3Qdq!rseBGifmid@~37WHwvoZn5IjPCWdT^nxB49?${ z;zPR(m+Lm_`$M3SI_nZ2rF+AU3Sfa5nrY>u8T{r8^HD;pUugu@*Z(@X8`E~p&Iq}l z4=ky;%v194Rbqo>%T!rnO#hH767zZ|UX)_L0~Jzpm5I-YO9ZJ8bym@8u@xBeiBW!v z2^iC=*zuLfD{b|pehPP%i$cS7rCxW3ucjlQS1AeN?-D><2)7f1PSq-}V3%UN-NeM7 z{yneSe`+X4=_e{!awD9w>mchy22sxG#-73MjcdjGWjuAQV>Zo_1f8r%C8_Q)_l>a4NQi$2DQ`L5riJhSJnwZf(Bw6R6 zRbchwXwtEK^!T7z1650>9Aq~&#bZ& zCRU-6a9z(Ag(su?kgzXFH)_}Y2j)U`<%|oQxgWbHB!t_3tR+9c2*H~0O74$e{&bvj z@j`$tODwC_)sJrn>Y5D2HM=epw|s$MaNitDmsbyVsL18hjbXm8seh9xpnsY_Egx4M z;h-m+M7IUH=!E6R3>ATl{JhN-+} zFH=Tt!*jPB#ef=k+jvytT7_H*g^H5D^+?H{57#@7jk!mtctwKvZ8H>2qT19q*#V=# z$OlEXf?Aojb@7m^Hg1?G^4Z{`+{`&Nt4KowvU~w34Ld^jc@_K@w8M(|I5oROZU9Yq zDT*!I%t!pV!7Lv_|3G`{PVf)3i&`px!Xh6uqvXruu4_DROLOgVHne7B+1+H^xK?{E zCJ){U??K^GqZjRn6Rmxn-cjr909BE>uIu5nCIQ_!hn5+mqqNgU3*5++NvFC-TR|?7d&cB@SM}yn-F9j z(L9bOSb5o(YfZFVVaHg_>PJd`{`lIAWyrCB)3g9DnxV(|fZ#`MU0pI;lc?1A_6LF# z9@Z7z;>~Xa7JEUhyI6`+WQ zyaECupFD8ELNh%4%p1G?+u^5so#|0OAmZ&A_lh;)yliFbytfH%>;!K9CaTt7I)LJ- zPxp*RD41VKh(ATL4O(>e#MgmZK=7~IM*nqN1G^m>AinN6lFOGE`@t6BL-3*3db7dr zyJ&-Q|CWhH!?s*4E`g&LgD#?GPlao>WtXJ*NVH0ScZ<|UbeodC;i@5)X7vo^^ zc4d(V-ADZFypBPt(415^%KMxY-6x0nztj;N;)h^;=ftTqKhJ7mUVfjZ%UO|SY;+${ zdW;K-$IBvKA6tC9JfR{ue=ku#K5D#p2j80I?co=rf5zoAl9mzt!yVbe#kg=o72M-J z1plHz_lbG<@6)crjqwkm{?|WMgsvyHI#v#UNlzuUEDBv8S>i{eX)lP(dDi$GO&@j) ze8Fp)^M|$yxgVXTHd}pmu(?@q;;L@7SuMFvl6M*_K1pe>+qbc3n@eruEq14#QqFE& znoEGIES#z-EGT7EEc?Z`04jT=iby5|j5>B^_T_A)C^)>16Bbl=^nJp5m%C_?-;l0! zao2l`@a{+1_H|wF?9$tpyv`UM@AelGlb#=R^4rS$r<1Ard&PS3G)Vwisa~ML=35P$ zobH0iL>-$|+aNVIq#uO+xcB#!}2G%C|9$> z>$I}HmG41-7J|3TaE>%uh*%q1V5kdSN9RvLYkYv(5lQU1_^!10C z(T@zZZNSg(Oxg$j82@OtIuW&`@0?l17xne*hQui+^^RaG$^HCaO_ziHL)hPP6S?Ix zl1OW{)-&Q_#>Xl}l;+*coiC4@R)UHb`rukLyHzc|n^LhQW%%ho)T62m)ZCrnz_CuH zN8q&jkKpZZ1Yc^uLv3mk8Ot|*)Fka@S~&Sk-IcQWk9uprY)0`0|7v8m(UKkKcdC0g zsQAM@eQ`9@`UK*7=(vTDBKyRhlYF2#{)8jqjaA+|{=bVbTPA*HlM_?$w>94NH6xVr z0FHKc;Oc^XVQ)$4*}irwAVH9O3^-jg1}h8PRPO@fMJ7G_PCe;X06zUF`r)b9lksuU zaZHFAx{rN1i~jA!>J4IO<7^1p^ekyCuBq}IUyH_qr}R-^p54lJ=z4W0s=C5+zoq}& z4BvA3Q)=bbgRSi=ua^NkZ&q4nQd2&fUHS9^+mNGs8`&E1A#C^BBGxUJKFU-v_CUVz zo!A`>+cb%aa*>EtJ6gF5hf>C)V)i<#&}Kga;AB&>j3=uQ?sP8WhBj{ zUKcaYLP;@bd9hKL-{&mr*%f7-|Mbz;S2dJ$lOm8>`!kWaBY-HM4$`Spin`0o>v1Ju z$4|GDI&J6|f38@sj&-1M$5 zPUc&PNUu0OAq(-~;su@N0xY;kThezsp~Mmnuh)l)GanuoG8rewqZu+VpXtUZJ%%Zn zZrERMCSUP;i_tWCNjIhCy3$8D{!Mu6pzL$RxWK`QfiBq5&~Ay`ot(ZWgQ2#4Xu2q2 zCe(XxO&I*NG^UFsnc0*Uc=OE{Ey!0swWvdek8e^fP@v~FOtKenvByUGjl z-T-kvQe6(P=VT=SaAPtw%XBynM8}mwq~MnAY{~s*+0S@2HUGkB88Lw>c-O>@CSKA^Prf=^jl}zlY@UKdEymd@}jaI_9J^@;=h(ewg zLb7OZ8Vo|dLO9JF*-r`y9F-;UAY5OyT)!#hkaPOYfNeeF_!@RZQr|fAnwYS6?w@1f z5_p;`QK0w~WP_(8BYYaB2)2Y0Jnb0oOJ06$;Qq+285PcjmA9rgZTO~~rl6z3W!7h} z9CC`IxqGPBUMrZrALo8Tr+TS-$!&i}qN#yF!}yeW1y~Y{si@V*Y${jUn0q^WkPaG? zXc!uXby!r0{f)7eR43+#;F(u+D;H*^g8ae`){8#e)v48N30%&;b8qpTHIOY9w1s^* z5d%{RqRXJeLR)*wg(v|Xk4Io4O-gAx`dyIxTX}41Emx0Peey5^*bPs#c4+EjOe<{X zR-X<&+Z%4X4z1bD#=3OQZ)7nBvl7n;hCgB$QM>rtZ*> z+vaaAjnl%IT3Rl{rs?|r!b#Yvyy78Pc?W@$0yN#!S?H95r;W*b=KhxLIiWkKTY`TL z+x-bzozOC$CII$L$0rf^&nssAx~nK`TMfHRHS)haqvBiHq3Y#r8zHP<=WM}5%Pc4XYpSf5(LLPbY0DvIAV{FCy21TU$W!gABUy4 zc)a~|$c>pdpmCPSx2cIwO;TbmnMaLnhh9$avWk7tT&*vV8craY%5QuMKITZ5V@Oaf z&z^j`EBvw2jrKyt|1!Ydjq^)Jz`^hVQ((98?9gkp9c0g`0_5cr<5(VoJ^cZ_;klR_ z9|J$~`|0!@{aI$q1+OnQ88WvV(T8$tV|fJDTBj(1)ocF8Q!Y#)>UrsyF;}T>h^l!T<0;`@>OdgX|$IhvQqx zj@m84uCK6ehb`JfCT>=5p!HI<01i~~5wx`IXB6KUK3q{(Wvhqu5GnP@nX0^I`u)Yp zjeFGPn#;m2h7KpQE?@;n8OcR`P&Uv2GgnjV$=HT@+wQB%3{#pX(;S&ov zYh@MBLBX~Ss8Ffpr@J~=KTUBq3{u+C9F!woiM{@L>xuS(?&r|YZ;j{uD|HN8r{;8; zc+!A|%kJ(m=gccR({#awOy1seF2tXF*~8BnnaYeurxgD4^~1k0w7#Ge*z(+YEs`@XL19YbvPdZ^v79`p|(s^$A7qg$4s?qL#F z1g=1*!jIWfDr)~)d&N&mRL&XQG&nW2{EH1~?waUKSe1Q?u*XMW82KU3ZbPz_nUGLz zYFH|3H9Rd8rGm#7W-r}hEv`M<(yGK&R_fKGj+P&SCI5U?PBgpIE!M$$U7OIxgQz*8t4u$Z+=E5KctA1Z@TWB;UDu9}0=!fiQ!<dG>bA z{>4zDrsHdO$zwA7%hFhrf7!@eL@1ZT{bDyIEG^4Ss8YhFm$aFu-7)&y4znV>n|`@8 zqbhoLo<|TF#kVo1@oBH>AsAnnyt%2`MMP)u?T_0!z{dVHTf~E&Dg9S9!4Qu&;@h&t zAZc8>73-0$J}dWhXYs(iB>V2cfyZbt`r$rSQF`JNUr0@1&(Iel6nk~eSfJJ;?#E%l zOj6}oqslH{`TWuEuTx0m1?1>yd#e=;oR|S%82p$;SV?$W;VG}FH(FgTfvPr7NYH&m zy#mazC^N9tgR;17*a_F(PAGyovJ3J7)xY6rtpYEWG*xwTblQf71R&%v$P2Jb}QqwaO1x!%L}v zzomkVGFrUDy;(Dp0@w&{JauUK{sZQuWb}GZBH& z%L5I&Pv-vi>&~~030l@z2mnx5-rZWwFipsXKvSqSpfV`DZ#)t^tLZlIq{w{-If%0D z1SxA!K0d+?b0ridSYKbB^+uwREL=TP9>(fjNyX?Tn`tN ziRh$4o+>G7?b~tlfl%&LAz*TGl!osScLn{msV#%r2%}cnj3`r$tm6~5skiAZwJgB!AJUZoCkiVSZ(ovaq^Ba(H?MZ2 zczn$FT?jYEzj|SADYdosxa#Uzf#Kc1H33O_2o(qH^f+^4F%PbzXbxRgof zm@57oxBe$wIhiecb|N_^rm!>$+Hfo6b)b<|HFmcsJ=)w9<9F4B*md97h6c^xaI9GS zZIO?DcAfhxd@*<$@FY|@qhgGAk%h19a^Eq}P4kRTeqqz$AsEvbcpgIwy6>6SqM*BpL!)fK^~Ntq^)>gf2Iw7p^JW@hBqjXoM|b}HYLF{yYdjI5wP)G6uvb<^y7o$d+C$Ho;}DIMU; z1mr|&W6GBnBEOWU$Aw8=mQMa?zyHV}%!Fn5uj=3jlN|-Hnbd+DJwMJSyGEaL2=j_b$LQ2+uFHrEWLf8^EY-LsNx-pR192ofr*2CM-r|&WPIDEGnTr0Y`V^# zqNnE=|NB;ij!+yl7<{mF52XxpF3WMP#f(1%J zR4`1ub55v*0)tVe=lkq@(_=<)2zprQjxS+ytuGt+g<@=lQ3&;PXr-(Ol4?y7qJGy1uIK?tf1u(kjL!|0h1;3%cii5D%-d_UIxEBUa! z_PZo3sW32H$o{u&z4DgUR|*e)UytXD`(yvz5JmR*|I326oJs{g)B?{&Sv&n*GD3F2 z^_;y|Ad+`~KX?3GFaPI#{VspRfgcU`K5jme_Whq^OIGm}#{=P+pYQX}`xE~?5n;v! zYq|gNva@v;pJUp$|F5?`&z-8rcKitWD#zEoeW(6?Td&|Tdp%H%7x0vpANv1aKhW&{ ziLmI)jJ4CP;IM&z~fiyf4_aOe0!b}!psHMbycfD$GH@K zxL;PPvhB&8xW#!)``oJkU)g==?^ki#xjR0ZBa~aPgwR3MP}*Uq7!pp}pBpS@uyyK~+|pKy@hJzf1=);T3K0RRrO BAqxNi literal 0 HcmV?d00001 diff --git a/docs/src/developers_guide/imagehash_index.rst b/docs/src/developers_guide/imagehash_index.rst new file mode 100644 index 0000000000..a11ae8a531 --- /dev/null +++ b/docs/src/developers_guide/imagehash_index.rst @@ -0,0 +1,20 @@ +.. include:: ../common_links.inc + +.. _testing.imagehash_index: + +Graphical Test Hash Index +************************* + +The iris test suite produces plots of data using matplotlib and cartopy. +The images produced are compared to known "good" output, the images for +which are kept in `scitools/test-iris-imagehash `_. + +For an overview of iris' graphics tests, see :ref:`testing.graphics` + +Typically running the iris test suite will output the rendered +images to ``$PROJECT_DIR/iris_image_test_output``. +The known good output for each test can be seen at the links below +for comparison. + + +.. imagetest-list:: \ No newline at end of file diff --git a/docs/src/developers_guide/propose_changes.png b/docs/src/developers_guide/propose_changes.png new file mode 100755 index 0000000000000000000000000000000000000000..d4e367ce1db71d34d652c1750be9e4610b4dc536 GIT binary patch literal 24197 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 on `SciTools/Iris`_. * :doc:`metadata` * :doc:`lenient_metadata` * :doc:`lenient_maths` +* :ref:`ugrid` -.. _GitHub Documentation Issue: https://github.com/SciTools/iris/issues/new?assignees=&labels=New%3A+Documentation%2C+Type%3A+Documentation&template=documentation.md&title= .. _SciTools/iris: https://github.com/SciTools/iris diff --git a/docs/src/further_topics/lenient_maths.rst b/docs/src/further_topics/lenient_maths.rst index 643bd37e76..818efe4763 100644 --- a/docs/src/further_topics/lenient_maths.rst +++ b/docs/src/further_topics/lenient_maths.rst @@ -84,10 +84,10 @@ represents the output of an low-resolution global atmospheric ``experiment``, forecast_reference_time 2009-09-09 17:10:00 time 2009-09-09 17:10:00 Attributes: - Conventions CF-1.5 + Conventions 'CF-1.5' STASH m01s00i004 - experiment-id RT3 50 - source Data from Met Office Unified Model 7.04 + experiment-id 'RT3 50' + source 'Data from Met Office Unified Model 7.04' Consider also the following :class:`~iris.cube.Cube`, which has the same global spatial extent, and acts as a ``control``, @@ -103,9 +103,9 @@ spatial extent, and acts as a ``control``, model_level_number 1 time 2009-09-09 17:10:00 Attributes: - Conventions CF-1.7 + Conventions 'CF-1.7' STASH m01s00i004 - source Data from Met Office Unified Model 7.04 + source 'Data from Met Office Unified Model 7.04' Now let's subtract these cubes in order to calculate a simple ``difference``, @@ -129,8 +129,8 @@ Now let's subtract these cubes in order to calculate a simple ``difference``, forecast_reference_time 2009-09-09 17:10:00 time 2009-09-09 17:10:00 Attributes: - experiment-id RT3 50 - source Data from Met Office Unified Model 7.04 + experiment-id 'RT3 50' + source 'Data from Met Office Unified Model 7.04' Note that, cube maths automatically takes care of broadcasting the dimensionality of the ``control`` up to that of the ``experiment``, in order to @@ -218,7 +218,7 @@ time perform **strict** cube maths instead, Scalar coordinates: time 2009-09-09 17:10:00 Attributes: - source Data from Met Office Unified Model 7.04 + source 'Data from Met Office Unified Model 7.04' Although the numerical result of this strict cube maths operation is identical, it is not as rich in metadata as the :ref:`lenient alternative `. diff --git a/docs/src/further_topics/metadata.rst b/docs/src/further_topics/metadata.rst index 79e9c164a0..1b81f7055c 100644 --- a/docs/src/further_topics/metadata.rst +++ b/docs/src/further_topics/metadata.rst @@ -38,8 +38,8 @@ Collectively, the aforementioned classes will be known here as the Iris .. hint:: If there are any `CF Conventions`_ metadata missing from Iris that you - care about, then please let us know by raising a `GitHub Issue`_ on - `SciTools/iris`_ + care about, then please let us know by raising a :issue:`GitHub Issue` + on `SciTools/iris`_ Common Metadata @@ -120,10 +120,10 @@ For example, given the following :class:`~iris.cube.Cube`, Cell methods: mean time (6 hour) Attributes: - Conventions CF-1.5 - Model scenario A1B + Conventions 'CF-1.5' + Model scenario 'A1B' STASH m01s03i236 - source Data from Met Office Unified Model 6.05 + source 'Data from Met Office Unified Model 6.05' We can easily get all of the associated metadata of the :class:`~iris.cube.Cube` using the ``metadata`` property: @@ -990,7 +990,6 @@ values. All other metadata members will be left unaltered. .. _CF Conventions: https://cfconventions.org/ .. _Cell Measures: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#cell-measures .. _Flags: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags -.. _GitHub Issue: https://github.com/SciTools/iris/issues/new/choose .. _mapping: https://docs.python.org/3/glossary.html#term-mapping .. _namedtuple: https://docs.python.org/3/library/collections.html#collections.namedtuple .. _namedtuple._make: https://docs.python.org/3/library/collections.html#collections.somenamedtuple._make diff --git a/docs/src/further_topics/ugrid/data_model.rst b/docs/src/further_topics/ugrid/data_model.rst new file mode 100644 index 0000000000..4a2f64f627 --- /dev/null +++ b/docs/src/further_topics/ugrid/data_model.rst @@ -0,0 +1,566 @@ +.. include:: ../../common_links.inc + +.. _ugrid model: + +The Mesh Data Model +******************* + +.. important:: + + This page is intended to summarise the essentials that Iris users need + to know about meshes. For exhaustive details on UGRID itself: + `visit the official UGRID conventions site`__. + +Evolution, not revolution +========================= +Mesh support has been designed wherever possible to fit within the existing +Iris model. Meshes concern only the spatial geography of data, and can +optionally be limited to just the horizontal geography (e.g. X and Y). Other +dimensions such as time or ensemble member (and often vertical levels) +retain their familiar structured format. + +The UGRID conventions themselves are designed as an addition to the existing CF +conventions, which are at the core of Iris' philosophy. + +What's Different? +================= + +The mesh format represents data's geography using an **unstructured +mesh**. This has significant pros and cons when compared to a structured grid. + +.. contents:: + :local: + +The Detail +---------- +.. + The diagram images are SVG's, so editable by any graphical software + (e.g. Inkscape). They were originally made in MS PowerPoint. + + Uses the IBM Colour Blind Palette (see + http://ibm-design-language.eu-de.mybluemix.net/design/language/resources/color-library + ) + +Structured Grids (the old world) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Assigning data to locations using a structured grid is essentially an act of +matching coordinate arrays to each dimension of the data array. The data can +also be represented as an area (instead of a point) by including a bounds array +for each coordinate array. :numref:`data_structured_grid` visualises an +example. + +.. _data_structured_grid: +.. figure:: images/data_structured_grid.svg + :alt: Diagram of how data is represented on a structured grid + :align: right + :width: 1280 + + Data on a structured grid. + + 1D coordinate arrays (pink circles) are combined to construct a structured + grid of points (pink crosses). 2D bounds arrays (blue circles) can also be + used to describe the 1D boundaries (blue lines) at either side of each + rank of points; each point therefore having four bounds (x+y, upper+lower), + together describing a quadrilateral area around that point. Data from the + 2D data array (orange circles) can be assigned to these point locations + (orange diamonds) or area locations (orange quads) by matching the relative + positions in the data array to the relative spatial positions - see the + black outlined shapes as examples of this in action. + +Unstructured Meshes (the new world) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A mesh is made up of different types of **element**: + +.. list-table:: + :widths: 15, 15, 70 + + * - 0D + - ``node`` + - The 'core' of the mesh. A point position in space, constructed from + 2 or 3 coordinates (2D or 3D space). + * - 1D + - ``edge`` + - Constructed by connecting 2 nodes. + * - 2D + - ``face`` + - Constructed by connecting 3 or more nodes. + * - 3D + - ``volume`` + - Constructed by connecting 4 or more nodes (which must each have 3 + coordinates - 3D space). + +Every node in the mesh is defined by indexing the 1-dimensional X and Y (and +optionally Z) coordinate arrays (the ``node_coordinates``) - e.g. +``(x[3], y[3])`` gives the position of the fourth node. Note that this means +each node has its own coordinates, independent of every other node. + +Any higher dimensional element - an edge/face/volume - is described by a +sequence of the indices of the nodes that make up that element. E.g. a +triangular face made from connecting the first, third and fourth nodes: +``[0, 2, 3]``. These 1D sequences combine into a 2D array enumerating **all** +the elements of that type - edge/face/volume - called a **connectivity**. +E.g. we could make a mesh of 4 nodes, with 2 triangles described using this +``face_node_connectivity``: ``[[0, 2, 3], [3, 2, 1]]`` (note the shared nodes). + +.. note:: More on Connectivities: + + * The element type described by a connectivity is known as its + **location**; ``edge`` in ``edge_node_connectivity``. + * According to the UGRID conventions, the nodes in a face should be + listed in "anti-clockwise order from above". + * Connectivities also exist to connect the higher dimensional elements, + e.g. ``face_edge_connectivity``. These are optional conveniences to + speed up certain operations and will not be discussed here. + +.. important:: + + **Meshes are unstructured**. The mesh elements - represented in the + coordinate and connectivity arrays detailed above - are enumerated + along a single **unstructured dimension**. An element's position along + this dimension has nothing to do with its spatial position. + +A data variable associated with a mesh has a **location** of either ``node``, +``edge``, ``face`` or ``volume``. The data is stored in a 1D array with one +datum per element, matched to its element by matching the datum index with the +coordinate or connectivity index along the **unstructured dimension**. So for +an example data array called ``foo``: +``foo[3]`` would be at position ``(x[3], y[3])`` if it were node-located, or at +``faces[3]`` if it were face-located. :numref:`data_ugrid_mesh` visualises an +example of what is described above. + +.. _data_ugrid_mesh: +.. figure:: images/data_ugrid_mesh.svg + :alt: Diagram of how data is represented on an unstructured mesh + :align: right + :width: 1280 + + Data on an unstructured mesh + + 1D coordinate arrays (pink circles) describe node positions in space (pink + crosses). A 2D connectivity array (blue circles) describes faces by + connecting four nodes - by referencing their indices - into a face outline + (blue outlines on the map). Data from the 1D data array (orange circles) + can be assigned to these node locations (orange diamonds) or face locations + (orange quads) by matching the indices in the data array to the indices in + the coordinate arrays (for nodes) or connectivity array (for faces). See + the black outlined shapes as examples of index matching in action, and the + black stippled shapes to demonstrate that relative array position confers + no relative spatial information. + +---- + +The mesh model also supports edges/faces/volumes having associated 'centre' +coordinates - to allow point data to be assigned to these elements. 'Centre' is +just a convenience term - the points can exist anywhere within their respective +elements. See :numref:`ugrid_element_centres` for a visualised example. + +.. _ugrid_element_centres: +.. figure:: images/ugrid_element_centres.svg + :alt: Diagram demonstrating mesh face-centred data. + :align: right + :width: 1280 + + Data can be assigned to mesh edge/face/volume 'centres' + + 1D *node* coordinate arrays (pink circles) describe node positions in + space (pink crosses). A 2D connectivity array (blue circles) describes + faces by connecting four nodes into a face outline (blue outlines on the + map). Further 1D *face* coordinate arrays (pink circles) describe a + 'centre' point position (pink stars) for each face enumerated in the + connectivity array. + +Mesh Flexibility +++++++++++++++++ +Above we have seen how one could replicate data on a structured grid using +a mesh instead. But the utility of a mesh is the extra flexibility it offers. +Here are the main examples: + +Every node is completely independent - every one can have unique X andY (and Z) coordinate values. See :numref:`ugrid_node_independence`. + +.. _ugrid_node_independence: +.. figure:: images/ugrid_node_independence.svg + :alt: Diagram demonstrating the independence of each mesh node + :align: right + :width: 300 + + Every mesh node is completely independent + + The same array shape and structure used to describe the node positions + (pink crosses) in a regular grid (left-hand maps) is equally able to + describe **any** position for these nodes (e.g. the right-hand maps), + simply by changing the array values. The quadrilateral faces (blue + outlines) can therefore be given any quadrilateral shape by re-positioning + their constituent nodes. + +Faces and volumes can have variable node counts, i.e. different numbers of +sides. This is achieved by masking the unused 'slots' in the connectivity +array. See :numref:`ugrid_variable_faces`. + +.. _ugrid_variable_faces: +.. figure:: images/ugrid_variable_faces.svg + :alt: Diagram demonstrating mesh faces with variable node counts + :align: right + :width: 300 + + Mesh faces can have different node counts (using masking) + + The 2D connectivity array (blue circles) describes faces by connecting + nodes (pink crosses) to make up a face (blue outlines). The faces can use + different numbers of nodes by shaping the connectivity array to accommodate + the face with the most nodes, then masking unused node 'slots' + (black circles) for faces with fewer nodes than the maximum. + +Data can be assigned to lines (edges) just as easily as points (nodes) or +areas (faces). See :numref:`ugrid_edge_data`. + +.. _ugrid_edge_data: +.. figure:: images/ugrid_edge_data.svg + :alt: Diagram demonstrating data assigned to mesh edges + :align: right + :width: 300 + + Data can be assigned to mesh edges + + The 2D connectivity array (blue circles) describes edges by connecting 2 + nodes (pink crosses) to make up an edge (blue lines). Data can be assigned + to the edges (orange lines) by matching the indices of the 1D data array + (not shown) to the indices in the connectivity array. + +.. _ugrid implications: + +What does this mean? +-------------------- +Meshes can represent much more varied spatial arrangements +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The highly specific way of recording position (geometry) and shape +(topology) allows meshes to represent essentially **any** spatial arrangement +of data. There are therefore many new applications that aren't possible using a +structured grid, including: + +* `The UK Met Office's LFRic cubed-sphere `_ +* `Oceanic model outputs `_ + +.. todo: + a third example! + +Mesh 'payload' is much larger than with structured grids +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Coordinates are recorded per-node, and connectivities are recorded per-element. +This is opposed to a structured grid, where a single coordinate value is shared +by every data point/area along that line. + +For example: representing the surface of a cubed-sphere using a mesh leads to +coordinates and connectivities being **~8 times larger than the data itself**, +as opposed to a small fraction of the data size when dividing a spherical +surface using a structured grid of longitudes and latitudes. + +This further increases the emphasis on lazy loading and processing of data +using packages such as Dask. + +.. note:: + + The large, 1D data arrays associated with meshes are a very different + shape to what Iris users and developers are used to. It is suspected + that optimal performance will need new chunking strategies, but at time + of writing (``Jan 2022``) experience is still limited. + +.. todo: + Revisit when we have more information. + +Spatial operations on mesh data are more complex +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Detail: :doc:`operations` + +Indexing a mesh data array cannot be used for: + +#. Region selection +#. Neighbour identification + +This is because - unlike with a structured data array - relative position in +a mesh's 1-dimensional data arrays has no relation to relative position in +space. We must instead perform specialised operations using the information in +the mesh's connectivities, or by translating the mesh into a format designed +for mesh analysis such as VTK. + +Such calculations can still be optimised to avoid them slowing workflows, but +the important take-away here is that **adaptation is needed when working mesh +data**. + + +How Iris Represents This +======================== + +.. + Include API links to the various classes + + Include Cube/Mesh printout(s) + +.. seealso:: + + Remember this is a prose summary. Precise documentation is at: + :mod:`iris.experimental.ugrid`. + +.. note:: + + At time of writing (``Jan 2022``), neither 3D meshes nor 3D elements + (volumes) are supported. + +The Basics +---------- +The Iris :class:`~iris.cube.Cube` has several new members: + +* | :attr:`~iris.cube.Cube.mesh` + | The :class:`iris.experimental.ugrid.Mesh` that describes the + :class:`~iris.cube.Cube`\'s horizontal geography. +* | :attr:`~iris.cube.Cube.location` + | ``node``/``edge``/``face`` - the mesh element type with which this + :class:`~iris.cube.Cube`\'s :attr:`~iris.cube.Cube.data` is associated. +* | :meth:`~iris.cube.Cube.mesh_dim` + | The :class:`~iris.cube.Cube`\'s **unstructured dimension** - the one that + indexes over the horizontal :attr:`~iris.cube.Cube.data` positions. + +These members will all be ``None`` for a :class:`~iris.cube.Cube` with no +associated :class:`~iris.experimental.ugrid.Mesh`. + +This :class:`~iris.cube.Cube`\'s unstructured dimension has multiple attached +:class:`iris.experimental.ugrid.MeshCoord`\s (one for each axis e.g. +``x``/``y``), which can be used to infer the points and bounds of any index on +the :class:`~iris.cube.Cube`\'s unstructured dimension. + +.. testsetup:: ugrid_summaries + + import numpy as np + + from iris.coords import AuxCoord, DimCoord + from iris.cube import Cube + from iris.experimental.ugrid import Connectivity, Mesh + + node_x = AuxCoord( + points=[0.0, 5.0, 0.0, 5.0, 8.0], + standard_name="longitude", + units="degrees_east", + ) + node_y = AuxCoord( + points=[3.0, 3.0, 0.0, 0.0, 0.0], + standard_name="latitude", + units="degrees_north", + ) + + edge_node_c = Connectivity( + indices=[[0, 1], [0, 2], [1, 3], [1, 4], [2, 3], [3, 4]], + cf_role="edge_node_connectivity", + ) + + face_indices = np.ma.masked_equal([[0, 1, 3, 2], [1, 4, 3, 999]], 999) + face_node_c = Connectivity( + indices=face_indices, cf_role="face_node_connectivity" + ) + + def centre_coords(conn): + indexing = np.ma.filled(conn.indices, 0) + x, y = [ + AuxCoord( + node_coord.points[indexing].mean(axis=conn.connected_axis), + node_coord.standard_name, + units=node_coord.units, + ) + for node_coord in (node_x, node_y) + ] + return [(x, "x"), (y, "y")] + + my_mesh = Mesh( + long_name="my_mesh", + topology_dimension=2, + node_coords_and_axes=[(node_x, "x"), (node_y, "y")], + connectivities=[edge_node_c, face_node_c], + edge_coords_and_axes=centre_coords(edge_node_c), + face_coords_and_axes=centre_coords(face_node_c), + ) + + vertical_levels = DimCoord([0, 1, 2], "height") + + def location_cube(conn): + location = conn.location + mesh_coord_x, mesh_coord_y = my_mesh.to_MeshCoords(location) + data_shape = (conn.shape[conn.location_axis], len(vertical_levels.points)) + data_array = np.arange(np.prod(data_shape)).reshape(data_shape) + + return Cube( + data=data_array, + long_name=f"{location}_data", + units="K", + dim_coords_and_dims=[(vertical_levels, 1)], + aux_coords_and_dims=[(mesh_coord_x, 0), (mesh_coord_y, 0)], + ) + + edge_cube = location_cube(edge_node_c) + face_cube = location_cube(face_node_c) + +.. doctest:: ugrid_summaries + + >>> print(edge_cube) + edge_data / (K) (-- : 6; height: 3) + Dimension coordinates: + height - x + Mesh coordinates: + latitude x - + longitude x - + + >>> print(edge_cube.location) + edge + + >>> print(edge_cube.mesh_dim()) + 0 + + >>> print(edge_cube.mesh.summary(shorten=True)) + + +The Detail +---------- +How UGRID information is stored +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* | :class:`iris.experimental.ugrid.Mesh` + | Contains all information about the mesh. + | Includes: + + * | :attr:`~iris.experimental.ugrid.Mesh.topology_dimension` + | The maximum dimensionality of shape (1D=edge, 2D=face) supported + by this :class:`~iris.experimental.ugrid.Mesh`. Determines which + :class:`~iris.experimental.ugrid.Connectivity`\s are required/optional + (see below). + + * 1-3 collections of :class:`iris.coords.AuxCoord`\s: + + * | **Required**: :attr:`~iris.experimental.ugrid.Mesh.node_coords` + | The nodes that are the basis for the mesh. + * | Optional: :attr:`~iris.experimental.ugrid.Mesh.edge_coords`, + :attr:`~iris.experimental.ugrid.Mesh.face_coords` + | For indicating the 'centres' of the edges/faces. + | **NOTE:** generating a :class:`~iris.experimental.ugrid.MeshCoord` from + a :class:`~iris.experimental.ugrid.Mesh` currently (``Jan 2022``) + requires centre coordinates for the given ``location``; to be rectified + in future. + + * 1 or more :class:`iris.experimental.ugrid.Connectivity`\s: + + * | **Required for 1D (edge) elements**: + :attr:`~iris.experimental.ugrid.Mesh.edge_node_connectivity` + | Define the edges by connecting nodes. + * | **Required for 2D (face) elements**: + :attr:`~iris.experimental.ugrid.Mesh.face_node_connectivity` + | Define the faces by connecting nodes. + * Optional: any other connectivity type. See + :attr:`iris.experimental.ugrid.mesh.Connectivity.UGRID_CF_ROLES` for the + full list of types. + +.. doctest:: ugrid_summaries + + >>> print(edge_cube.mesh) + Mesh : 'my_mesh' + topology_dimension: 2 + node + node_dimension: 'Mesh2d_node' + node coordinates + + + edge + edge_dimension: 'Mesh2d_edge' + edge_node_connectivity: + edge coordinates + + + face + face_dimension: 'Mesh2d_face' + face_node_connectivity: + face coordinates + + + long_name: 'my_mesh' + +* | :class:`iris.experimental.ugrid.MeshCoord` + | Described in detail in `MeshCoords`_. + | Stores the following information: + + * | :attr:`~iris.experimental.ugrid.MeshCoord.mesh` + | The :class:`~iris.experimental.ugrid.Mesh` associated with this + :class:`~iris.experimental.ugrid.MeshCoord`. This determines the + :attr:`~iris.cube.Cube.mesh` attribute of any :class:`~iris.cube.Cube` + this :class:`~iris.experimental.ugrid.MeshCoord` is attached to (see + `The Basics`_) + + * | :attr:`~iris.experimental.ugrid.MeshCoord.location` + | ``node``/``edge``/``face`` - the element detailed by this + :class:`~iris.experimental.ugrid.MeshCoord`. This determines the + :attr:`~iris.cube.Cube.location` attribute of any + :class:`~iris.cube.Cube` this + :class:`~iris.experimental.ugrid.MeshCoord` is attached to (see + `The Basics`_). + +.. _ugrid MeshCoords: + +MeshCoords +~~~~~~~~~~ +Links a :class:`~iris.cube.Cube` to a :class:`~iris.experimental.ugrid.Mesh` by +attaching to the :class:`~iris.cube.Cube`\'s unstructured dimension, in the +same way that all :class:`~iris.coords.Coord`\s attach to +:class:`~iris.cube.Cube` dimensions. This allows a single +:class:`~iris.cube.Cube` to have a combination of unstructured and structured +dimensions (e.g. horizontal mesh plus vertical levels and a time series), +using the same logic for every dimension. + +:class:`~iris.experimental.ugrid.MeshCoord`\s are instantiated using a given +:class:`~iris.experimental.ugrid.Mesh`, ``location`` +("node"/"edge"/"face") and ``axis``. The process interprets the +:class:`~iris.experimental.ugrid.Mesh`\'s +:attr:`~iris.experimental.ugrid.Mesh.node_coords` and if appropriate the +:attr:`~iris.experimental.ugrid.Mesh.edge_node_connectivity`/ +:attr:`~iris.experimental.ugrid.Mesh.face_node_connectivity` and +:attr:`~iris.experimental.ugrid.Mesh.edge_coords`/ +:attr:`~iris.experimental.ugrid.Mesh.face_coords` +to produce a :class:`~iris.coords.Coord` +:attr:`~iris.coords.Coord.points` and :attr:`~iris.coords.Coord.bounds` +representation of all the :class:`~iris.experimental.ugrid.Mesh`\'s +nodes/edges/faces for the given axis. + +The method :meth:`iris.experimental.ugrid.Mesh.to_MeshCoords` is available to +create a :class:`~iris.experimental.ugrid.MeshCoord` for +every axis represented by that :class:`~iris.experimental.ugrid.Mesh`, +given only the ``location`` argument + +.. doctest:: ugrid_summaries + + >>> for coord in edge_cube.coords(mesh_coords=True): + ... print(coord) + MeshCoord : latitude / (degrees_north) + mesh: + location: 'edge' + points: [3. , 1.5, 1.5, 1.5, 0. , 0. ] + bounds: [ + [3., 3.], + [3., 0.], + [3., 0.], + [3., 0.], + [0., 0.], + [0., 0.]] + shape: (6,) bounds(6, 2) + dtype: float64 + standard_name: 'latitude' + axis: 'y' + MeshCoord : longitude / (degrees_east) + mesh: + location: 'edge' + points: [2.5, 0. , 5. , 6.5, 2.5, 6.5] + bounds: [ + [0., 5.], + [0., 0.], + [5., 5.], + [5., 8.], + [0., 5.], + [5., 8.]] + shape: (6,) bounds(6, 2) + dtype: float64 + standard_name: 'longitude' + axis: 'x' + + +__ CF-UGRID_ \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/data_structured_grid.svg b/docs/src/further_topics/ugrid/images/data_structured_grid.svg new file mode 100644 index 0000000000..2f3a1ce342 --- /dev/null +++ b/docs/src/further_topics/ugrid/images/data_structured_grid.svg @@ -0,0 +1 @@ +23, 28-19,-21101525-5-15-20-30xyCoordinate ArraysxyCoordinate Arrays23, 28-19, -21xyBounds Arraysderive point locationsassign data using dimensional indices,position in array == relative spatial positionderive area locations & shapesPoint DataArea DataData Array(bounded coordsalways have points too)my_variable* x+yare not lons+lats, just a demonstration! \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/data_ugrid_mesh.svg b/docs/src/further_topics/ugrid/images/data_ugrid_mesh.svg new file mode 100644 index 0000000000..ab7302346b --- /dev/null +++ b/docs/src/further_topics/ugrid/images/data_ugrid_mesh.svg @@ -0,0 +1 @@ +5, 7, 8, 14`xy1212`node_coordinates`every node has its own x + y coordinatesderive node locations1515xy`node_coordinates`[5][7][8][14]construct faces by connecting nodesderive ‘corner’ node locationsassign data using 1D indexing,position in array unrelated to spatial positionmatch indices with facesmatch indices with nodesNode DataFace Data12Data Arraymy_variable12 ×4`face_node_connectivity`face_nodes \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/geovistalogo.svg b/docs/src/further_topics/ugrid/images/geovistalogo.svg new file mode 100644 index 0000000000..4c68f0ee3f --- /dev/null +++ b/docs/src/further_topics/ugrid/images/geovistalogo.svg @@ -0,0 +1,573 @@ + + + + + + + + + + + + + + + + + + + + + + + + Cartographic rendering and mesh analytics powered by PyVista. + GeoVista + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GeoVista + + \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/iris-esmf-regrid.svg b/docs/src/further_topics/ugrid/images/iris-esmf-regrid.svg new file mode 100644 index 0000000000..e70a9386a7 --- /dev/null +++ b/docs/src/further_topics/ugrid/images/iris-esmf-regrid.svg @@ -0,0 +1,93 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + Iris + + diff --git a/docs/src/further_topics/ugrid/images/plotting_basic.png b/docs/src/further_topics/ugrid/images/plotting_basic.png new file mode 100644 index 0000000000000000000000000000000000000000..ba2b0b3329d8930508ece378d1317940f964f58e GIT binary patch literal 42338 zcmXV1c_38p*MIKJFf-Q4PGsytmXIYeiR@d1ETJS(*`tNI3R$ugMN-C6*|J3_WuyhA zkZ2)GNMxyOVP@XP_xH{pW$xVjEay4rbI$pkbCc|C&A2(lH~;|L7Um`g0KmaNv4DVq z|7m}^=mx;u?G`3`oucw)ht4+3nVwx`>92e{|EkXK7cRP=m=@pL#6QKyo zna?I$f~~av#b|ffiBMA8pS8<-7hb%iE@BdoXcdlOEzp=gOwvuWM@@fA@2C@J-%dgM;rKhqg*c2-0b$DMG1fY5Q5XF36v2{=I+Z*sN|~ zzlyT3de|hF^6TwBzP`l|9J4hI3S_43FB&uV-;D`3Vl)W&YbX4meSysEt4HJMiV6BS|4 zq~jDr;`n~dau!AQ-S}Dh^VK_TJ-wUfCg1$gLkv>b5qlSy{^(${sn_@!^%;q5$KoG%%rL)VY%=-E=$@rf@BSi+sp zs?5c^6w*I@Qg32<2`jH{q}`^J+Gu*;83=za5k^-$%@JQ-{Ri>Q6)9v-oCyj z?+{zPpRUn7q*EDhkzLe(9-%TA(iUv?$&I#Y(@vTa)`O1rE!tsp(@g zwhjv?f4Od^8XcEBhNFlU4QR+#Q%=YN z`z!{bkoqG+9m&A12}I|4>XY8@sXBbUVqZRJAB}PEIg7nUL_IK0fhwxmBHEwX$zmOd zI1a=P|KUFC!yU`i0a&s?J9ThSws&RCu<=))rVRORAHVH(dzp77~P-_Mg_%!A5b1>w0K%Q9l=Og17VWXKwnY1)BYUdDQ~ zsk)X{-Q?xq{?CDh?=@}9<>r1JdpYo~eH!t9c3^nzh>85KNg$ZbRTereyTT7fEqF*q zY04Mdk&F}^?a8&+XMZfj6F}OBI8l1$bp=F4-%R2`8`G)KY8_M`}l?LgdH|&dmi@AJ?h=r z`?t@How$D|r{1RcW{T1JVAXy^wTCM8fniBCzNWNv!n#gBqvOWYw_j8*b8+w9vfWgf zCHs0kQ;jH9IK_ffB;Fa^m6jha86aKcJ)ym7Vv$Nxr@_SqIyCH13$!ias z)#lF0H=lpR98{kUqUd3}3*8hXJ0IZ7O5apP@XWE+yr{7`(7!xo zGgZACu72E6z}*j@T^%4p`u7(4K9yn%HpcrRHtNFpn9lkw_5%%$vK$l^(X z?wEFjW`gP7(OEexTS(2;y9IL0mgT3@nC{uhs}DnY`M|!R6PZ)I%zZP0zlI(~U`GlS zWL1qHKQ@_6)4_EnN);=uZQeyZ+U}Bcv}MN6S!^1%;o* zDQ%YLGOUS(zVvKy+9;0V)Q%Ls+{d;;xb)&p6k$?4iRn4wuxRA@;3KbPeCK)VM^2S; zZi{3zJ#;?7=TGVNJS1^z*3))GOclp>ww`hbV$B>e&7L!kpb4bP`4}nH39Q?xOq+zi zw>>xR^JsSg-TJpJQhtELhn+67#y(N89&8r*)3)PjLuB;eO?1N# zFkxw!6|zF_?!KMOM5dM!65vrSa(6n=s;>N+Q21U!`)5tReHM)>HVQPGM95*%>KEZN zJ2;-~A2u`OVHIVhaj|s#MwPRcucg1KsTK2g{y~w|X;gEvAAep*kv~Ye7!ga3BO?Uf9;&P;NdVyk;kb zbkBtHbk7-6a$ljoE5R?JB7H?%e=07>;Surkylr?*j4)6bt6{r2#19FyHw&3_ zRdOrk;HS9w(BN=|wQLz?F({<&#n{`M7f|DHbdx%E{{xYKHbFi`{4L^JT0yT8r&RCq zcex7U_Su4qNzA1!R1P9~zmqZcoRc9V3ns#k+oCvnWr?S*a`!-sQs6|xJ{|}WgzMys zDNIh&4GR<29i`tXoZ5?cYprS#|ER?j%!#q}54L+OT7o*m3-5Zj2TS#Kb}nIGG#XI` z)Ul+Cppe!3ziBuaV~cWD*XU<>vdT`|} zrFprf-3oi43tT*lQZ%q5#p|~0;}&SnGimeBT;v=)frp4@4<<3)udHT1PlR5V?l#z!jiWiY zT61QAESck&_+{shpew5(LChZH?i%=MKjFEO`#%IN5^)l7ZryzPf2tO6;g-gsFzGwTlj!kqnEA; zPq#s2D4HWL+6`lf-GUSz(k?y_q~ca*PUyL?DEX3+bo3hNyL*cWdRq91=%qmwacRW= z&Ltw{S{~Qzqj3Ob4pnvhgkE?4l-kejN0P$SBygE0r zvcdv<##~G1{&XSNLWK!OJZa=D24`tCQ%1^jUgxcU6tXhH!&Z%wrX-&&Xz{z2xc&MQ zeta$?KG)1kxajg~ zt+WEYK$GXT!zkL=RNWL6+17bd#QYwj5sH$x7&TQss~k2CF=}JgPybpl+AjV)<1H>P zm%BTTd9}|f?FW|Wj5K+_x^gus$-*at=v&YBMh|SfY^b6t?PoF5R_>E_C&Z3YMJ4D= z{qJ?WPcpG81}n{lTblEDp-*3Z6K{d);uB47qJGAd8=XiFZehUwt1yr_yV?@IXr!kn z$k-kt%EiK>1&Vn8KUba>EV}%A9`^nr-;+(h+W9V4=z6o7YORgUN|8~**Y|Gc6}sCe z)$-ljObgD)0>kZF7%oZcoE zNU%%5suw@wjmj$q7pq9#i`i*v!a^hZXU?=Ek6+2cuw%hfx2+^L=?3%4&mvaT>hPL* z%dH-&c^eB{9Cy;BL_iok5`+#sp!1x;_;$<+-jk#U;tr@Pi7_s_jDC<%eM@M#I7-D; zb%@h_HM*0tX4oPgny%J4+9{pW^d1nI1Y2yX)Zx0!!JyzrmVY5DDXHW0!{YQ%c2|4A(X8(jfiCin}HqzVC?YomMOZi(Xk84K^OjRcGN*0ZBaqX$ht<9={ z;!~P1LcbR4+dXfaO0pKn#hG8zL_&e&RiGOX9)IVtaVA1u%;-xDM~G;)>aOxDgdy96c|tIbS+Mf;xBfnK*Je`fD? z=E-nc(T?(D8&B-b1Qx&JfF9~)4m@PdSf$j$2s74c9v1%IEhwoYA%l_usrI3>WIyZy z@te5nYgm(e_ts@(WHK{n()RO=?BoNNO(IyF->CM}|NH`Cj=bB?5=aGv0}>xFsxUk- z8LVo_q%_U=^3nIgr^;Qb(##s$kvvnAi5pIfRa}hiOa!&>_<-?wOPtzwVP2l2q5>!O zO_p*pARt+s9wG}WUF-ZEcCNi&8~F5jET5jh+U?E?>+E&bdZ=w|MgU~r-HEgj5#ed7rTH;TQ&8)Q5)D;A;z&L2DZL;R z3}k@1_XcanS0#bIG6%z^7uXfCiu+PuSvKE(sQH+u`Pro^wL%MYD#QSS$cVsL*^QSy z)YE@#ep1!--C0=vefu_kAQI7H-nzkaoSD7k30d?W#ptF;V+L28J~5*cPSw`dHV)<> zkbSi6D@255`d9 zd;LG&)0^p1-+0{*MX;d0!Arm4d!8ba!ZvZ>2D@n{XewxV+j+$Bqux%Uw`V9H*_dY} z?c3Cp|JQ+Thp_%zKF>mq^P>w1EDPYuW}ePRb`lupqTGnzHGOtWoaWErqj5G^0jY)# ztP_#`Z>6&k2=P5FQhX?##P}hNP$F)_p9BXmmI=&|Ow_Qk!7yUPoCgMgk9|J%6#;a*6D>FTCT?yby#?H|kcGz#|p0h8!rwHoLU=?g^Y@YtQduwB~)X3dH z_^k%E8e=hOb}cAM6KQ(|*||E-RK>DKy-qrK^A zA!r=(%~uS~2Vr-wJOpi_V3bPV9mfP<-1?}mXAQG!-DyFfKZeP=>fX?a8 z_GNzQLQp5Chw{Vz*zF(0e-+1*%p71Vj15x2I^CO+_a7fAV%${hNME5XK7b{t`+ z6X9Mtvlw6@{^b))gF9h7ZSr!cBCgCXC_KDwHjt1J_|oY4ERNhaI_jOkDsrGY6b$S1 zdDTQL0{Uas_u!K!XNmv5>X+6=f&Y& zdLoRX9WftSePU~w-x0#To&;|{F0P*nh&?_ReXw1Op>t`JI$l^WOes-$gn8|DNVvUp z?_K?8Pnvd6+RTAl+k%nX%0!jNi)1Ec0iO7Pd!`C5VBPJ@i)6~P@B3PL5?EFQ=zvNK za#grCuv3_sOmJ!f9KP_-sURPRdddQ!tj*Ba%nX+|iAVg7w*H2+mAjk4y5 z72Aig|5#|6lKjmoHlD>6;`6m+13{JCns(=`XM&GmU{5G;HwQ{dg;XJ?*|l}aZW9(| zAiSBImgYQSPRDM@7uGcUc2j$aubti^inxIN_l>r=?fuRB#T4qfvuw%*F)Qq#_)-1& z4Xd$6qnp?)D|UDnAX3iCC9t+LlW-oa?~`U>SEeK>Iv=%&q#xk+ZH^p`qvOT=XZ`6b_B(%SSK@#ebS|uMwA>p3UbL&l)3uilB1x_7hGM^TKG$mD6_7=T7>Rm{H7096 z#@;n)ohi+C#$@Wt@fy+?M7G2lYPQW4JD!cwyOe^-w#!Nky0ZS}wafRJ9*gM0&#hm~2A7p5U390^gpFC# z9)#ChnsfckAK!GL<*n|*obpQ4*7Un#qDQ&mpG@oID5%YT8qObz7Um#|38Spu=e9sx zlo|AFFJSu}?TFs$X%T*& z#*}c_vz9NCuehc#V-KS&(=)sGz1lr2ejrdexY$04`Z5NX?WY~-(TVxn1r+0OG}Tm7 z)?qHz<*s;MFTJ$}kuEIZ%0K+RuP)Oy@?l_LJZ-H;+J43yST~*hs-{**O<+A}PaR5{ zL|laf%DQ2MZd?dw*Yx&k?{J%Ex>2(Zy}(`vz8LP805J{lxJs=ueXu^L9ueo^hr&MjSLEB8~t#fqMWy>ZVl#=W5Rpfdej8*Rt^Y#cdG#T`a4AY7mh}1 zKO_qd|B+scOdcXhf{`j+e*3cTS4@wtWlSQ{XTs1y3lNHW$O8R$JgoBtqoXWms!>=|V=6=H7u z{8raT=Poo4&qoFeNCMR3e%^(_?3U20>Z*!L8bKo?vXta|Uw%M%sBz_dAxjbB18fPb zt_RyB?HDK4lXD?t0$Q9dzWrJ!-d68l<3mUaKl8ifCs4+{OHHVdR3grYY8A3nLMw(# zgIjzKc_QJQfC56pP?dzg?1$|zFSfe2m~K*M_R1171Cd{sZ95+Cc76YElqv?G*HH9P z)Pk$HvNwQ_F8fU9bAkoR=a1Re{dts{a`fMj1#sJrgc_^~%L2h}+!gsA+^>s`QjKYz zwQjLr8CkZEFlJiZt?`d~BC(ue7x7m?#rKnoY8CPzVqi907V$S+Gh!9Ki6?bVwdC(P z+HJsx_L_rkb*Qs-*RaJ}8%>^$JQR#QVOVEd2_M6;UK0JQ_Sisd^}X17qyqG=5T13e zpJuNv87-4)+WN_I2PvKszi|Cw!kac$f`@~6w zvE+|&vl_O|6d`ji8(5&Ew55CN+>yW?p~8nCx$R+M>!eTXfch1-iE^ zO@N4!pk&%P9=a?l1(V|x>V+#yPaIhvD0p?K+~LWbUEuC@b0Wu(u!?Cu)+6-u!L^|d z1V==7zck`nkeLixe9uP#_HGh-6!%D+W(VF<7<25D(2&6@JmHQmqNR^(ac*i8na9x?tWowP?h2PNs@X;sH2hz%f(q zdW(JWwQ}4(h1CSTohKAej;^NbSzx~U$CuH4C zAyOP5ghXzxV}QiK`m=E&dX-bMXv<%aa9{}2WbMp0J1I-~!JNoeW%?9LKHrZIrfVAw z!t!2)auzl!mtC!##KLrkD0^>xuLk~hh9?rZ+ zfN&LC9Il9{i!<$BF=ie#9KP)Wmtda)@yRkV6mZ1VhRY%9NzAWee=e&9JY^FB;LO=C zd}PghocXL^J;_8ldE5ELQESYL$JfAA6aN}iENv|S{DnD@rm9ZYm|8jduo4-@2PJ9Z zLGi>HV^FA5I=v+nOksxQQbeA?Dnyt(zpkEgJJaM=}+Fgfg>*o-NIl znB^4Hvj$_{ifp?|0`kJL8#sESh1@C&;yc!B%6+ zw$eM;yh!F(WiHn zh+xSm;qeXNl!vq{F^PWLP#mqWz^6>3r}pR1pO4g#iR=7qdE7ABiP15&jdXge{$HF1(Tux z5_>y%17h}}y>G)%n_YMKjneL60-ls4#AO1L9w_=?o4M)y5Rbzv2H-CjAU&doqUSKjF+H9e^3mU2* zI8d?=dyw+*co*N@N#sI4jv$MGfL;o|ddFK?SksTE+=a1QQYhIjJ_9)YVhBR@vQ-d| z9Eah~x8;L?!Xt#4CYNmj0+4zEEuhDat+)(rzJnwD@Y=m(#0|AyJQM&*=Ea*he;SA4 zT@Ux_$YwiG#>>J&HVlA980rbw@Q~ihqANs7@qg;oexw0G#R2FRX|n(;^Mrm^<>33c zYar_N>orLLP*rs`+@Ekh`aBL@9+vL`_7a)!>Z_bcj6IvLA(8Zo8`-c0(%*7C?;5Z2$ynTHI2$;L+I0TX}Lsr~pF zDpcEsh{(eL#E~|MJgBvKqpwu$Fj_Rn2lQ_W;2TVe9UFJ60lLh)N$|i4NebqSy(*TZaysowp$~4W+4<5VCOvYdiAe{-Gje~8h-S-*7{=$JM23_wQ;``JkPD2hopig#z!wb5b zAPVxvUdp1>P5tJc@n)oRx*7?9ZdI5$xK&^d{Am#U)BAzPi-B zli&LO-`|H!)BA_tfmPj&`4bFY7~xnbh|hYP+DDfS3x4`-0o3?`RsuT!Q)12-Zay6x z<(eI&5vK2#UP(U`;#|oJE`p{;CF0tuwHjcnB`zqWk zT%X6J4Tioz{aeC<``r%XfVXFLh$A)_Qt8eO-nyBM6jh_qMwzeAH~=N`rZr}&9F>0X z19UkdYzD-ULr^i6j23QpoMR30L%l-#!+_JM8HU2Jg9Fyw^)RU%eB%picDsDpU|CU$ zh92_ZNfak{R4I}3nepqNn4B$rJ&4Dgs^^(`+}qpF0hqKdL*2{-?FF~NK$@53$imJv z@~ZN56!lactTEsLg|nuux#E+vo^4vX}`}Atxvg4nt;UV_srjAJuN$j4MdWdK5Ex z#Ff2{c&DGWn!=9iYkdHu*Nlj&e{EtkK-zYU$G(o6{7anP=^1NgW)_dl1ZncXIe}Hm z+IzpcUjaOUb{M$vfGuDm0h~)~!0cuOef=;>-CP8kUe>^Nc@L3P>dGcygQ zBeb#PAk^c902_!JhXkG#v;M^q8u9}?a*K8M;%RTfdG{2W2A%zT>&r8aa4T~OIsi`N zM|aq_9i%*i6J&!IC9J)`O_X_DkPIlMf}4a_X#H;Dh%?4Ja5aBTGF)^BK9d5is_~yV z>g*lHBL`B1b|gQMHjrShh;f5{2dLT$+3E3=)TC+Z#sf^K98#@I>>b;5gob6M2)S=6 zAD-mx=a#e*>0BJ4N=UH`qYuvpdRf`XqdM-fw^uJqq%zXe(#}P%c)`79yvPL5`r=5? zw!$;(DNsv2FZ{eNU(oXAiygRj-Dt}+HP>v@qDm~H_O!yie(jCHkp42}TKZUYnM2BK zH5W;M+~KT1iPA&$@a5_{`L^Y)w$1$|hl+f0=8w4+f?Wx8%OJde zyMm-k^oAiM12-el(_o>2qofH(jyZa0zI%9-S~WxUhsp<1iDbO)@>CmS9?JIT8XU?R zF0K_K#?Zs&=J~`xD}MJPMAVj1sqsepq{?M z1GYT5*#XPY9exk}e%JHqx@Q9Jk`sJnE*n{p8<(&qxyc$Q^&sK>(;s&k0Sg68KSj{~ z=!FVQz-e|$CcBe3^F9-A`rz;$!<{>i=+X}UYKIAtH}NM<|89p8?TZW2+niZ*a8`jL zc-E7}pYXB#wbe%HcT3eLW79=xuJ*65BH$x@+UK|3*ZF%k&iz=yR295$4Zr;1HOG$r zbF|bGb$A*N7b|?_&wno;&GDF@kM&W9OA0^D`da(*?Sf9dS~{$1c=r z?OMq1dn-#xB~rr6tHxQcg5=RrDs;Bje(%ZcxjiKbNOzKn`#!;-V`{j_0)^5@zR0{{ zaBnKr5+5G0N?rOCHJAUWswQkx957;o0pOL?2Wc#8YwJok?K6E_Bscj!mj_}gth(q_ z4mrPSchU8r1F4#zM#(?UP$a#|jqoXVU#3Nqd;YhhfX{(e)muQ~qZ|5Fv zZBlH}wr6uX&r8X|%0@h3US9nM%S3XlHwue*K$g3X^;i6GX|)6NV!g`HTva6bZ>;(p z)9RQlehUK&vd%sI(x!AAW|l7{GVhxF$ydsDsr%0ggZw^H&(fMjwVByUEq7t3-_wb% zptAWZOBM_T^Wfn_W)d}MF9bv1yJ2A8(p~(HO%!>M*YM5h;8)~VA)Q&?`TShkK@&Z2 z0-&~{+vesz?Rxw1Hr9Xq9zK=L=;3iecaMX5L{emLe{mD_eaC5bSY3Kdnktgt9Jv(6 z7O%H&opyVRXeF*42(Sj@v8+d5sI$!efm^fDmtij_#imf4-l|2u`z(vr{a;3N;u$}! zNfh-+NWog(YGOs+U?n+>{9906e?I_2mw%-D-7H}5U;!DveTNF{0K8Jv~;UC9AjU{ zRNz=EGSc?svllx^fq_F4|KKeZTyZ+{`t&Wi5;bLiMMTkwRYY7H@S;YS55%h~MQ`;6 zVDa$=yYI^;uePTgASZ?+5@)}xm+h}f9J_7AY!R;T5YUnTnqSW97zYM zR?fwZEYktXBPJ?(GcQl;^Umb?bp0`Ggx3M0i`V%}r014X`C8|fD{5*!hCsbo<9V53 zGhPa_#Ln|TEkg$blsYB82OZ-hi!C*5W4tLOB3*fCC?Dp99L^sa=$LO zKeW6gQQ+QVWCdFHh6C(W;;2u5Lo6%;V?Vhqm<28W!bHLxZ5Qnu^=)a24B_n_e%(3~Ej9LIMKS96u;y3c^?(p`l4o_*mxl=hmf^rrI9bp9e^3h^eAp|Vxq z?Tne8;r5R*uR9_cEOC$$BMWZ@&qfY=-|Zm{axt-{msg{ngbLdmz%D`S0&)6>UBe!o z>xjFP6(-{s;5kYp_XAU*)9hTd-3gaPbxioj#Obo41huv7mj@}2I%8L}xtvUS;8dP# z-9}=o_gzc0l%xS?q4-FNj8I*)6aaO!wCj}b5wYWx7S1AO{U7q?+LRT)Ls1!)$qeTI zFZW$)^Sim{eE+9U)30o^Q~2QGL1Ut6tgrJI@Jj)VMgXk*6qFTsR4jz4J+A}&z@Hi% ziR&t)WWmZ+0B(4mdHJyz0o@^koLs75Pl-7^-m7wl35xF!S24n&goNp*?>PaEe3-Lj zmb0`IO2dgqOJCn@jZmoo&>h^v?7BCJ8E~|VF0U?Gw$?}kxWFGIx zYIgq=e=c|kdFrp2R#r=hyYE?RttK1^B{UGRfR^FE^uE7{RbKT}>Rj~*1K)67?o3a? zwM%lSH``yh%ACMGi|I|GYc3vwSvcSi^ZN{Tu<&I;{VGRyaf5xJkm??Z4PMLF zumV!*|BqaN3*zaF4)*W@^U`-Wnv&o9I|mtLXWiQX?ERyRr(F^Uoukgcc@xj4Z*jD* zr9D~uvK7`As!Dl3O;U3^c)X!)Xl-;tHBomMPX3yD9s9DNg|nyS@bysg+@=K4G|P|6 zt9&ai&07HxwH2`f4$tA;7hGQNh*f(sCblBO)Ni~6x)H7vv1i=X#MAJ^DavC3g`2*k zO$U4De@7?(zQjphsWw9R2mk^7{Ds5I+Mv2}%jnzltz#|okXU$Dn zb1s%|IFXmLEqB+^L$pl;iBW72w-48hRl>3!on{*${NN~xouhFBK+3s#^=d~)hnwA1 zcY^G3Lf=NQfEB11yzoib2U^i z(S$t@j?UTEw$ITqtDY7O`^9c*9@n~{gZ2x$vXwY;Z%QJR4276m&87JK$H*yEyB+S4 zQrWA6kYoXRY4aQNPvGDK!H37+2drSM`B5chZo!G?d=yBc5uC4L0G;~}13fBt52LaO zWOt*7Fof!h7I0binLFhu2QAaOWfOv)?uSx`ZJde?%aj3Qd1le3J#09hv;U@ucsEP> zvt`2X^vBONK|}-Q2|O$KweS8=JX&@Guzg0}@==2Eo43qY^Z84tsN$Sd5dV{M_CN!Y z6u|qc1BNolOVxnEGYCEVm}K8%I0EOk`6j@1_w3rR1`K!G+A5?8QCFyRct7QX+b?Wo z7I2*hew$E35}9LTch8EZ%ax&xyc97j0LP&VJJ1d*a+BpC_@c*Do{z%j6!@X-<1ERM z2P*5Es@GWhYHMtg{s6d({Zu$tTrJW1lnz1EE-4<>TEXse(M1w>b5*DzI9D^|h+r`^ zb=-EN|G^t{8IzpIgaRlkD(V^a#{xGKL6|UB7gC7!_wQlg;3t1hKBZq!d9l>6QGa6{ zcFpnDrEQ88<5b&Sjm;385BSD7>lmiKwMWb&;jteXAy+SDT9nuU4Ts@=5U>V4{S(*3Jxvu_`zSi zevl$W?L#C1*|%Z~1SnzU*j=wSoBLLbF4wHb$7lo51=Ri4=bRsW-o>etm{|+t}2UMQhxC%8TB&p>w|n1S!k6K-LN^+=UJ ziTYC<#nDxzABpr(1$&$TOYh_K#T3=_ZL1>I>o?(BGJWwlTHNVLc$^7J@*tt}g~j<_ z&)jSA-mHe^Qpk*e`zD^qYspC8uflvZ-8my>qs*xx0vBkeThf-c_ux3WUE2i}=cABE8L!$BppRv_=p3%szbS?yQ3%j@x#N=@gZgI?d3ba{|wIK5z{_M|oz z+Pb=eUBittvjagtlKrmEqN;y3(BvG^*nt#5|>@}I6e z{#z-x{_AP>zn@5>TJkY>(Oz5A4M8<>(-XgG+TtO7zBMcutO-5!NL|Z==GU9#69wjA z$C+5l=^37T+Gc5C*tUnlLxv*O7=X&mu_l6;V`|Lj7tTXiy(iX=co6P=cMMd$cQZg5 z0BeC{*yRgQ^9NtPh6=h1{HS8|74S9d%;G@ug^na88K(6SV(OvFZBW*8;c z-onD9GQ~)PM03~UNub1x~+f|Pk9}~%S zksL|4b^1*P-LNDNRAjp68wce*l^xFKK}$6CEMaYlkOe9@9Ju{)Jj0-#0_@*ycDKS0 zO|M=S^vF^q%-8{WzZ40i=pSIoy zpdhcCe=I*zD*$sGTx2WQHeeb}q#K@}kNDwf6`-#RXr?g}a4Z2oI9EJ`qy@L^%Pe@1 zfVNppxHBEKsJ6MjyPIg0zWLY0Rc!cR-=Y(oLZEG-g$#w@1nGA=9|gcyd*HO4pCN!= z6Je(J<$!8FMG%hH1K{-*#_Q?Z2y}bs9QMA*1d zmH-q2dTbnk!M%%WGH`}ZW)NrDOjwqi`*y3#*?fjVVjPFRW$e<}WUa`gw{V4*Hiyxc z#)EE-1eW%+4VY^V?0~yHII|sqv_M@v4H9iVE!&`mpS$0#gF8WvoO4fv+3E;ue_NT&XyGwPt_7o^yS8@gJb$(TJe3^Shf5V;&sG zgVnK|mBE;fhHo%n02Bo+2Ta}jZ|6;^IWPqE_1Oj}QjntBzgadAgw0|wh;Xetw*)Dj z0wE|)a9N!f&|$On!3_9i5IQtaAE1RKeg&=)1VD38-IUoBA%UeFaEkrEWk>v8`J%rY zhC@)|_R?!rmGQWdcTaAZ%)~yMMHwCNxDG-6rSU9%!;@X|u+8w|*Qb2I|E=xiAU%U$ zft>wv!@0xDawQHdY&g=!a;{@>FFwL9A6J440RH=7tL2{<`hRgE@P|^jTGs?M&Y@^c zO-;gk0;o=d6ubIBIEE(9Oox(zAJI0Jke(Knv>6ltF#X+Qv+)xEQ+pN`#5d*u0u(SR zT1cgF!WzVz9vo#e^@55LyqU{V(jn8P*!mNPTEJ^)S`qWL{(b(<-@zGJ3{Z!*01<VADfs;AG;|p)i?zy|C6RjaTGe~=|Di@j7=&t)F^Ms78CoLx zItK@9vfLZ|jDgOFqUd-sC1|%dOQ>pd(1oiyM2f(g!X>7=Ff|?856K1-Y~@V`eT*u% z`m)%EtMFZ?muscLMvqNFBwUd_0S6`xa*%GWzh-J0kR|{lyJ2Ja_t#wLw#}Fh@<;Rx zye5#(+01hm9O|a-dk?*5XS)VW3QGZlJ=GI>P@ixt_Kx&okgGirZRh-bQyQl5o0&}| z=^S`FVY>xB34D|@L38zmrllEwE@;Pbsi(c^G8G#il23QRUmtH4549fW{7*{gyXhnU zNuC@IU54$=oV=6keATvQ{9rHCssb5!I(K2c<05><7#eir6`qktFf#u;dCFlXqHf|8 z{ugU@Ys-h_gY?=C;-n~25whrT+3TDhoVVh`Pf@h}$?y^nJW7DdYitY`Vum}p<32RlXqX%~EW^iWgiN&hIno~InL z;Ooi1$%SSsod4}i9QMk#u|7WV&=X>8Z#aKbe&l1hf^(%<3NveW`NPdQPL9JA=%B`2 z`aw@N#do;pRxk+O3(;mo*gb^$c5)}pM;10;K;H&$ZhXI)%+DBt!wOjV7|9?M&;+%> zxRSt1Q1$43Ia>!6Byk;esq{GZEPJ2>Mzc=vHaFqR>r1ICZ*rgUSF3ewF1niyb|<@1 zA47woX~TXf>qfOeeZ>3#Q{c6P07uf3>yBF>Sru~*q))(jU5!u1+;dS2ovb`K4Ix(`2Y zAxR(PF@ba?=q~mfTmM+@q7!_>c@g##q|E(YXiGQYg}Zq=?V^61u~aTuocvjIA2Z?f zliBkQM`&5v`SzC&X&&EoMs+3iExGA|@2e}K&<&0xT>k2DsUR#_t@)iST2T74^pRSt zrbII@eW3F3@};gYFi_+jJ3 zpNj}+@+YuQXVzb^@=krEdNj-F$^)@-T_R}$`0dA+>zn85GriwD!!FS#FZ#5X31)1;-BDhrseJ=(ZIp(Vz`Htu!iGCUY`Y@LHuw3P>ZEmdPM<6#+CdxiCF4M=CHL}5G1)NfSLVJy(rS-{ z4s;K1HHdGI$!jhiFKNE;w$I0OOj_F|r!!!Tm71Pu>31MK(}cHql_DR$`3f6a?*F%)(K+rWns1YcJ>1*bGWO7p0@vL z40?yNmpiA2uh0Cs&MI$Nb$XDtb1K^k9T!l%BdJ~$f`PEts)oqIH(E6z$-FGLYSv^+ z-MI26WG;lO!TNN5a?6A@E&sPm*W>&{Pu`@9uHv~I*l;;f^XG^E2C7R+?x z2ic-ej2M)k`)1OgYY>|HEQ4A0S*PB``C$4+v&V|s`(1>eXa9xEV!3m23zd1keU}H_ zGWcL2A{t$X=vsMc7HyQ@i8ZN&cZ1#yklEA56)#9Nhl<#e_!7CXLB zJLV|`>!8Fk6oicfo+IpFR9U}xhZ(&|ZYkI0;p5n__mvgP+a+$_uY8ofrv{4gctio# zH3NE`&Mk9&r;r1j>%Kc)RR!hS%w0QIU-d;k;#0*blUH8a_?|omuDP5%E{8cojlDX0 zv^H6gJSGo1bgSl^Wz7Y zeQ62vR$;-$R2bt7Kl^6U8a!<^>QMMTJMrvd<3Y;dC8O(pi|qFa zTjAZ*nGXsFvSAoPe2%~(SZ5fo+Dl5;C!K0Cc^4po~F+bH!M*I+)YJKA60 zA-BHgr%K2FG4-W^P=0UN&mdcglvMU)2~pW1%upiPN(eEEP_`o3&6H3SGPW#@HCd-f zcEb9zRz&&d3UQr2i7w}Wo?G-R z2?WNZ$q~A&pY__$6o>_GlaJid5S^$+&~HR1urWYsOz}|Ly`PN#R6$O?4QtQ|`8~#= z)2w)sVQyx5&YL+}`duuwRxA>LxmweY*|-AhzUQ~q{rWnil;4INx0bD5%%9^|ATwZ) zZI;QB(hFVBRCEPJNRhuK9^7Gv&`?DpRL5?{V32$5Ki6?lX_n;>Gp4oVqfxm(WFbl|2byEJMT0+Ur5=@G`BRgSgGB*9oefA zU3wHCY{$o$Y&{-_GecGd-q6u1wz^-fMK16O27gykA^`VS=RlRM>0P*vId?)NM;Xz#V!=y*aTwhcxB6Q@RA zkEo65vy}%0Y)ns{i6Em!gQB(kp!=Et4mVeGlgr1f zDd2+hXL>;#YPx1tw=&_%Um>Lavkyq*(1ejoZ$goY1w&Rfx?tqUY^?xDa~L6UiLb-8 zOx_jUFc}ajShB-iW686BSH18C z=D(O&`*o|U2aJhXujj7D4%&La0j0N_8S=>r25C_wlWnrA%?b4d2cgR(xJ~!lmH*Nj zkt%=Oi$5D{?@>HWErfL|*o}=UnF$P;0Ku4V*FtnW&tiP=zz70)C+{UodvfBpH*tuj zNhfcl;%;F;8d6n0D8=%v1dqD(;O-g+?Zn^6w%``tUioXWy2t3!ju-s$nl07@K*ykN zkvnZ*A06>{cftUJ!TfhErGvfEfO&-we(uu=ee(~6p3*cEN8Wqm=d;u_ee2${aqFfX z%R_ft6u_2_zp-hKO$c|``_x+JIA z+Ih}(=|SVBfJ2Ajn$6zw(};ao)K4%k1~4S)@$XKg&-3Vw+3q>sYbnhue%$ zwzW4^`MCOQ8i>n?NViu|M3MtkE_9jy0&Xkw18K&?x1n?^^O+=7MJMJHqaA zKjXA>r2?pN(j&eYYo_xjM&}()CB-vCeqie8LmWb2){miE6{BI>97eZbQ%Go)qKT(J zCtk+izVqYq$nV+5#i}fS5-wygu|l@8&TPV9wzn_l@U8rI1-+G+nTe!LL27Erw)1>M zqikEpTZJ=NMV@Z+)nrw>O(WDghm^E$tie%_(x`ybP$;0*E40Aab4wW?C;;7u0HNV`iAijbajrSIGi^KGp8&oB}n>tP%h5z-tS3u&^R!Vh0Ox@md3<7_Q_HPX)p6f zsCjww4Yh5|pz~i>CoJ~po;2=K{t1<36hR^m6`NCXFPP~ihccaE@+gqMN!Ul9rI9}c z&1yLNV*g5S@Tz{=t2GVxR0yoAIC&HxINpE-iM9?2cZ?Na&{DL-4u6H8apX-^jaF`_ z16kZ-TD1_7b|Pt6+|IMS zmh$(5zEYfOT9w2oCvT2hiN0iDPiJSfjxS~6;_qubhFA}*Ti4qBQXWeFt7qEksNGPp zOwIZ7$5K-_a>is3^KeRH?^SWq*|$p4l6TpUK!`Jyqc9msSl%0EG?W;H8&UOeznrV9 zXEwLM6W&RPw87+ooQ+Eg{=daeMU37H^6j9_O6}*SZ_irN+Met;O`fS9X6T2@Y}bZ$gahthMY;eo2+DVmCm}=Ld4Xp&e7(kCohj`p^TYKK(DB zr~NEgP}xHWrpsNq<>vgrnkaI5h%MOR&fbn=+l^K-D@cStK22kM(5KGGMTYe5>0vpQ_X z%M5_C_lcWvi{kY^J*r=1svp)WXIN4r;e zZ6NWh=wG;uBjaF<{t<_<(V}^?ks`irW}6~7J=(lUv>-F-y7u-m6Dmkr7VdgBBKMCS z2ap;1w?Z#W=A16Hz!*B?;&r4&$3##i|HnV}Cw*=nr#saD(`8(eS0p&M%0NtMB>;au z3wdtC!bB`0usqW3(ofskmn6LJjsrOx{Q+OklcBROVwSfg*SHEs?|j>d>H7BYa$ofX zgkU#CK%*cZy8ta*HE_z8W0Law6P`(HD|+&T6nSsJpmEz=dzAA&fE}($E2%V_ z@o!!t!YqoPphi;!Pem__J|jA3HhaT_GuG~)ZG<6(>?C?`rB z#67-{#V?%S2CvOqiil=-7tNS<^uU_&XUQ{tsQm1A=Lc*rL0Wy*e)(jumV#^TkJ0gI z?g;nO)})CnQveFqv3sD8Kjtuw?S8=W-Kl`d$;ambs4tYanMH&4+{zzcJ{t`J0;k4j zKaGo~rMLp|r8}PyW-{YK)tC1K*cSH+dYf2ckG_uKy#Y|LZ!JTLh^m5gCR^G;VUo%T zTf@j?jw%M3d!5X)MQ5M=Z9fVP7#PWa|H!3KLW?Q^p{!MW60nJ#d$bS=kdJ|2)1XWd z(n;o_5KcmeSTaCMm_Ix18e7QF#!f3RoVaiEDbDUf*#|ymaq&tUK(k>D-W048c*yy) z0?>Rfpsxqkv4$3NA>s0AqkpR{Z&|S3cbc_9*5fKHsqq$)ObifHX7E(4OI9mpPlIhe z^ZFWq@QmZs;wSDLt099>F49kM{D_lqrke23uTbT4MRSUzNcu}Lv%Mr{a$Fg29yV0B zI;+|iKaQQA(P#%^r92fT)#fjl-bGiwDLjkeWA zDoNE5-H}A;EsRX=Cz{kzjvjMR9|Q+C(B%pqTs#K^+iFiFu3;cnbT!2>?}J3CF3L^& zV|69|pt^b;e}K0T@!z}}+W}DwnbRw|4bFeE+?RBghoI=D?~K>NX-HaeLVtkDQ<69| zHO4Ny#2AX#&)bW$`ugDPsjgL+t{8LM)eC#Zq59(tfUjr_;@b0pGWY7%)G0czS;2=4 zx|xDH<7E`h>?FqIB!UIMUH3v%BF1mIjne!nS z2!8<* zITN~xa^S+mz&hS3o28Da2KLSBS(>%%JEQ)o?d(USzC2AW9#t|k%W%`=?>1A7k}pQq zuktn!z8e-ZREsrrub;DO&}3#1o=Oi|gqSN^>+jOD#2uhmSEx-Y?Ns<1V`YQvpn)y1 z&{>n~bCHyYmHC;IXLq0$iMF_Xc^G6rB@P+3roQ=>%lL0&0r~SfBUd3yQk6vt<7E*j zV)kypm1~@7{Cx>J+k~1~MODH!qM|X{aV& z6wQ5xrqc|{2L6%}SOFWk<_wK@wG(^e(2JB7@o)2n?{feefA>R%Xhk;TN_O3-&#;Bo(oL=KJpaF2ZCCx$KSq_D z-tP*$Z6Vsn$DfyAbg6lx)q=s`(Y<0vgsvmZ_RZnCfJkE2c_DQPHzu&yN+uI<4iyhkD2iOUh!e-;mc5!KF^1gMfQ&~&$Z;?qWuMJ5{}u&|Es zvO@pc%u!ft8AHoCy!lQ`cs@`ZT^Q-#<6mr&|^wA^lKD5+M?=x_HSzJ8+(X~M* z6u4{|l!uDrx zd-5`Nq<+;k(90?^?sT1=_YWSdTZgALuY)ZJ+{Ugf)GQZO?Tq`oi`+a&OHNsUJ?Bto zG}hy&%sJhAzeqAEkA6`(vZz3&P2CbM+9(&O3h9Bep`f)2-8%14lGmI%h3YvthITeZ zR1`ZfJ29@O1|8{|yPl6@FQTaMT{?(0<7!wmwFWK{_ z>>Px3le*0+E*qK!R(~x~BD3bo?_{Njl%6*>!{g@I<8WrkYSx9T8K{@>L2PN2Ve~Qn zN%Ebe>d?3WgQRwA-6lag1MZ4E&feSi)5)ws;h~|?9~4F27!ZOtOsRCr2%I1Ubcn2j`-KM4@$o@*lgdtXK*C^Zr%XH5LTbz_5zw% z6-hbavIB8%CEYf4KZz0pG^@()NO z`#DFPpM}NpCG($zx&GNdmz}qhEp8&=`Oz|u^q7gE)-Ac_Gk+v3#1AUj z5OI=tAWUbJ9QdRY2q6xfggP!jOBj7Nhd+s*1D-Z;47wk9sHNrZ8ky}lew=o^H0yr# zbF|kLaWX7gdS5Y>`Fv)AaFGlHK!<+(2q4dRh0>o8!93-?Bd2ohE~S}Oum7jlk+1oc z#X@W?WT0m8X@k?J7i5T_B&Owx*@x@}%v6w?<(w^NXko$px&L11<DA0n zK#xA{67lAq2Q`W`g(AEH%pI+c-vapl$w;}eKl_CZS{OolP9xn)I@Fr`nXGSNJrFV z*Op3jA13#bgt)lFp9SJaQ3%1h@!!#Qf<(^e7kPgu>nJM{oXAN(>dg%eFlzIdigOi9 z&BZxCPI8G?d^VnN-UFlYx`4lK_QfV^ZU%kSJujRr4JI=S1WLLnKYKfvye|Q5H#k&u z0Xj$g-@D~VIV$G#H~7=1M^@`2J+ZOYGS27;{(D6*ooo^b> zMc>bk-o&-myp^(sLCu8%h2}v8ODxLA@LZMWi|Dx*DQhnGdQJoAX3A9hvMEAC>TW?r zPv`*gYYKaekx&!y`gIByh z^GEESqtVB0z8$X2jNIJZM@F}l(Ux)S%SZ2azJI3ivgE6Fug)@HMv88W0KQocH>7U) zI=TBj=Z}HdM8|)|NscX+Q#$l~lSpE6j(_X0NSd>%#sN~h<4PdOFniS3V2`>|<*zLRb7rq8;rlD5hsoK8#kyTLPm zkpn(nNxN9?v*F6Zzizh2TxWgq-#Y&JCD^wcfKr06((4mv6Hyv|9vS3nl9sG>j)r>V z+Zwl+lT#^p;ANt)j;e#XyY6eT+wD%g^<+ak93CqPCS?K3N)F)*b59%e7d0S{RBp7C zp3YL~?bB^#Gy&nk%MR+8QuhHQ$?@(Qw>=|5YTFyNizimmLzNHY`1K)$Zwz^SrU=RO zLOz=qpSmN&Bg^#|XAky}$&h&3A65UBj3n%DL&uu3^1><98ek4R0P4%^E#eJ5CnH=^ zL#(Vqvf~I_^_P_w5d zqA&gCfGn0VacA6a7db?`nJw;{qELggp% z#Yb1=uUxUQUG8&1G{i()OMchv2O!L7iE}j2L`dl$q~Sc;3rcq|Q6-^E7~JnW2~Pqk zWYe&FsrAN0@iDnh_S8i@$NE z7=|{J#6x~(>G>1VrJC;RIK4KhOdszKi#!yOCGqL(@dJ5PhyR*Ey6C}>R-e>(nR~KX z5h}ldG&j*@6s3sk5W5GNpTGs?Umm95lMX!5F;S+7&#us%fyKwbw{zo-HPa`cm)soC zQ0%h%oYLFR3Y}j!!wT47gqizer~xG{QuUmw7t-HG8G3ty3JS!KUN&@SaA94W5<1Q2 zz#<{)y&S+`l=4ik@HjG#RV5n@A}22=RARyiQ1{D*`OgA51fq)$-yuL_NWxiJU~+$S=b2VK?!mwvzAXk#5Ma5HMUGoT(6<& z_{0K=B6pg`kpTww5V;1yLOU=cLXtPSurx<#F^TR$eVC$$2k1d7h75J)zciL=4%ArC zza{fmRcdjrLl{-X7^U|uRr#EOJ%ReaO#_Fw)S_~d@`y1ur%VNL?pOQ#><`pE-jwU`2ojSqA0X7qCl zFlK~SB5u|12Xj(}gs9ng?ql9RNpUAT%?H6Qv55qn-46>^`vT+I5eF|(N+vqW80b4F zlrfh0=$AdSsi_(U4&_&8&m!C zcMvx;C=NR|0$SdH;><9x$DejZyavA!PilR?EASLr|3(-*J&bam4rG{=TN8(!O4(Io zf*zPO$W%_L2bv^b1kf#UQ(Dd_HsGT3ID)QU?m23c>$QtdUd4an;)Z;=ZQC!+m3m!) z*v8=!;FxKX6(zH`e$i^jO_x%Jkajm zUw!m}8fHls)zX9RVW3OD80agGIdR;Qh<*ycv^^%{At$p_5OSsTNTPssT zU`DGc%`Lj9WSSK=WrDT zEaw<7U5?a`;t_(F6c`j<_`@W(eIW7c!o<28W2mEM@)?>t!U)rWQASMg1G%?{8ZOVu2&cCM16MXwBU3C_ z;C*qZ(f;0`CP`%f8uuL+c@-7Kt5+W$OoOi722=i=Mpc`&o^GIOGnv^e=o#;lJW(w2 zt6Bo_8c_k_e_m&`D$6wMg0UI{I%SJXHp+0nL+dKulH zKa%?aK(XD$ryq$!k1w+TVU094;%AsslA7hI#U}~xH+dML&wfL}#L%jUb8gMas@gp> z$@;u~F9?#k0}Dv`10ij?z*!ov<_;vuL8{y|tatQj09^G{Ek1OX(-^+cON|uB(EXx> zmQ#584S4(_ZTk5YCgC9tHTQ9c8#U=#Ptl|7DN>ZOcAS}QZxceDV!yL+50-X1*K+Sd zgFoe~gPo$|T@CEMX2HtIXdWuUXpCsf;e8Goo!8#Sc>o2VC-B?#@sM^*2m(-scXnrt zj*~*a?)Qcqk1^fmOxEjC8Hove1&ZO9HpS9q#FC%wA4=N{=uKiCQviOt3 zF#W$`R-gxUHdmVxQ8eJ55xLLAi1_6O)ZDc9v~?&ZE~cBaMI3o5+GVv;qXqP*HVK)i zo)9KsQ`S@PHCuB;qA9}5?mIIimHb6YNx3#Yh-$E`^DUQ%m5b5K8>A~-KEUSD7hMQO ze=3B_k0atA;x!^Ww7&!XKwyuR z$YFRT+51Sj6UgsD{<1#}?x)y6wgttgo+GuXpj!+XXGsWHT}Cls(8=VTDZ=fljM9;p zvH#Pa)Xw2X&t5>^S~h`wF@>FN%L8K-*K%^K$8)w}?Te1vG;z6m@>9`@O$AnpE%nT3 z;bl}(g@0Y%k1H7{i!g5JlR4|D%=0ZN($28{uF@_8==+o5sP}GFn!Y4b#JMN=EOiE? zembbifre|gVOT{HKjN6-5uiC0=6OR3GPVJ{u)xK+@{rK=IParoqtDF!nF5y+13t9{ zlS`sRaVGb5uAjTwuX;fL?YR~P%W`XU?bQTD$rLGox!rxp z{}l$E@pz$qD34L_?WthZLw@XNJ8>O#imVe=a;Q(L&2kd`H<76&BcCB}-$db$(Nk!g z)tNgj;jSx=-+8c`bEp02j$`VTW!4`VdpA5D*XOK7eFtCmF6_Xw>;fg~rr`)Hok;D_Zf9k8ZF=>jvR6HFNj@kxv`giwK&|HuGDks(=5cAA1 z=m`_}a8#8v%Ur}bvgTo4+Nkuh@utk{Vyf_7c_wnI^SEtX5xs|V&*T7;pUH;4Y{PS( zbz^vtNQpom$n5FIGDia*YCysdrs($kwRVzLYja#t8`zjSkT7!6`Qj-p?`RmHotSmT zkYSqze(sq8qgo!rQs?6|Fo@*DEgYZ3&XxDWlS)QbOzjjLL9wUu8c3dKGyg#2xPb~s z>AzHk0jV%O@Zs!eEE^2W%-nmT`FF`msjY1tzea-3an!z<7P3nM)51a|Et|0CNcqX;vS zNG-7nMv$aO6-uE_X)}BeeI#Q1j63b&@$auDF888sRg~43>h9VA%6ZU4pOV;5l30Q& zPQ%@zp_CbtUgG=V2aw(X5a*?lSNV6#&QRnJkYGMSmE#mctG+l>bpbjq}~5zceSGD9QisrlV3QF zd4Yw7^(XGlfAJ3*Du!F|lO!l|jzD|x`CzqgvN~-SQ;|&Cr`mM=$D)(M`0v^SX8?&g z{zjActywAFmjw$S^>)N2dox9DB!2*?-p!?!T$YfXydaxlK&#TUWN2Y;FdDI3o0FdBtZ94{C(m;{aB+Bv z>bjCI^@SbiEjeYuDVSlO$B+L`=UR|U6da(x8T^2ir$EV{QbP#DcryIoc-i0f1eybN zn?q+}515%+D6g&O|F13|1mfMRJJ`)RWxT~%VvfQ|aFv7mo3rScXQ@oO@5PzSeE+h2 zceo_nKuTJ6pjHQUNim{t4HTmI9G-X?21dQktd5{5ic(!gD9Qi|GhQyNZ(EKqwadq# zmej4K3s60~8=pBLLo zYqSoRog-I>5&c;X&DGgbkklbhbjCIfD zw^cs}#!^B`uTRUOTF=@TWh;hQxCDqxTGm7j6aD-;?o*k==d2onAwAD$XH2${%k10w@Y4igL;*@Av8K1v)n`ILGzjUdO9!ya^S zZZT)K8QjywpUYqORutUoGMvgUnH$HsRE;q-H2nDCn6k%3jYnd}bwX{QXtVj7cc}FV zvC_nZD(i?%RJ?iVWDNL*jKlr(`S<@Yp|NRL9MH|Eaq8(<3z)CcTiM?!!&9pO1E}W~p9Nk@q;W9^`(u5j zq{K#jyPF(Hj<+Jr!(Se-DWk~o%GFaGUJvhCWq8oVGg01do!T2w?LaGA%o7cm=EZKT z#{S2<)40jPR-Sikq38X`gQlXx$4~>wANXz8Q3o?ZBef}W`bJO9-Y@17nbWW@1%15i z?d{9PyzyH)aH*HK-TiuzHOtm(m6SYb)Fy`hN5c!(x9vUW{i5(FTIp9My`-y#i|#x3 z89o@OdgB!R8n_jTVY>sm^eu<0`86Aw9Q6%RKRv7dEB$hYtTA}>XyG*I%wj>IG9nJgmuLF*Hf;H>1-GY z=tAKaD*b<-O0N-|O2Q$@1Ub~=uMxKh&SJ2m0^o;n0fggQ^Igc=fW5UKa*KEERw|Z$ zFrVw$k-8sq76xm22rU zLfeGedfgzd=G9Pf4z7w8Di^ph+7!XNjPnG0AEmxizSNd~Y{DEjxZfzDQPFctjCf-YiR1Nv+h3k4mS` zyKgM~Rba_|nePK>?g3{>Y7n9=%z(`p^OmbvE)vsvCxH#P$NFO;Xg~Gk-_{vGNgVYg zlks$Fh37mG6qKiB)%Nr9|DIR{{py{bs4uGO>Vf1jZ~A%|{2_`;nph824j_7Bk<~sM z=6FAw>LI>~-QO5P)xEuWEPlD@x_g!D#%P7ykpEf@eF;ZfdULe1i{q;u_$xUK9?;g< z*a)6vubwk#{cl3;%Ry^z_4yP}xvG`ozIgFLREOfATX{V*>c?^s3n;u2@Y~sO(&idF z|1z~$Mf}_YW`ZP8yRjP^8&wH1d$iTEuGH(^gM&516wj`j%{E-&*mYX%-HCVPQXDmc zG>9#+Z?$$-?%eyQzOvo`+GNWbp&JGasVpOXhgh`IM5GOYY@U z6z~$>1s_*++S}xHt6aRU^=@ygjMk!~;nytS9YD&THz@kpRa7yzWEZJ2H;aKEG{b2d zy!c&hVzyfaAzo4Zq*k7mLB#*$3sg;MI!gR!@dBM zmB&AynGA*UA;gkWQ_c8M#o{9{wG;^?PsqMHYur+6mRvD&mr)YWB<0J zjg5Sa^JXK;vRV*z_rOG&8@)NrFI+0+be%uatwbEIFS}PUkMybaGqcq;&v}lHTp;;f zU-{k;6|~)8ns3&M3tICp6Dteo&wq8$6BYEWzyd!LdA*iY=njJGaSo2v?+wSJG${WB z{a1brrl|w7T>Lw+2L6%HEm3}NXqohC^hI+)zFy>w&q!2t*z`^K{k5PG*IWI1zv>!tG`kMc8?Q{J zZ!zVk-+^lb{hS?R4VZm1R!1(L_qf)?U_5rmW-|s{ng!o@OTiqxm-zBd=+M}9R!HCh zUl8Iy9U``JsJ(oP5{K2Qwc34RmHU80vP<#PXU9dJ5JxML^8HqRH0wR2TQUkgbAE-| zWFv&7KJyL%;eveVMzM5$Ic?njXv%f6n9y}nrj>gQsTsb}$|-YgCC|&1`P_iUyLLtB zay(9RBMPYt<>~D(&H|adWX5Zw9HYzcgOCD*W%o(9qNV``031RxR=-?GOZ`?Z>5-RX zy5zv$TwMCAeYdt*0>E@{?BJod?lT>Y^kpb(G^__DSJ86@dav($Hi~gly&WnUBgU)NT2g?7bSBwVJiS zwL`Ya=;nV%<^DNka&%prnaMp*E2}14q91h8i+c@mUy_o-kC!){whGv7$N^*&dEYwG zD78%-FlE$6#6Ab^S>{Kj7~E3oTEHOiLUS2|xFyJ@YCnY6rMOkxG2q~2?ii=)!lXKR zSemR#9&*GYshbI1MfvnAidU{|bb*Wu+?0i-^yS30#&`|vzZ^+|J-6S*P74X$Ww6&t zzs}@c#wGc}YxS$t=8%AO$|*!}5V>k15Tq7ri&+@i^2z_M1D6E+_Lx1!z=lp4bS>%l z#fuE+dC?pwIlkE{fmPf3BM;yF6lLQxrR_cDHUBsJ&U|NJYbW?z!}05*AfMv3@zMM_ zG!q360zS-0g&=xpq^aKHd$Sm$!)1VNp15Okm7L<)SxcuO6Cd{5MXCRKK4p=UviWKE zAs=A37F7`|$6k%3qG;wus*AJSgYfbcdXiD2j-c=CsAs3jUV~8W!t@;?@FwxH)|IVa zldEYYRK@DA5u2A&M}$;qOdZc@-QC?_KfIn)Mk4oa<9s$j;xggcsj)8)y#ERCO^ad) zy2w+T!7|RT`dGGKtpOQeN!rt6Y-q(7$J4N|g`Z~TuCgyYwhGWnknl11ZoL4W(l3|k zPplEN~8>w4aOE)FFSt z$lFnc#!if$ZIUZ&BB36F8`0&sq5TYg1YXc-m7mD;&gu82Nz)gKmzLrU)3zBWSz+=i ziu7G`m{=`hkmuCJEhdqh*bA_Ql#@nkKQ=ra0-v`{@CQDXZ~CxOU{NA_jef9C++Uou z(PG_L@$9mybhy59oNjVpw`>eNViZ%_>Ci1`Y=t>e0%k9!k?5~j-J?}(a- z7d^(eTpJ2+9(u)28ai(t>}~PNJopZdzU$APTXgCK{eR~PxB`dj&i>uTn|bU``Gvpy zCiW-rt4bp-+ZnvnE)<<0A8GR_+I;PP zkRR|@4%7?aTt$qymZcsX(5ZO(E|$KB?U^|=*O%J@t>!|ggN>Oe4@2!11nr08S#xPQqjFA@CKoNW=D)6;F$nZ`tXlo0zSNTWYKQ34@AvBaUnZ}K$@ z$j3YYZvAW2qaBoRhYJ`23jm=f9ZTEJQ676~vWb!M4z>lCBNilwAoQ~F@ATI4;SR0p z{_nrlC|~Oq=blKesc*h*VZiB(4UkPzNKn?*zo>8-^Btsdfs|fs(C$>2d+pv_4mQXS zJoNRYZ<()v5u)}IP!=_QrPWjr<2T+L1#M1+3Dpwft>Q0P{aSrBB0#I>ByKO6WA{Sj zK14+&yFfQUNhiU>`ny4BRQfp3ve4D;94qp!25h9(cN* z!vD-@prZhE3LKhB)B(}k9O>CnoRd&P^up~Q%xjQI=GJ5!axdJNgT7oA%dMj3qN$6ZNh zvhvZhL60gnb;P?W+bv!z!F1uGaEnW~6BV5Y;>>OKmXUPI;c3emh`i`lLmHR|L4n8o z;fn?i<(plN^5B-6&g$*r_QR@H9={&qv%j}BkJ`9{1b9F`5T~GB@(%u+uyjJ&pziP| zXJhVw)ZleVr>guOMh#R_I8ZJCTR;u23GS^p!ms=9_#GaaX~$X`X&L;%O}y;=sF-@? z{#;<)VTg3-vXS#)zk|=QfTx+DkL5(E zQWm6T@oGHOkfC zOCmzZFP^%@M4!#Wgq&DW&zGTIw)1)3gBZ1wXI`_^Bp0+XSi4i2dsA(rYJy4(gC9Iv z>b&_$zZ>%hc&S^2-v@sYg+9Y&kTdw1(;qhcuZ9)rh2)Ey5pbOdSb34 znVf*y3v~~49xEIBn(S6N70RbUS@@gO$$Enjbix}X?c@GyTPq+d2US%YpRLygirh0A zaB}lq_?69ej7gF*=yz{{q#d9OwWB+p^PPwO3fPl(Hm)sXVM0LO;ZHeWp$P%2UBF43 z75FUW=Fqz6d+@f8hws`zd2D;w8D~Icv?#W31-)X&Voh6JXz59i^Y=)FH) z@N2OR>sK*k3vgX}McTJFUKi_aL%-LD^0A44-7V|l2t973w$V6R;YpaO-p-ce(Nf#9 z_tRQ=5AXtz8Mzc|^6DE|I0gH+-my7m4}xuQ`t)$zT3DtL9-GABSm((EOW1jQzVwCm}g5J!}=P5o1g8MpD$&^c}sG= zM+R2AdkkysiIeyW)H|7h+B;DAd*-LN-B`(=CJ}$h-vL;31orb4yABC`cPF3zydRFV zZAX#D-*Ur}8YAI^_DozIY5vbyKEC9brIXVfwoX)%d{B(^hoBgc@@*uKZ<`-xjvn_P zgX^A%{-F|E;Cg)v^)d z6FL7OEH?00_YNm*r+xtkszg`t3jsn?-Z4BenF{lIHp1-ODuUhs*HNN-Rn zA7W$wtNzMwjAg4!EL%A=g)baNr@Cn9->H6TGwS0Shf54&HwXWG3XjSaFraQQ2>{ z^lRh4>PlD{aVaqXAlo8H6;Sjw`u-s>1`rtvNXlz>rz4KT4}yVxah-*4EiOW+6WSIQ zR8O#@`+poYJn2+20L{o9WppgvS8`l~!tuY90)Ef{!XGEfhtzo;H5-pWnIe`f`hVb6OFlmmecvp>9T^O*nHoIZo?=S&rJJ_bq> z)3mzp)+wT-q6qj21-qZ6mq`T3nz*6JMkX|c#ISst7n%(TE%pFNRc%t4zN>`hb+dE6hQP!l$u9sSMxo_`LbrpusprZ%rGe+;q@YCVaZq@6L?gW`7c&8{Z@~E?CRuh zyW&B0#UOI%W#C(%y8>uiSU9MN8mxXct8zohf7}^I8O%M=(5W*B?)< zrQL(>=*OAq>3@Emf8vp>rb}ct*i_A2Af3V=!PJLP$*5(Jgx2nlZGU`vn7TC!q15*N z93pWb+km^d_NDYpIE`q66S9yQrI8dc2Wm#Jtxe$IT5X$mz%z_>t{B`GqD_F8hu?1U zR35U&1nvwuVhpz0>t|Mt)D~xFi?{Cx1-{JP=Bp+QSdm9X=iMGh9;1)k>oq_nT(Wuv z%){n+{Sp7w(u86z$r+)R=sji;py4DVxS4=W=hDqxxBzA&pA6*Z#>xAi?UEpyx?#O$ z6V>eT#G<-3TT9=Edzza`3L-(z_WNGMjAR{`#JdvWzRhvOE zSrA~V#rcKKo%5T&KH&@M5KytbesD-Y8gWQ=bq9cA-wuGH|Ba0A`~qIt2K77Ko#jUX zveCqddc|_bes7cTh%Wl9{SX&Ifr2|UEbI^ru3jM|e|ATWhDFl7`r0mT}vL(AnvO1!y?3JCkGP2J)H-2xwzdg?9-uHRG zU+>rR`Fg#cPe3)Z!$CZl_O4(51nMPCrR%xG2Y@33drZxZ0e{T zTqY!N4zrZJbDGI9XC+a`dEu}rdX>_9)4BXTQNyyW*5 z_0MZ>|AkY3O$-ggmJhiyq^)FHv+6?Sh};^FK25p+Bs=f@$>s)g03KX-0E&7ftbEiHcVgzad?blPaqi9F3;r?GF5JGII_=psdjeO;oAVr=Djxe4Pg~aB_#ox1Pd@>G zCj@NQgd3|}3k!Zq`qKbt0TyLbfb@(0GEeK^$&cX*Skf-xL|w872nd@3lr8!pq-$T6 z!i^h5;9h&RVexqGfCBE9BK^?k?0~8qNHtLpTrCwa>Noxeh^ymGY6jr*#ksAhTWIo+GfIEaBAm{5}tNY(^95&46ciwB;FtkS?W~xci&Mu z%$jKaQ|UcRZ5r+{es}d8^(3zJ0xa_WURzgV@#T(rQs2Cxi3zCLkipmM&u*Ynj&*ts z;ajaR5b$zYK?QFKo&+8pR6pzA1Sl7D7g+JaAmn+_DQ7==1O zo*4u#k+4;Z+{WGhrUD0lK=6T|y$A5LgkB`Y+^?07TiX5*M?J|5r?DAmMdXrR- zK2|z;p@4&bIuz%;aiQr^cS@7t9Vl_;uX;V;9pCs*t#pf7;7W&aQO0Cp{VQs)_j}`Z z$j21x&d$yORgZ&1WDG5N1H@CmzJC84v%tqZ5j7_F!fgfIkV39eB|Kwc>Z~@EG+35Q5C+|AFBb zJ^U_MDUyOmegPbQ^RH)2Uz$GkyMw#!%W7Gf=5W`bR< zuvW=U7D0z~i(+9{1FVf9L~|0Uxik0ijd?NUZ!4g~0I?Q~H?VdL9J3yPBbhHlE>ANFIZ}48 z0)ZX$CsTkR_5PLrlD`q)`IGmNH9QZ7wVrw%7t4dol| zFt)u9;`Uu|;6Q>c?=fCcY=EVJjHwaiRjIj_U|qmL&9NIujw7lQ=IGbBPDO$ivTR!R zvM8h*7bZt>eBk2d03$w~zR=(vJ_pkltHW!qJ zm%dUPwAWr0%XMd1F3*WhOx^3}>Vx?`O@vBMdsMezuQrr_V*=W*&z9$Vp8T8JXSxG4 z(4}9&MC!6(t$Z(VPP?>vuV9v)=r~AY(YIwB6X-hSW{Sfl?4xsgBfrPAH$n=SSnzg&HLuUAuMH^+`t3H=v)9YoEkFVI-Ej9Ni z@7uI5+n|lqL1(K1r%Xm}^LT)~+7Tj*=Yr&x_*;%80UMmH=%(m}V`MzSO)UI!7{?-)aK|^I!eK|I2{$`Ex0}{>Y`a_3rJAtqd|)9{ zIV-pwvJ;Nt-g`p4M5$wQG?Dr61Qp>&Z&%yaC`?Gvu{*F1j0j(3W_dKuy+&Es$FscP zJZ2Tm(er%&2d#xX{Q9W~4S>zEZ*HInKN@k` zJh6mDKXklt^5T|#(qZc1&hI;O3!fWD=zk+kI4oqSwj5nFfjGIE_w zeWNstQ2OBeF07?PFTF4h2%Ca`XyhYHkLbHx@^+jodN3jWSGJCBkmL#^fjR++C@4wq zCdKI^L;G=hM{okyRX9fs1sO&sM?_+J3D+g>L)ATV^kagW%IZA!lOBJWQb5v-O=J!s zWye#5Dk3JZ1e2IA9GljofnX$bAyfH3pr5*Wp*KLQ>)q+y(WNxvsol0PXO4s&L7UfS zr_6EQ;}7EBhvfr(=2+TRsY*QLmg_mx`>aRj2Rn;x%$_3m1m~0$F2PMl3wdq0hy78q zp(Yn+vDFAiGm*MiDE(WdaOJ985^|68((o>d4M-P1hA<#JYP=ZF;UQAA1{>d8iFyzm zeVRdH*55ClDzf51&=)$_RBiJYfq(@u8S+st6?WQ)L#Ko>yr{E#0TqbnIl7R45n)oc zGx9^~qaJG;+eaG718YTI5m&An4Ik$Q^!Ovh_;1x6tzNi#U>w{pQX@$H!|`+2!6#Y> zFjA26k)(fot+tD2OWJm%%7%GFg}4tXsrnh>2m~HK#?$77HmE>8SZG#hVY%~HJf04X zl0Cw@viy8&9Q$tka~fsLZroTJ-yvxsgX+71DG#ap+p{!!lgcY;wok+xjJ&rxvCYiR zsfg9k6)_~3*5J{FWakY)pQEyyt^ zo(A%2YF*2JY30mblH{b6mum*F{jBhPr%IeEF~@d6%&$^y3~8aWrZ;h!PDxj7+4WIs z7K<(^9ZZElY;|aU#eiuf*-Ti#`E0wqY}zgdS=^oCmlI;OUZ?Z^qlBdi1#H}6Y!EGHKFuHk2{;u!1Jq}+AXOo#g#T=s4^ln z&j<_;R09M|^;duxLMUbrQG3Q+R&lb| ziNE!69L|&vC~H5aW5C23q96hzcnJ{1A$EwQw zbp30kY~TabX~%-dJoPq;7myM>c_OHq2&cmcYrRn3R9;MmK~Fl`N;*8%cFJ*1X|4Ga z^GBKzx(N%h4Y5xvdWp3JI<$7rZ_44B?h(G5TiyN6($8Pe%h%9!I=*gH@^Y{6M7m_r3Juo8-36mNp=vC=_U95xz4V9D!c? zt1uANv7PD-j_1cR?6fx{_f^}E;ytuAVMqW1op!2Q+Mb}8ZdWD5Pc=^NW?vN_M&4^~ z>*{*nXrkg_6?SywL?c*aUAu3-aUOfNyV$HJbv2h`c&aek;zy?JrKJDdqViuQ*w!pn zwd_wd5jI9fM$XB6cOHq}#0bm>51e#4{Qh=EZfx#GmzBFr$?=GtXG7$-*jsCdS=+Z& z+435!9`96_`a^&|%f_jh9$7b8<(V0t5Oc@k%STS`(Qh2T@AvdZEm4w2S?C&$dmn6d zn~}bpabg;qW{(CFwf5nrQj3Mm z{92NnY(Nl@;DqGf?S?*|-N&5U8}`=Z_cBgzE7AXLVe1 zV7tJ8aeah`6xcf??dHGU<$foM42?c0$AW1y< zaQJc={zx7gT;Xf-KINh6pSv^X#FSC645yk*C{XgJZk_}G5!7V znV)T2Hm4Uh{H#pp?RqNRC4BcQLIk~o!QRbm`kT`fO^Llk)H%lq!JX}aGH;&A&ZYZ& z#>3-}VjA`#dCDS|HM)Z;f#osxdjF@YwX`fWP`(9wo?=4IS<*^Qktud3rgBRRsB}&D z&Ymt!k2G&8DZ?zq(&t&d0X0Dox|I{iD){3q&G@60JK6IW zdKh`ljI_hzmlslsr}maj(9`(pjXm3R=QY*r8~AGGidyM=(x9; z4Iwuht;=^^g)cdXt(2Y|7iu)G0?B}J-d}G;pCOwx1;TVYK`N(()}N6{UB&^&5{@By8~6a> zzE<>jjZXKacQS-lG>6{wEp$i~ zA;}a{x$%#B2=wYcbE>ji8cro1@g}a0v!o@Zc&yda){VVAOeodM37SMuG z&##1_SLlanwG5bsa9TmXWy@s&_^YD0y**(9iY95w?A6)RRy2Z0pVc1x?tUcu42NNO zaQLnBGptuLiMbWw)Tl&Yw!sN0%!6wa5-%Oju~=+aV!j1Jl4CaH7asOqHKtkOzkC62 zOLEW3&c66r@;&bxR~Awy*2>boF88j)ptZWN<&|*LV)mZ87n&o~NXX-Ob;5xqfHQYz z1i%{w8s}{>8q|tX$eIc!3;x8>L?OcypBedZ-z?+LL{ZN+{-fpy`X33+16jRyo_MTG zLK?sN4A&qe{s!B(WQFLe+wLj--zW@Mi6>?NMPNRbgBbVgCk7}X$e z@*Xn0pJp=+_)5HZy$ADmDK)B)+Dc?iD_%-tp{W-E-?6I&W z^&N&b@3v|e^xwZtHyZZqug;ko(rC_@K+>=}G^Xqf2<(MF5NR>UlR=rXbmCz;#7T>q zu}C>R1TR}KsK0H$LVs-%erFZozsX=4S<@$|!j@$7M;Mn;F=|4IQt-Q_!2={at-U5K z`t02bjskf4GI|b9<($C26YItsqMZKV|#hL=R8b{-;@+!+osnlwq845pnb zD5h(n(_WPZhr#kBIOs!fNaY~6bAEYpQzGHmLhDvSKTMpBuUkF*W+XzcaIL2@wKSzV zlX}05#K=;eq#@XP0d@2R6^HaiWx|KuI>7D777X5z@fX+kN-BtuXPLflD;STLs4*RM zJHr^wb4@Gap`|7RjTr%;sC}4~{T91EVB*=)I(G z3uu+k*mmL{Sm*>0!-zdhjiUT_RupkpSmEq1BPr9lD~r=K;t_rgdD>?TA!#4Jq|+jR zE6(?(ZVp^hzdPkrezP3@N{m;kpXa4x*LwfO3)j!5+D+F5QkMf!=GYbk zYgYW?^+DS`UU1+KG_Az`SUk}68s5}z9Ep!tD4g4I*-1_x5U*s)2_Cj6ZOn^HA% z_+;hy4qcYqb(7;ybuc&iv%%C_X3KOy#sh6aO+wq91P=HT+o*3-zY@60nip!i{jCIfvk@w;0%m zHOfzMYMjpRftm+(>uikXvR0fbVjhf?NEydT$W8HS-Ve^7wYJeuSIrCIJt`BqN}C4W zt*ev`3d??}oUuz4`rc2e>cvyn=DGK~$5jVpVh z1j2(=)Tsnj#oTndOzPBcI%In;a5u&DoD|vu;J?n8L{KunEzkEx!GSiHAJFZ0U+9AY zzp~{?oO}C>pc;c99sOveL|lJRc7e*XF8;C!LmfBcv{9aroA+lQ5HSyaKoGCMy#RS` zHiHy}ZmIt^0r!2ME+i?MS1_^8hDObSMvyLI1=Nj%)*aQLNy!jhU5Ma#222ZZQ{K)5 z0qtd(7#->@Es`EM!~zTuUmzL05}i|X-qGoKPpFlC(v++mD6)A#bce7MNv4DJKrQ!b z%QP8esMqUTSVXvS~K)U%9pe=zfnD7#$uG@rR*kq5BDoH0GX+6OaG0??k zNRh}b$0^2|e!AJkeT8aOr8E4I1GZ%8T)C~1}TFU%^=iaqCj5?LN?CVISbGKi% z?%bBz|167ck(2VW)%x_z?ab}hOtMeK_+ue)FKD5jl}^^r`bpDyCVu@nhC)`Aqf?|n zUU~Pm9K+&LYUco)i%C=0*SmF+O!ddFd%iJSerO=nisR%(_V@|0{O{w3>%q*x$=@*5 zXPkZp{P$aqOHr24+W+^@_SD?}_n+zlL%{!ClB44VAxGMD8Bm4Le23R7g4g z)u81j<*O7^eU$`JKwh z-yl)nPgCnyoukYMW6U7+l7GbLkJ~z33jQP@_I_zCe7}4C$mg)O&xLR>#Q6 zZ)%Tr9ljx9HDjR;#yN%RWFN#|2a8ej%l0AdqT(LwHuQy&;X0ecDiY`8lXTY}?^7Lm z?#DG23?^B;woj@LWj?Z#`*s4H z(mOfh>~jhg$X>b#2vI&VhT}a}32QH>_0Z;?7FEkD`S11%Pj-3?SLfavm|Qcd85Aez zR)^ON08g9*D-vlZQ7xcYTQv;4C=0r67m+pf~sGk5&%7X-96E0!S+M_*dd z%R<0&NPKmyF(Y{?F_f!*FbSVGk93<65eF9%wmy9SnB1LPgn#O(g1xq(^|1S^sh$tD zXA81mmuMPF03w@+H1fLFyKl%lZ65?HO|f@{ zhulW9jA?eyZc-ps(>WNmH&lwG8b_W}9%iv|7oH#dt7##s~m1U=X2k+}`&HfOb_X!O!W(&gJ?sts?Ps>OnrV&s5iU8V9LEu_2T3(>js z%k*tYA#A%FxmCt3OSz9?*}dn*+~@CROPQ3Ske?reYhVbe@)Vr1@COn3_VFRBTRd5I zNwT$7*(|HRzkyZF*Xi`snn4t2PU`ffgdU0r+*;PG zdq>e^szy}pmOgAJ4tz<=msLsG zQ!rFHC;Km1@KutRK*Dpap!hHK-I$46b4}y4K(An0zH)?ngX{ zI!fVn+ug5`HQ>*p2Xr$^^L08ZNV_VE(9G6IuB#8w2$Y=!WW9Jt`^2R*D>PPnL( z^SY=NLg+4+f?mI)wkke_lnqBqU1QpA4n0}J1J-426N@iv`sSROQjh;h&^#oG_3-6O z;;!sK;y^*Zb$e)!JWwgltX^6Pba5h-^Q%SAg`eSHTbs#735QYwpKvThGaS1U86z?q zUK#9=iep6m8(O|nG0cZ01a7L6(OhM)5f;4K7B*bbv6N3&r04;o)&1{ z9lzj{a!|cJ5BSvuuARNu1OQhD4Q)Oil8)?%)L6IiFyqO#rnkRk`Xo`iLJJmKnTP&YE8R(XJckJdWms-qoTrsNA;286K?(q z&08UfE1cnH3X&$SL(6|COJ?ogN~f1f`XG$i^p9VCPrtgV}^N0rVbz|=7{&pM*fr{7AXA#2Mm zal{@cpdo4`SK~cO>so`yUTw)m(rRK7y>dYt>0KnG&@{F6jz*MD6&~f`P@MxWhKWd} z9_(MLm&D4R+%YP9;k|j^Dr0KqX^Gs(*R5ZQB0~wi&$A^qH$wZ69Xmytgsc?{1^xM^ zYV()APwX{=oYaEQUj0!w@)0?{PnHn^&wxCGa}A$`ZI=0Z^Nip71aR0?mjsJA^U`sX ztP}QRZJBh&s6L0@X)0#Fv+^zWT#PW)8lF1F~^jiUH|yb)SQ#X5A<_$ z(_`Yf%wdkN>SndNWP>ZZE1I@T5S^;Nh0GcL(09vnC{-Me@E>?AOyv&$lt* z&5i==CLa0n>C7vm!my@40k}UGT2eHOC?-<+$@#Q0cWDu^S5X{Lp`a<8vWc`VZHwq8 zRu{aR?8)6^E>Ka-q};TO-n&at3A-1Q@k`p>8~^wY291tdUIxrMw<86LZ8g*}csYa9N)q8p z$d4DF>m9)M1DgOv6-vk)!&o*T_;}`NTqRs1v^ApwU?~PKDebEoq zwRf2@S4hc>d+OR8OEq)z>Qxh3l~Je$4$HQ7(S6R)uO$1j`t5ads;#V$?=o2kKpzjp zXuOPfm)a;4%j+rk6$ zor72iiUcv^0G{B%#VrS}0pFJ^b7wQOp!!_gIg@AaoAU3p^TT7z(!E;W)PH6G?@)X~ zH&_XCeFf$2S3{$39#bxE!gE(uhx^40cMvQ7V>ZeL%eu*R3T@Zbf2)!h$Mh8vG-qaC zYcEMP#BJ(Q-1~(s5&t~$Aec1>5$kYXvCr_dAlhJ zhUU1_;VpFzp$DC8h9&1Wv0i6{J;WhKZa~FKYRqm=h@)lJ420iF@X;fm7RkuItgc9f zZwWbKQx=8b0R;bd0QI_i`U37Vnbg^ob(LDo9Y4Q!JNrRQV6xP4!#BZz7ZMZKUD+qM zf+gqb?_i_0992oR)?tX0TAW{7rr)gj{5NBzlmqG00a)QOaY)V@OO-;N(XeCZQ6Jr* zJ9j&cBIfI=`V}7lwZf9+<|gt`>8f}{q^f5}%{PRe`USFlyX2o-+GDOVzd-cbGfb_; zLE>}8aqo8qCw95nNU{;=oGSzv%QeEGG12dSA@4G)82?eX_xg)#zV##h?F_`kvIP<)DGiN1Uy~!X2HQ*ROiadK zc4?;>59dvZ<)tw2E!%u4vcy*YOB_(@sBY_hYqU<&B_`0>toT$Oa{~@UU*uD(n7rOG z7AfZ&Y@bxlxpz~qywmKaGB2tUe)W7y|DBQQko&0*M&vO~)DMeMoB=?5Sq_Gp$e0K& zkos5y63I`$p-UrEA+z2#ihVB_cM*Ggc&Ec+R&;_5690)zNJFES5gW_7$y?ejvPUZp zmPIf2cLjXuzUef3-TAVfBeXNV2gKgJWl~;htn$U1DXbf%ASV^8p7uJ5{ZHmCQD&V4v=nN7z=6Qii5 zG8x-;cm3@XatG)w8z)2-c$252#zk3+$kHouHVA!iinxU!sP6XRN|q+8za<)xDuqdL zglJ#&J}?C@ zNIU{hY?ATh#1vu(8w6);+@wqdzH*T6JmvCNaYERydG-;Fr6KM=r~o&&%uuSI-PtbU41CYEwsq@yiCK zCn#TJ_~OWv@%TdN*hazy|8Sw1go}I+^d{cHS~^-@ILpa(RWsG_qAs(fnOmi(-!Xv$ z_KQ;)KDT1JVW>j9t5#Hv5c2lOD`hPVnnsFgyYeI+CL!NeGmla(0&giC0#@<%U>^DV zo`^VZGw%p%J0JaM2f>PqCa^zs!Xy$hQMa}2?N1hb@xr6AdYF6D2kCSXYRS;E?gfOW+gogB30&?WN z#i7vor~OEpUkSCy^H1w3=DX`zO{v-&7f4^&WDVc%bhVnZHtzp~{04|rVf>{RMFQ&0 zC*MUg>a~3gyb}}?%jta?o*Fnh5d#Bawr$akP>d6pnNUnl6a=$aDnR$x{~C6X`4+Om}Rqt6|_NW6`m2U$FHZf8WAe5wwqX_@rB(t1ej7X&;pJmB^o^@(S5A83QQM$@O+ zr;nKMmx4C?A;*z5%!k6in$X;Ic>-oM(kIwpQTI1lRypI>Xl1pwzhB0YX?Zg`oRCYg zc(kt_gx-%tKPT!bQ#7`s)9~3{lk!w_;MyL`imo+i+f-LA8)BJ^y(dl);~J1A59bDY zH~#|Rdt>G*6S~n%049CG++@n;j6YAXtR_ENuoQyng^K9EtV;SapXjb*<5s%{najQI zYnVYbR*c%|*;#{aZUI51uuHhf#O=g8Lg+~-f91qxkLu`Q;3C~4y}#@?>7U?7+EZuh%H#?svz=qo@i5?ydp_|b^~ibaT`az+Gg8@=Q3p7 zJpU$w;^0Y&(Bt!Ar}7oR(~g`$3td17+^IGV)L&ugQTaudLS3ML^pNx>f`9Ads}Fl4 zW$cFc4@1)FA9M)wNWXbBP4r(goc|a?nc{E!_7q2RR8>)3P@gijxR)qO7B+O56;j- z=HnvvAD2+MfBKiU$7`C&fnAc3e@cCgiLQd%+SPyo(Yr{MS*!@XnwixCJy_inYCZx* z^^8y>dHy~c{IvHmu<@@9A1L*rA+N?EtNN#utkF|k(429?%>{AN>#TC^NJzh#Ar;Y_ z6XS2%{7*u|W?gkSEV!Gz6CZSWX-e`! z<0xBLIPvlB#^-~NjH!LYWU(BIg=Z;3#+Au&FdMzdsX83ZKkaAEedOhpcu|_x0I_9V zqPCmk?A6)%?wohR!N2s~pT#vqJ@eUbc($;qP&v#XYq!G!#C)#`2g_P`3o++K+0&g% zF#BtgO2*K;z2+o!IQU^uYtyZnqthX18_@qg{m1-3IX-g~2G#a2j{bvMs0@G@`1^@$ z=S=`(w5b}q@Kyi_)!Ro5#DJGI$GQI)Uu2Zog{Kb5sTq=8Lp`d`PzgfDtx=>m6Ma`Q zXC4wG2@hEnpF5M#Pk<}ULEMreCCY!j*1{v4L#+aZP8it`lyo|j;6M8WL;n|&+sO)m zXpQXHY==4URvtN$4mN$717$`81oj_%v(3VJR!)lkBP*Z5`~~rCQnVlWu1knoC+ccy z#WJ3dg{*o*PPm$ypS#Y$x37ZJ2+KF2qF&_C>Ah~VjLvY2e)P}KhU?BJX36`%WL7Nv z^&{Gv+e3(7#aMi^zgV6oV*`;G^j+7bD9G!S{V>og)T)NKeWeiOyYH+xzT1aRoYx~p zkJp*hN2IL9xLCoDgX{#z18-}Lqf32#n)%Mz47AFU-BU8iQ4t%b?E8`99zU*)^!|m$ z8#fh3s(tJui!qRL)5(qP7L&W0TD7XAb_CcJtL6KqZD0^{#7e67n~K)1HI-day+_#p zh^JTIs&ouwst4;*c+EBO^=1dGgIR>J>CeHvrhcYF${WAn`B2Lhx9PAa+W8XD0igfBIe`a+66fAzo;mros`9uaT#oE+ zvEWA%tGT_|-iJJ&IQ$BQufds{;d(UF=Tatj|NXlBq4J>FYu}% zM&rky-CNF6tVl$^1lM!Y8~cN}lSPq;qD`(SRm~(HQMrNwpSJcciQC4uub(z`a(&Q< zlI_OY2ncr6LqylHaw6B*a883RzvVazmCY4b2zeSL$0o8!o8D*EW^-kVBF_;A#vyV^ zmz{1mI!BZ|PctHokDI?Sv*L|eS!Lbh-lcALncg%rN9(VDnG(H0_reF$(0O#fmx*#2 zNlrJUVhwcEnelnI2^cUo@Pe7|e~rbe>k$MBjdtn^I+1V_@XkhiyOS2w+$?g*Z^Vx2 zzB{FLvf>}ut?W?;p(wKO$mN~}&C`Bd^&jj%Ox7d-3Ka7WYoi4nDCqTlEL>#HHc9@y%^f zxRFbi-mDZlVZd${Yeaf8g#N8XM@-7M$RohOaMc;U`S}J)mlFKJOPS^KFU%M-usL+q zIDYUbdTC2{`}%xbpvL2w%rb_k;7%{TNJlf3~rbe2Q@o)q(tD)%Uw^N;s2@} zULfWkZ}Mb74H1+`==^bW>0mbP&j?g3=PTJNmDu?1O6A3@)y#C)iy-vpY#H3 z^tLXP-D%+Z54{1IgZqBL(!8j5(pnbxdtj5&?B0(z#L?Pi@J9Q4>Vdh zG2h@wF0**bL3>=L4P-tBv22M#!bZ=NmTcL|3TY1inY#6Lk8N}mVdJCK`9x#hjlB|21PB848OT=PNhybgZ$P!T7x~p+$$Qer2ym|$jIor)2m*`~lT;0< z2J`cHL*CXlwZ@}%(P&%lYNbJ!-G1b%cx}TbUs**UNE3P#*SC;7l|dH=$TRM#bn?8~ z&nqms`g^k~tW-Xi&4m2d^|v-WRL+sypqN5h8OE=ydtKG;J%-!q*LlMWzoB(OUO+%r zucjhoS;J3db;^Fe8dCyMk(R^Q084XlKgEqxG%*JQ?>zM_kHHmjBJ>Q?4ajsAgM9wP z>GeNwT1eJ`gSKs+9KhDO=fJ-IV|X-=%_5&>$?DgFgSZd03e zXxCIihnk^NrR((h*XE*gZqSFyFg{HkVflPV?7qP~$Hq_ESNG4nVc79Wn`Il37=2+Z28oroJR?B`UiG}gX)X0c^4J9V@Mc8Az{V*T$ zPMT z2N(L#nMU=0AB{sQb1MBvb&{^cy7fO5FGYEc^T7NBi5%P3-rI^kC8YyaucEJ}DsCNR zp_e0Vpr1blA2L&&i9_d%QP(P1NBMMXpw)h1;-`@y2p#Grh~u&p?-`$;Vuv|Hb>`!> zw|Km&W906u`KWq#0&FQf{aNGe!sv%VI)*M*zn39tgsZ7F%ysW!sH$u8oIuEAkz?SO zjqU-;Q+YrLqIkcFZF2m>_VhvQr+<*0XUJ?Kkqgx4Zs0y($NTV30~w|xG`XU0%COd{vYig$G~wG_oWMTUxN48F zL|y%%Hw_;B9Nb3#o%gU*_K`g9oE-pPKR?4)3Ja(F6eh0g_!>7t%NT=}I#-S2G_d){ z;^LmivGR@lk}T_0vL^YXN&A-9b5&`V)3m9JvZM>yAU}dQoyI_idN_aj&>kTTHE<`z zGIuqR$6sQWP}$`D?4v{{4CNBwe&r@0a=!MO!Vw^n3~0bH(M5S~3^BLz!5^Mc`L9YD z!lwR0mnNY}trK+_MVJ79v;$Z^Er16!&S2zFkFEjhe8KOX zQ|=ueYTq&|h(I?=F{Pgjd~n6*PyQ7TO{uA=cKl@}?6S5r@RSa7cHh%xfbY?kt5>wl zA*kyvujq3Dv)1kxvC?TfMtt+T`crMli_h zsFb1}zMd=mUi2{^Xxxk3T(FUHGMMBxaOMdolGPT0_I~=jdM<0wV|0y&zQq>DZj*W>*oU*5k40Xu{26mJi}kBzF;g*9JFl>FpH*~C*|OZAgt zRI|{!Gu?G=IKPi$fL8l!p}_&$;$%I~=L!Dol=q-7F(0Sy22XY;3p&DoC6+*zMxJqC z;z~W;9k1^+R3|&{f)xBK?F+h~Nb7L&e1SG)6KBg16Odhmr^{$Xl2`chIa49&SW{BH zJ3xEJlGGqFxD^+`}c#Li2)c1n%5 z0mP=dAC4M^8(jz-)>bWa*vbD+5H5_Gz^mY{t=~a`&Bt4FH93Zqy2K^ITA0CwF36Ao zCWi;gwLK2wHz~h)Qy}^;-7XyT)54v7?90yLrWMDFTQiYHsc%eu?U?jD@ovJfq8qh# z^#~bk#wq_5G_^tBobz_SFYvqvCtQ|9zEfvLN8NmYUVNGw8B_?Pvtx>+R3*xx4x*|K zwEgO(@iWM^SK=fA7dV9FDA4#|+=_pk5*WmxGIy!Z`_arm1?m@ltD~cviB@nP*#`de z#iG_8hy^XK$CK=^CxC)8a;ANI6mWOGP27>zCEi^@(_tdQDt-Gnsi%hGonl%pU=%qK()!!9BG8JJ{w5u;;)??(hp9ucqd8 z+%bhTJUSb+^hAk)*v^X*gfwt;wV1xlvq7#JfwjDYQrQ-$w-5vnTzBSgOzCygYb};m zBzgUl*==Y|m=Xl*5L(BHUfgfTdIVolBU0mlPXZca_zQt4WdBXSqbS{n>txa~ux847 z2BkJ%Nzpo&YlidPMR-z$zTg8=?gM~QWRUYRJn?NvvjU(h_Qu!d!S4~xRR;gIAU*gF zSo1SOar;4HUXt)vbf32dq`xT2s#$2Rh6DZqe-Z!aL)T#szQM8v1a*>OEY>IiX&5BD zdT90r5EBQyhkzRxG<^kgrBD;6E%uq}>5kWGeIb`}YK`Hq*$|;sRiLJCAkW ze*^9ZuSZ8KZ*tL29aNu}Ux7nhSEnTo>E$WceiByrP{VKT4oedN5mKw@aH1xqx(})Q zn78#RA$|ztoN{_p*OKWETc}(=&Y!k|Y*|HA03I_vEpp>)d|Ex19yxftJ#mP5gnU)} zb3`tWnhW%i{vnALR_;+%sKN$306&zZqdjW&)@6V_F%L@F81WRC!a;UyN`*qwk zJ~+$S3Y>}pW=LE4383QyXQBA!63wTZW+4q5`S8G75Y$$ON6`DY*ujA(r62Wt?F~Cm z>9=?0txouiZ1svYh!eTNQPGRD`()53tIp8vPS(uQ0sRIP`IS@4x?XgcFU7FkAM)W` z=yseeKXO=;)E>j!@^?w!#3!TgHj@EkHX69ClEPCtZ+xBn+l%RG*OKOq)CZHisX1sx zcT(F^;UI@h=;;h{dcqMfXUi1H0lnCS`A{BRL+nR>e(@~aobxc$N~g{%|DAAYKEqrx z#Fx{jI#SMuR^*x4`Z_~b{@2{cK_FCl3V_v}a3-Ldbw48W9ibGFo$_ev{f7Bu_5j;c z_Fmr#<)V8c?eS9X_Y#mhiQnRn{7&bNv4gKb0lpG~ad;mO>ycHd2XSC(X#bSOd5R}Y zWY5COy=?s{FimS+&)n3skq={Wma92fkOAu8!h+a5`l8PUpus#pU7lYw+FO-O=tC#| zvERwEh?%kGSb)>pfx`QF7G>M66ml?^9*mH1VO!y=J#iOaPvZJp-rcZT$+z-D>OGX9 zSu=S@Q_-Kq(kqa5bdJ3R{mie6gE@Cp#XoMb;e)G{Z`YU-YF)V>IHLHn3 zXQ%`4RIC*}$X>{=&1zBrp66Z^DTZT?JCa-p8=w@^^PDmKt~iVVjp(Mv9oR2z2-eZc z!O+i%b~9Y3f1h$G7P>B1i^xCRGO~Qesjm{2+ttd>ETmx)GHG@Ba% zp;q1uEcF?(ZaOi2NDOF!guxg>srgX7A<1r-1lXV_VdejfGs$ckV*0k2&!C<( zrpqXVgoVI1`JfJiROdsSjV);wJuMAap_Ze7?iR9ElVl9;f`5=>>6?Wz5|#f>wWH2t z*+sl$8!%_1koSO|kgjywcYfb9$Bs15SWHme^ZD@;;)ee!^UHVaZlJ&S@YbE3o;>on zNHOZ7I7@*CdUY|ah5K;*w!%BE!vc1{7HEq)@NdO$0O%U(6@Ld-cZyY;cwNr$qGT~* z2h$BRj*cx*mJBik>tOx$#xtr=3rmIsg=;@X#2)^9nOv#Eon5^6z!r(4a!KK^yp77;bXF z_{o+p)u>Ra(SLiRlg;x#&smU|;ugT(&xazFEJ{fV>m%Z0v1hxJo zab2+axECu4H&B|hsD~OR9o=6I`n8am)PHA2Xb1X0ULI3@n0UQQ{TcwyogUB1WRL}1 zOmX4XlN;{bfEw61rPix?XW8BgL}2s&+RGNt&1isA0W$XxB)3B}BBVq<2e6Z<0}?7J8YN#H;3$|NI2GoX zy9{}Z94&ua8VZDyfByR>hw-*8Yo!LZWdAAhX;H_Nl@F|eP5q_r!(Qu5p5S7fBAIG&< z;9Do$mXd9;9G!C(Q%YcuVU8I^=YsA<2mpwLm-DBlME4>5aF*OLzE%#MWN_F)lk5b5 zgFgzp;vqwbdl^(A>tVgnTIq(~EuL{fox>b()^S;I?{IFt)?eB$_=56QF|x>EooitO zl9r!^%$bHP@#7a_hPKTz$kW%s1dMz&+d7TH&?}@ISgIt2x&Mb^r=3Z_z zHl8m^W=NfzXJm&&8;P99+z6~?^UR-CR4v1ae}VDHK$3bu|kBUu2Hi{p~0$n z>n^fw->_rB>=ycz;&=VgBJE7F)spNnT-@?&##J1;Vf1`C9u=HWTLRjPxTA`GRm%3?Zz@hPsZqjw$ZTu~h6i-3Sy> zp3EI0O5B$uSH^C8_;s&@Q0#@k^Rwynw5vvDtvWhJKk(X$-tD;ofXpKI!K_yy@HtD* zTT9nhlvf1pw-dFB z_x<{KY=qOx`QcR7qf$QrKQ@mF2FsSTlM(0wlmoZxdr{I#5?{(|d2=7Yrr=jgGe(Mj zYJ!j${_&_U3%4zVQM>Qgu=r;D_c79zF$DeC&yg41bAOTXmjn23zpdZAg(09ThsFYJ z`=iyPk+Z_@Jz5Dn2E&VZpZ5@^xX=@5x@$ynsyO#ZrkoAPrf82uQEfK9>M%2jmh-UD zEPXGa{YZ13n)SHb0YddC@Sqt?Ktn0h1|YXu@chZ{E}hG~3p}OPNm#LPX8aCC@50CP zYd1CNc78?g9svzv`%spZNk=}wNehT&cD0Co6Fhc<-+_HCoHKQE$%l;=^;x(PF#?@S z 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 ×2 \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/ugrid_element_centres.svg b/docs/src/further_topics/ugrid/images/ugrid_element_centres.svg new file mode 100644 index 0000000000..13b885d600 --- /dev/null +++ b/docs/src/further_topics/ugrid/images/ugrid_element_centres.svg @@ -0,0 +1 @@ +`face_node_connectivity`xy`node_coordinates`xy`face_coordinates`151512 ×41212`face_coordinates``node_coordinates` \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/ugrid_node_independence.svg b/docs/src/further_topics/ugrid/images/ugrid_node_independence.svg new file mode 100644 index 0000000000..ba72c42ffa --- /dev/null +++ b/docs/src/further_topics/ugrid/images/ugrid_node_independence.svg @@ -0,0 +1 @@ +` \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/images/ugrid_variable_faces.svg b/docs/src/further_topics/ugrid/images/ugrid_variable_faces.svg new file mode 100644 index 0000000000..378978abc3 --- /dev/null +++ b/docs/src/further_topics/ugrid/images/ugrid_variable_faces.svg @@ -0,0 +1 @@ +`face_node_connectivity`12 ×6 \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/index.rst b/docs/src/further_topics/ugrid/index.rst new file mode 100644 index 0000000000..81ba24428a --- /dev/null +++ b/docs/src/further_topics/ugrid/index.rst @@ -0,0 +1,54 @@ +.. include:: ../../common_links.inc + +.. _ugrid: + +Mesh Support +************ + +Iris includes specialised handling of mesh-located data (as opposed to +grid-located data). Iris and its :ref:`partner packages ` are +designed to make working with mesh-located data as simple as possible, with new +capabilities being added all the time. More detail is in this section and in +the :mod:`iris.experimental.ugrid` API documentation. + +This mesh support is based on the `CF-UGRID Conventions`__; UGRID-conformant +meshes + data can be loaded from a file into Iris' data model, and meshes + +data represented in Iris' data model can be saved as a UGRID-conformant file. + +---- + +Meshes are different + Mesh-located data is fundamentally different to grid-located data. + Many of Iris' existing operations need adapting before they can work with + mesh-located data, and in some cases entirely new concepts are needed. + **Read the detail in these pages before jumping into your own code.** +Iris' mesh support is experimental + This is a rapidly evolving part of the codebase at time of writing + (``Jan 2022``), as we continually expand the operations that work with mesh + data. **Be prepared for breaking changes even in minor releases.** +:ref:`Get involved! ` + We know meshes are an exciting new area for much of Earth science, so we hope + there are a lot of you with new files/ideas/wishlists, and we'd love to hear + more 🙂. + +---- + +Read on to find out more... + +* :doc:`data_model` - learn why the mesh experience is so different. +* :doc:`partner_packages` - meet some optional dependencies that provide powerful mesh operations. +* :doc:`operations` - experience how your workflows will look when written for mesh data. + +.. + Need an actual TOC to get Sphinx working properly, but have hidden it in + favour of the custom bullets above. + +.. toctree:: + :hidden: + :maxdepth: 1 + + data_model + partner_packages + operations + +__ CF-UGRID_ diff --git a/docs/src/further_topics/ugrid/operations.rst b/docs/src/further_topics/ugrid/operations.rst new file mode 100644 index 0000000000..f96e3e406c --- /dev/null +++ b/docs/src/further_topics/ugrid/operations.rst @@ -0,0 +1,995 @@ +.. _ugrid operations: + +Working with Mesh Data +********************** + +.. note:: Several of the operations below rely on the optional dependencies + mentioned in :doc:`partner_packages`. + +Operations Summary +------------------ +.. list-table:: + :align: left + :widths: 35, 75 + + * - `Making a Mesh`_ + - |tagline: making a mesh| + * - `Making a Cube`_ + - |tagline: making a cube| + * - `Save`_ + - |tagline: save| + * - `Load`_ + - |tagline: load| + * - `Plotting`_ + - |tagline: plotting| + * - `Region Extraction`_ + - |tagline: region extraction| + * - `Regridding`_ + - |tagline: regridding| + * - `Equality`_ + - |tagline: equality| + * - `Combining Cubes`_ + - |tagline: combining cubes| + * - `Arithmetic`_ + - |tagline: arithmetic| + +.. + Below: use demo code over prose wherever workable. Headings aren't an + exhaustive list (can you think of any other popular operations?). + +Making a Mesh +------------- +.. |tagline: making a mesh| replace:: |new| + +.. rubric:: |tagline: making a mesh| + +**Already have a file?** Consider skipping to `Load`_. + +Creating Iris objects from scratch is a highly useful skill for testing code +and improving understanding of how Iris works. This knowledge will likely prove +particularly useful when converting data into the Iris mesh data model from +structured formats and non-UGRID mesh formats. + +The objects created in this example will be used where possible in the +subsequent example operations on this page. + +.. dropdown:: :opticon:`code` + + .. doctest:: ugrid_operations + + >>> import numpy as np + + >>> from iris.coords import AuxCoord + >>> from iris.experimental.ugrid import Connectivity, Mesh + + # Going to create the following mesh + # (node indices are shown to aid understanding): + # + # 0----1 + # | |\ + # | + |+\ + # 2----3--4 + + >>> node_x = AuxCoord( + ... points=[0.0, 5.0, 0.0, 5.0, 8.0], + ... standard_name="longitude", + ... units="degrees_east", + ... long_name="node_x_coordinates", + ... ) + >>> node_y = AuxCoord(points=[3.0, 3.0, 0.0, 0.0, 0.0], standard_name="latitude") + + >>> face_x = AuxCoord([2.0, 6.0], "longitude") + >>> face_y = AuxCoord([1.0, 1.0], "latitude") + + >>> edge_node_c = Connectivity( + ... indices=[[0, 1], [0, 2], [1, 3], [1, 4], [2, 3], [3, 4]], + ... cf_role="edge_node_connectivity", + ... attributes={"demo": "Supports every standard CF property"}, + ... ) + + # Create some dead-centre edge coordinates. + >>> edge_x, edge_y = [ + ... AuxCoord( + ... node_coord.points[edge_node_c.indices_by_location()].mean(axis=1), + ... node_coord.standard_name, + ... ) + ... for node_coord in (node_x, node_y) + ... ] + + >>> face_indices = np.ma.masked_equal([[0, 1, 3, 2], [1, 4, 3, 999]], 999) + >>> face_node_c = Connectivity( + ... indices=face_indices, cf_role="face_node_connectivity" + ... ) + + >>> my_mesh = Mesh( + ... long_name="my_mesh", + ... topology_dimension=2, # Supports 2D (face) elements. + ... node_coords_and_axes=[(node_x, "x"), (node_y, "y")], + ... connectivities=[edge_node_c, face_node_c], + ... edge_coords_and_axes=[(edge_x, "x"), (edge_y, "y")], + ... face_coords_and_axes=[(face_x, "x"), (face_y, "y")], + ... ) + + >>> print(my_mesh) + Mesh : 'my_mesh' + topology_dimension: 2 + node + node_dimension: 'Mesh2d_node' + node coordinates + + + edge + edge_dimension: 'Mesh2d_edge' + edge_node_connectivity: + edge coordinates + + + face + face_dimension: 'Mesh2d_face' + face_node_connectivity: + face coordinates + + + long_name: 'my_mesh' + + +.. _making a cube: + +Making a Cube (with a Mesh) +--------------------------- +.. |tagline: making a cube| replace:: |unchanged| + +.. rubric:: |tagline: making a cube| + +Creating a :class:`~iris.cube.Cube` is unchanged; the +:class:`~iris.experimental.ugrid.Mesh` is linked via a +:class:`~iris.experimental.ugrid.MeshCoord` (see :ref:`ugrid MeshCoords`): + +.. dropdown:: :opticon:`code` + + .. doctest:: ugrid_operations + + >>> import numpy as np + + >>> from iris.coords import DimCoord + >>> from iris.cube import Cube, CubeList + + >>> vertical_levels = DimCoord([0, 1, 2], "height") + + >>> my_cubelist = CubeList() + >>> for conn in (edge_node_c, face_node_c): + ... location = conn.location + ... mesh_coord_x, mesh_coord_y = my_mesh.to_MeshCoords(location) + ... data_shape = (len(conn.indices_by_location()), len(vertical_levels.points)) + ... data_array = np.arange(np.prod(data_shape)).reshape(data_shape) + ... + ... my_cubelist.append( + ... Cube( + ... data=data_array, + ... long_name=f"{location}_data", + ... units="K", + ... dim_coords_and_dims=[(vertical_levels, 1)], + ... aux_coords_and_dims=[(mesh_coord_x, 0), (mesh_coord_y, 0)], + ... ) + ... ) + + >>> print(my_cubelist) + 0: edge_data / (K) (-- : 6; height: 3) + 1: face_data / (K) (-- : 2; height: 3) + + >>> for cube in my_cubelist: + ... print(f"{cube.name()}: {cube.mesh.name()}, {cube.location}") + edge_data: my_mesh, edge + face_data: my_mesh, face + + >>> print(my_cubelist.extract_cube("edge_data")) + edge_data / (K) (-- : 6; height: 3) + Dimension coordinates: + height - x + Mesh coordinates: + latitude x - + longitude x - + + +Save +---- +.. |tagline: save| replace:: |unchanged| + +.. rubric:: |tagline: save| + +.. note:: UGRID saving support is limited to the NetCDF file format. + +The Iris saving process automatically detects if the :class:`~iris.cube.Cube` +has an associated :class:`~iris.experimental.ugrid.Mesh` and automatically +saves the file in a UGRID-conformant format: + +.. dropdown:: :opticon:`code` + + .. doctest:: ugrid_operations + + >>> from subprocess import run + + >>> from iris import save + + >>> cubelist_path = "my_cubelist.nc" + >>> save(my_cubelist, cubelist_path) + + >>> ncdump_result = run(["ncdump", "-h", cubelist_path], capture_output=True) + >>> print(ncdump_result.stdout.decode().replace("\t", " ")) + netcdf my_cubelist { + dimensions: + Mesh2d_node = 5 ; + Mesh2d_edge = 6 ; + Mesh2d_face = 2 ; + height = 3 ; + my_mesh_face_N_nodes = 4 ; + my_mesh_edge_N_nodes = 2 ; + variables: + int my_mesh ; + my_mesh:cf_role = "mesh_topology" ; + my_mesh:topology_dimension = 2 ; + my_mesh:long_name = "my_mesh" ; + my_mesh:node_coordinates = "longitude latitude" ; + my_mesh:edge_coordinates = "longitude_0 latitude_0" ; + my_mesh:face_coordinates = "longitude_1 latitude_1" ; + my_mesh:face_node_connectivity = "mesh2d_face" ; + my_mesh:edge_node_connectivity = "mesh2d_edge" ; + double longitude(Mesh2d_node) ; + longitude:units = "degrees_east" ; + longitude:standard_name = "longitude" ; + longitude:long_name = "node_x_coordinates" ; + double latitude(Mesh2d_node) ; + latitude:standard_name = "latitude" ; + double longitude_0(Mesh2d_edge) ; + longitude_0:standard_name = "longitude" ; + double latitude_0(Mesh2d_edge) ; + latitude_0:standard_name = "latitude" ; + double longitude_1(Mesh2d_face) ; + longitude_1:standard_name = "longitude" ; + double latitude_1(Mesh2d_face) ; + latitude_1:standard_name = "latitude" ; + int64 mesh2d_face(Mesh2d_face, my_mesh_face_N_nodes) ; + mesh2d_face:_FillValue = -1LL ; + mesh2d_face:cf_role = "face_node_connectivity" ; + mesh2d_face:start_index = 0LL ; + int64 mesh2d_edge(Mesh2d_edge, my_mesh_edge_N_nodes) ; + mesh2d_edge:demo = "Supports every standard CF property" ; + mesh2d_edge:cf_role = "edge_node_connectivity" ; + mesh2d_edge:start_index = 0LL ; + int64 edge_data(Mesh2d_edge, height) ; + edge_data:long_name = "edge_data" ; + edge_data:units = "K" ; + edge_data:mesh = "my_mesh" ; + edge_data:location = "edge" ; + int64 height(height) ; + height:standard_name = "height" ; + int64 face_data(Mesh2d_face, height) ; + face_data:long_name = "face_data" ; + face_data:units = "K" ; + face_data:mesh = "my_mesh" ; + face_data:location = "face" ; + + // global attributes: + :Conventions = "CF-1.7" ; + } + + +The :func:`iris.experimental.ugrid.save_mesh` function allows +:class:`~iris.experimental.ugrid.Mesh`\es to be saved to file without +associated :class:`~iris.cube.Cube`\s: + +.. dropdown:: :opticon:`code` + + .. doctest:: ugrid_operations + + >>> from subprocess import run + + >>> from iris.experimental.ugrid import save_mesh + + >>> mesh_path = "my_mesh.nc" + >>> save_mesh(my_mesh, mesh_path) + + >>> ncdump_result = run(["ncdump", "-h", mesh_path], capture_output=True) + >>> print(ncdump_result.stdout.decode().replace("\t", " ")) + netcdf my_mesh { + dimensions: + Mesh2d_node = 5 ; + Mesh2d_edge = 6 ; + Mesh2d_face = 2 ; + my_mesh_face_N_nodes = 4 ; + my_mesh_edge_N_nodes = 2 ; + variables: + int my_mesh ; + my_mesh:cf_role = "mesh_topology" ; + my_mesh:topology_dimension = 2 ; + my_mesh:long_name = "my_mesh" ; + my_mesh:node_coordinates = "longitude latitude" ; + my_mesh:edge_coordinates = "longitude_0 latitude_0" ; + my_mesh:face_coordinates = "longitude_1 latitude_1" ; + my_mesh:face_node_connectivity = "mesh2d_face" ; + my_mesh:edge_node_connectivity = "mesh2d_edge" ; + double longitude(Mesh2d_node) ; + longitude:units = "degrees_east" ; + longitude:standard_name = "longitude" ; + longitude:long_name = "node_x_coordinates" ; + double latitude(Mesh2d_node) ; + latitude:standard_name = "latitude" ; + double longitude_0(Mesh2d_edge) ; + longitude_0:standard_name = "longitude" ; + double latitude_0(Mesh2d_edge) ; + latitude_0:standard_name = "latitude" ; + double longitude_1(Mesh2d_face) ; + longitude_1:standard_name = "longitude" ; + double latitude_1(Mesh2d_face) ; + latitude_1:standard_name = "latitude" ; + int64 mesh2d_face(Mesh2d_face, my_mesh_face_N_nodes) ; + mesh2d_face:_FillValue = -1LL ; + mesh2d_face:cf_role = "face_node_connectivity" ; + mesh2d_face:start_index = 0LL ; + int64 mesh2d_edge(Mesh2d_edge, my_mesh_edge_N_nodes) ; + mesh2d_edge:demo = "Supports every standard CF property" ; + mesh2d_edge:cf_role = "edge_node_connectivity" ; + mesh2d_edge:start_index = 0LL ; + + // global attributes: + :Conventions = "CF-1.7" ; + } + + +Load +---- +.. |tagline: load| replace:: |different| - UGRID parsing is opt-in + +.. rubric:: |tagline: load| + +.. note:: UGRID loading support is limited to the NetCDF file format. + +While Iris' UGRID support remains :mod:`~iris.experimental`, parsing UGRID when +loading a file remains **optional**. To load UGRID data from a file into the +Iris mesh data model, use the +:const:`iris.experimental.ugrid.PARSE_UGRID_ON_LOAD` context manager: + +.. dropdown:: :opticon:`code` + + .. doctest:: ugrid_operations + + >>> from iris import load + >>> from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD + + >>> with PARSE_UGRID_ON_LOAD.context(): + ... loaded_cubelist = load(cubelist_path) + + # Sort CubeList to ensure consistent result. + >>> loaded_cubelist.sort(key=lambda cube: cube.name()) + >>> print(loaded_cubelist) + 0: edge_data / (K) (-- : 6; height: 3) + 1: face_data / (K) (-- : 2; height: 3) + +All the existing loading functionality still operates on UGRID-compliant +data - :class:`~iris.Constraint`\s, callbacks, :func:`~iris.load_cube` +etcetera: + +.. dropdown:: :opticon:`code` + + .. doctest:: ugrid_operations + + >>> from iris import Constraint, load_cube + + >>> with PARSE_UGRID_ON_LOAD.context(): + ... ground_cubelist = load(cubelist_path, Constraint(height=0)) + ... face_cube = load_cube(cubelist_path, "face_data") + + # Sort CubeList to ensure consistent result. + >>> ground_cubelist.sort(key=lambda cube: cube.name()) + >>> print(ground_cubelist) + 0: edge_data / (K) (-- : 6) + 1: face_data / (K) (-- : 2) + + >>> print(face_cube) + face_data / (K) (-- : 2; height: 3) + Dimension coordinates: + height - x + Mesh coordinates: + latitude x - + longitude x - + Attributes: + Conventions 'CF-1.7' + +.. note:: + + We recommend caution if constraining on coordinates associated with a + :class:`~iris.experimental.ugrid.Mesh`. An individual coordinate value + might not be shared by any other data points, and using a coordinate range + will demand notably higher performance given the size of the dimension + versus structured grids + (:ref:`see the data model detail `). + +The :func:`iris.experimental.ugrid.load_mesh` and +:func:`~iris.experimental.ugrid.load_meshes` functions allow only +:class:`~iris.experimental.ugrid.Mesh`\es to be loaded from a file without +creating any associated :class:`~iris.cube.Cube`\s: + +.. dropdown:: :opticon:`code` + + .. doctest:: ugrid_operations + + >>> from iris.experimental.ugrid import load_mesh + + >>> with PARSE_UGRID_ON_LOAD.context(): + ... loaded_mesh = load_mesh(cubelist_path) + + >>> print(loaded_mesh) + Mesh : 'my_mesh' + topology_dimension: 2 + node + node_dimension: 'Mesh2d_node' + node coordinates + shape(5,)> + shape(5,)> + edge + edge_dimension: 'Mesh2d_edge' + edge_node_connectivity: shape(6, 2)> + edge coordinates + shape(6,)> + shape(6,)> + face + face_dimension: 'Mesh2d_face' + face_node_connectivity: shape(2, 4)> + face coordinates + shape(2,)> + shape(2,)> + long_name: 'my_mesh' + var_name: 'my_mesh' + +Plotting +-------- +.. |tagline: plotting| replace:: |different| - plot with GeoVista + +.. rubric:: |tagline: plotting| + +The Cartopy-Matplotlib combination is not optimised for displaying the high +number of irregular shapes associated with meshes. Thankfully mesh +visualisation is already popular in many other fields (e.g. CGI, gaming, +SEM microscopy), so there is a wealth of tooling available, which +:ref:`ugrid geovista` harnesses for cartographic plotting. + +GeoVista's default behaviour is to convert lat-lon information into full XYZ +coordinates so the data is visualised on the surface of a 3D globe. The plots +are interactive by default, so it's easy to explore the data in detail. + +2D projections have also been demonstrated in proofs of concept, and will +be added to API in the near future. + +This first example uses GeoVista to plot the ``face_cube`` that we created +earlier: + +.. dropdown:: :opticon:`code` + + .. code-block:: python + + >>> from geovista import GeoPlotter, Transform + >>> from geovista.common import to_xyz + + + # We'll re-use this to plot some real global data later. + >>> def cube_faces_to_polydata(cube): + ... lons, lats = cube.mesh.node_coords + ... face_node = cube.mesh.face_node_connectivity + ... indices = face_node.indices_by_location() + ... + ... mesh = Transform.from_unstructured( + ... lons.points, + ... lats.points, + ... indices, + ... data=cube.data, + ... name=f"{cube.name()} / {cube.units}", + ... start_index=face_node.start_index, + ... ) + ... return mesh + + >>> print(face_cube) + face_data / (K) (-- : 2; height: 3) + Dimension coordinates: + height - x + Mesh coordinates: + latitude x - + longitude x - + Attributes: + Conventions 'CF-1.7' + + # Convert our mesh+data to a PolyData object. + # Just plotting a single height level. + >>> face_polydata = cube_faces_to_polydata(face_cube[:, 0]) + >>> print(face_polydata) + PolyData (0x7ff4861ff4c0) + N Cells: 2 + N Points: 5 + X Bounds: 9.903e-01, 1.000e+00 + Y Bounds: 0.000e+00, 1.392e-01 + Z Bounds: 6.123e-17, 5.234e-02 + N Arrays: 2 + + # Create the GeoVista plotter and add our mesh+data to it. + >>> my_plotter = GeoPlotter() + >>> my_plotter.add_coastlines(color="black") + >>> my_plotter.add_base_layer(color="grey") + >>> my_plotter.add_mesh(face_polydata) + + # Centre the camera on the data. + >>> camera_region = to_xyz( + ... face_cube.coord("longitude").points, + ... face_cube.coord("latitude").points, + ... radius=3, + ... ) + >>> camera_pos = camera_region.mean(axis=0) + >>> my_plotter.camera.position = camera_pos + + >>> my_plotter.show() + + .. image:: images/plotting_basic.png + :alt: A GeoVista plot of the basic example Mesh. + + This artificial data makes West Africa rather chilly! + +Here's another example using a global cubed-sphere data set: + +.. dropdown:: :opticon:`code` + + .. code-block:: python + + >>> from iris import load_cube + >>> from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD + + # Demonstrating with a global data set. + # You could also download this file from github.com/SciTools/iris-test-data. + >>> from iris.tests import get_data_path + >>> file_path = get_data_path( + ... [ + ... "NetCDF", + ... "unstructured_grid", + ... "lfric_surface_mean.nc", + ... ] + ... ) + >>> with PARSE_UGRID_ON_LOAD.context(): + ... global_cube = load_cube(file_path, "tstar_sea") + >>> print(global_cube) + sea_surface_temperature / (K) (-- : 1; -- : 13824) + Mesh coordinates: + latitude - x + longitude - x + Auxiliary coordinates: + time x - + Cell methods: + mean time (300 s) + mean time_counter + Attributes: + Conventions UGRID + description Created by xios + interval_operation 300 s + interval_write 1 d + name lfric_surface + online_operation average + timeStamp 2020-Feb-07 16:23:14 GMT + title Created by xios + uuid 489bcef5-3d1c-4529-be42-4ab5f8c8497b + + >>> global_polydata = cube_faces_to_polydata(global_cube) + >>> print(global_polydata) + PolyData (0x7f761b536160) + N Cells: 13824 + N Points: 13826 + X Bounds: -1.000e+00, 1.000e+00 + Y Bounds: -1.000e+00, 1.000e+00 + Z Bounds: -1.000e+00, 1.000e+00 + N Arrays: 2 + + >>> my_plotter = GeoPlotter() + >>> my_plotter.add_coastlines() + >>> my_plotter.add_mesh(global_polydata, show_edges=True) + + >>> my_plotter.show() + + .. image:: images/plotting_global.png + :alt: A GeoVista plot of a global sea surface temperature Mesh. + +Region Extraction +----------------- +.. |tagline: region extraction| replace:: |different| - use GeoVista for mesh analysis + +.. rubric:: |tagline: region extraction| + +As described in :doc:`data_model`, indexing for a range along a +:class:`~iris.cube.Cube`\'s :meth:`~iris.cube.Cube.mesh_dim` will not provide +a contiguous region, since **position on the unstructured dimension is +unrelated to spatial position**. This means that subsetted +:class:`~iris.experimental.ugrid.MeshCoord`\s cannot be reliably interpreted +as intended, and subsetting a :class:`~iris.experimental.ugrid.MeshCoord` is +therefore set to return an :class:`~iris.coords.AuxCoord` instead - breaking +the link between :class:`~iris.cube.Cube` and +:class:`~iris.experimental.ugrid.Mesh`: + +.. dropdown:: :opticon:`code` + + .. doctest:: ugrid_operations + + >>> edge_cube = my_cubelist.extract_cube("edge_data") + >>> print(edge_cube) + edge_data / (K) (-- : 6; height: 3) + Dimension coordinates: + height - x + Mesh coordinates: + latitude x - + longitude x - + + # Sub-setted MeshCoords have become AuxCoords. + >>> print(edge_cube[:-1]) + edge_data / (K) (-- : 5; height: 3) + Dimension coordinates: + height - x + Auxiliary coordinates: + latitude x - + longitude x - + +Extracting a region therefore requires extra steps - to determine the spatial +position of the data points before they can be analysed as inside/outside the +selected region. The recommended way to do this is using tools provided by +:ref:`ugrid geovista`, which is optimised for performant mesh analysis. + +This approach centres around using :meth:`geovista.geodesic.BBox.enclosed` to +get the subset of the original mesh that is inside the +:class:`~geovista.geodesic.BBox`. This subset :class:`pyvista.PolyData` object +includes the original indices of each datapoint - the ``vtkOriginalCellIds`` +array, which can be used to index the original :class:`~iris.cube.Cube`. Since +we **know** that this subset :class:`~iris.cube.Cube` represents a regional +mesh, we then reconstruct a :class:`~iris.experimental.ugrid.Mesh` from the +:class:`~iris.cube.Cube`\'s :attr:`~iris.cube.Cube.aux_coords` using +:meth:`iris.experimental.ugrid.Mesh.from_coords`: + +.. + Not using doctest here as want to keep GeoVista as optional dependency. + +.. dropdown:: :opticon:`code` + + .. code-block:: python + + >>> from geovista import Transform + >>> from geovista.geodesic import BBox + >>> from iris import load_cube + >>> from iris.experimental.ugrid import Mesh, PARSE_UGRID_ON_LOAD + + # Need a larger dataset to demonstrate this operation. + # You could also download this file from github.com/SciTools/iris-test-data. + >>> from iris.tests import get_data_path + >>> file_path = get_data_path( + ... [ + ... "NetCDF", + ... "unstructured_grid", + ... "lfric_ngvat_2D_72t_face_half_levels_main_conv_rain.nc", + ... ] + ... ) + + >>> with PARSE_UGRID_ON_LOAD.context(): + ... global_cube = load_cube(file_path, "conv_rain") + >>> print(global_cube) + surface_convective_rainfall_rate / (kg m-2 s-1) (-- : 72; -- : 864) + Mesh coordinates: + latitude - x + longitude - x + Auxiliary coordinates: + time x - + Cell methods: + point time + Attributes: + Conventions UGRID + description Created by xios + interval_operation 300 s + interval_write 300 s + name lfric_ngvat_2D_72t_face_half_levels_main_conv_rain + online_operation instant + timeStamp 2020-Oct-18 21:18:35 GMT + title Created by xios + uuid b3dc0fb4-9828-4663-a5ac-2a5763280159 + + # Convert the Mesh to a GeoVista PolyData object. + >>> lons, lats = global_cube.mesh.node_coords + >>> face_node = global_cube.mesh.face_node_connectivity + >>> indices = face_node.indices_by_location() + >>> global_polydata = Transform.from_unstructured( + ... lons.points, lats.points, indices, start_index=face_node.start_index + ... ) + + # Define a region of 4 corners connected by great circles. + # Specialised sub-classes of BBox are also available e.g. panel/wedge. + >>> region = BBox(lons=[0, 70, 70, 0], lats=[-25, -25, 45, 45]) + # 'Apply' the region to the PolyData object. + >>> region_polydata = region.enclosed(global_polydata, preference="center") + # Get the remaining face indices, to use for indexing the Cube. + >>> indices = region_polydata["vtkOriginalCellIds"] + + >>> print(type(indices)) + + # 101 is smaller than the original 864. + >>> print(len(indices)) + 101 + >>> print(indices[:10]) + [ 6 7 8 9 10 11 18 19 20 21] + + # Use the face indices to subset the global cube. + >>> region_cube = global_cube[:, indices] + + # In this case we **know** the indices correspond to a contiguous + # region, so we will convert the sub-setted Cube back into a + # Cube-with-Mesh. + >>> new_mesh = Mesh.from_coords(*region_cube.coords(dimensions=1)) + >>> new_mesh_coords = new_mesh.to_MeshCoords(global_cube.location) + >>> for coord in new_mesh_coords: + ... region_cube.remove_coord(coord.name()) + ... region_cube.add_aux_coord(coord, 1) + + # A Mesh-Cube with a subset (101) of the original 864 faces. + >>> print(region_cube) + surface_convective_rainfall_rate / (kg m-2 s-1) (-- : 72; -- : 101) + Mesh coordinates: + latitude - x + longitude - x + Auxiliary coordinates: + time x - + Cell methods: + point time + Attributes: + Conventions UGRID + description Created by xios + interval_operation 300 s + interval_write 300 s + name lfric_ngvat_2D_72t_face_half_levels_main_conv_rain + online_operation instant + timeStamp 2020-Oct-18 21:18:35 GMT + title Created by xios + uuid b3dc0fb4-9828-4663-a5ac-2a5763280159 + +Regridding +---------- +.. |tagline: regridding| replace:: |different| - use iris-esmf-regrid for mesh regridders + +.. rubric:: |tagline: regridding| + +Regridding to or from a mesh requires different logic than Iris' existing +regridders, which are designed for structured grids. For this we recommend +ESMF's powerful regridding tools, which integrate with Iris' mesh data model +via the :ref:`ugrid iris-esmf-regrid` package. + +.. todo: inter-sphinx links when available. + +Regridding is achieved via the +:class:`esmf_regrid.experimental.unstructured_scheme.MeshToGridESMFRegridder` +and +:class:`~esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder` +classes. Regridding from a source :class:`~iris.cube.Cube` to a target +:class:`~iris.cube.Cube` involves initialising and then calling one of these +classes. Initialising is done by passing in the source and target +:class:`~iris.cube.Cube` as arguments. The regridder is then called by passing +the source :class:`~iris.cube.Cube` as an argument. We can demonstrate this +with the +:class:`~esmf_regrid.experimental.unstructured_scheme.MeshToGridESMFRegridder`: + +.. + Not using doctest here as want to keep iris-esmf-regrid as optional dependency. + +.. dropdown:: :opticon:`code` + + .. code-block:: python + + >>> from esmf_regrid.experimental.unstructured_scheme import MeshToGridESMFRegridder + >>> from iris import load, load_cube + >>> from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD + + # You could also download these files from github.com/SciTools/iris-test-data. + >>> from iris.tests import get_data_path + >>> mesh_file = get_data_path( + ... ["NetCDF", "unstructured_grid", "lfric_surface_mean.nc"] + ... ) + >>> grid_file = get_data_path( + ... ["NetCDF", "regrid", "regrid_template_global_latlon.nc"] + ... ) + + # Load a list of cubes defined on the same Mesh. + >>> with PARSE_UGRID_ON_LOAD.context(): + ... mesh_cubes = load(mesh_file) + + # Extract a specific cube. + >>> mesh_cube1 = mesh_cubes.extract_cube("sea_surface_temperature") + >>> print(mesh_cube1) + sea_surface_temperature / (K) (-- : 1; -- : 13824) + Mesh coordinates: + latitude - x + longitude - x + Auxiliary coordinates: + time x - + Cell methods: + mean time (300 s) + mean time_counter + Attributes: + Conventions UGRID + description Created by xios + interval_operation 300 s + interval_write 1 d + name lfric_surface + online_operation average + timeStamp 2020-Feb-07 16:23:14 GMT + title Created by xios + uuid 489bcef5-3d1c-4529-be42-4ab5f8c8497b + + # Load the target grid. + >>> sample_grid = load_cube(grid_file) + >>> print(sample_grid) + sample_grid / (unknown) (latitude: 180; longitude: 360) + Dimension coordinates: + latitude x - + longitude - x + Attributes: + Conventions 'CF-1.7' + + # Initialise the regridder. + >>> rg = MeshToGridESMFRegridder(mesh_cube1, sample_grid) + + # Regrid the mesh cube cube. + >>> result1 = rg(mesh_cube1) + >>> print(result1) + sea_surface_temperature / (K) (-- : 1; latitude: 180; longitude: 360) + Dimension coordinates: + latitude - x - + longitude - - x + Auxiliary coordinates: + time x - - + Cell methods: + mean time (300 s) + mean time_counter + Attributes: + Conventions UGRID + description Created by xios + interval_operation 300 s + interval_write 1 d + name lfric_surface + online_operation average + timeStamp 2020-Feb-07 16:23:14 GMT + title Created by xios + uuid 489bcef5-3d1c-4529-be42-4ab5f8c8497b + +.. note:: + + **All** :class:`~iris.cube.Cube` :attr:`~iris.cube.Cube.attributes` are + retained when regridding, so watch out for any attributes that reference + the format (there are several in these examples) - you may want to manually + remove them to avoid later confusion. + +The initialisation process is computationally expensive so we use caching to +improve performance. Once a regridder has been initialised, it can be used on +any :class:`~iris.cube.Cube` which has been defined on the same +:class:`~iris.experimental.ugrid.Mesh` (or on the same **grid** in the case of +:class:`~esmf_regrid.experimental.unstructured_scheme.GridToMeshESMFRegridder`). +Since calling a regridder is usually a lot faster than initialising, reusing +regridders can save a lot of time. We can demonstrate the reuse of the +previously initialised regridder: + +.. dropdown:: :opticon:`code` + + .. code-block:: python + + # Extract a different cube defined on te same Mesh. + >>> mesh_cube2 = mesh_cubes.extract_cube("precipitation_flux") + >>> print(mesh_cube2) + precipitation_flux / (kg m-2 s-1) (-- : 1; -- : 13824) + Mesh coordinates: + latitude - x + longitude - x + Auxiliary coordinates: + time x - + Cell methods: + mean time (300 s) + mean time_counter + Attributes: + Conventions UGRID + description Created by xios + interval_operation 300 s + interval_write 1 d + name lfric_surface + online_operation average + timeStamp 2020-Feb-07 16:23:14 GMT + title Created by xios + uuid 489bcef5-3d1c-4529-be42-4ab5f8c8497b + + # Regrid the new mesh cube using the same regridder. + >>> result2 = rg(mesh_cube2) + >>> print(result2) + precipitation_flux / (kg m-2 s-1) (-- : 1; latitude: 180; longitude: 360) + Dimension coordinates: + latitude - x - + longitude - - x + Auxiliary coordinates: + time x - - + Cell methods: + mean time (300 s) + mean time_counter + Attributes: + Conventions UGRID + description Created by xios + interval_operation 300 s + interval_write 1 d + name lfric_surface + online_operation average + timeStamp 2020-Feb-07 16:23:14 GMT + title Created by xios + uuid 489bcef5-3d1c-4529-be42-4ab5f8c8497b + +Support also exists for saving and loading previously initialised regridders - +:func:`esmf_regrid.experimental.io.save_regridder` and +:func:`~esmf_regrid.experimental.io.load_regridder` - so that they can be +re-used by future scripts. + +Equality +-------- +.. |tagline: equality| replace:: |unchanged| + +.. rubric:: |tagline: equality| + +:class:`~iris.experimental.ugrid.Mesh` comparison is supported, and comparing +two ':class:`~iris.experimental.ugrid.Mesh`-:class:`~iris.cube.Cube`\s' will +include a comparison of the respective +:class:`~iris.experimental.ugrid.Mesh`\es, with no extra action needed by the +user. + +.. note:: + + Keep an eye on memory demand when comparing large + :class:`~iris.experimental.ugrid.Mesh`\es, but note that + :class:`~iris.experimental.ugrid.Mesh`\ equality is enabled for lazy + processing (:doc:`/userguide/real_and_lazy_data`), so if the + :class:`~iris.experimental.ugrid.Mesh`\es being compared are lazy the + process will use less memory than their total size. + +Combining Cubes +--------------- +.. |tagline: combining cubes| replace:: |pending| + +.. rubric:: |tagline: combining cubes| + +Merging or concatenating :class:`~iris.cube.Cube`\s (described in +:doc:`/userguide/merge_and_concat`) with two different +:class:`~iris.experimental.ugrid.Mesh`\es is not possible - a +:class:`~iris.cube.Cube` must be associated with just a single +:class:`~iris.experimental.ugrid.Mesh`, and merge/concatenate are not yet +capable of combining multiple :class:`~iris.experimental.ugrid.Mesh`\es into +one. + +:class:`~iris.cube.Cube`\s that include +:class:`~iris.experimental.ugrid.MeshCoord`\s can still be merged/concatenated +on dimensions other than the :meth:`~iris.cube.Cube.mesh_dim`, since such +:class:`~iris.cube.Cube`\s will by definition share the same +:class:`~iris.experimental.ugrid.Mesh`. + +.. seealso:: + + You may wish to investigate + :func:`iris.experimental.ugrid.recombine_submeshes`, which can be used + for a very specific type of :class:`~iris.experimental.ugrid.Mesh` + combination not detailed here. + +Arithmetic +---------- +.. |tagline: arithmetic| replace:: |pending| + +.. rubric:: |tagline: arithmetic| + +:class:`~iris.cube.Cube` Arithmetic (described in :doc:`/userguide/cube_maths`) +has not yet been adapted to handle :class:`~iris.cube.Cube`\s that include +:class:`~iris.experimental.ugrid.MeshCoord`\s. + + +.. todo: + Enumerate other popular operations that aren't yet possible + (and are they planned soon?) + +.. |new| replace:: ✨ New +.. |unchanged| replace:: ♻️ Unchanged +.. |different| replace:: ⚠️ Different +.. |pending| replace:: 🚧 Support Pending \ No newline at end of file diff --git a/docs/src/further_topics/ugrid/partner_packages.rst b/docs/src/further_topics/ugrid/partner_packages.rst new file mode 100644 index 0000000000..8e36f4ffc2 --- /dev/null +++ b/docs/src/further_topics/ugrid/partner_packages.rst @@ -0,0 +1,100 @@ +.. _ugrid partners: + +Iris' Mesh Partner Packages +**************************** +Python is an easy to use language and has formed a very strong collaborative +scientific community, which is why Iris is written in Python. *Performant* +Python relies on calls down to low level languages like C, which is ideal for +structured grid work since +they can be directly represented as NumPy arrays. This is more difficult when +working with unstructured meshes where extra steps are needed to determine data +position (:ref:`see the data model detail `), and we need +to find ways of again passing the operations down to more optimised languages. + +The Iris team are therefore developing 'wrapper' packages, which make it quick +and easy to analyse Iris mesh data via some popular Python packages that use +powerful tools under the hood, working in C and other languages. + +These solutions have been placed in their own 'partner packages' for several +reasons: + +* Can be useful to others who are not using Iris. + + * Everyone working with multi-dimensional geographic datasets shares common + problems that need solving. + * Wider user base = stronger community = better solutions. + +* Only some Iris users will need them - they are **optional** Iris dependencies. + + * They introduce a lot of new API. + * They introduce new large dependencies that take time to install and need + disk space. + +Below you can learn more about the partner packages and how they are useful. +Specifics of what operations would require their installation can be found in: +:doc:`operations`. + +.. important:: **Experimental** + + As with Iris' mesh support, these packages are still in the + experimental stages. They would love your feedback, but as immature + packages their API, documentation, test coverage and CI are still + 'under construction'. + + +.. _`ugrid geovista`: + +`GeoVista`_ +=========== +.. image:: images/geovistalogo.svg + :width: 300 + :class: no-scaled-link + +.. rubric:: "Cartographic rendering and mesh analytics powered by `PyVista`_" + +PyVista is described as "VTK for humans" - VTK is a very powerful toolkit for +working with meshes, and PyVista brings that power into the Python ecosystem. +GeoVista in turn makes it easy to use PyVista specifically for cartographic +work, designed from the start with the Iris +:class:`~iris.experimental.ugrid.Mesh` in mind. + +Applications +------------ +* Interactively plot mesh data: + + * On a 3D globe. + * On your favourite projection. + +* Extract a specific region from a mesh. +* Combine multiple meshes into one. + +.. _`ugrid iris-esmf-regrid`: + +`iris-esmf-regrid`_ +=================== +.. image:: images/iris-esmf-regrid.svg + :width: 300 + :class: no-scaled-link + +.. rubric:: "A collection of structured and unstructured ESMF regridding schemes for Iris" + +ESMF provide a sophisticated, performant regridding utility that supports a +variety of regridding types with both structured grids and unstructured meshes, +and this also has a flexible Python interface - ESMPy. iris-esmf-regrid takes +advantage of having a specific use-case - regridding Iris +:class:`~iris.cube.Cube`\s - to provide ESMPy-Iris wrappers that make the +process as easy as possible, with highly optimised performance. + +Applications +------------ +* Regrid structured to unstructured. +* Regrid unstructured to structured. +* Regrid with dask integration, computing in parallel and maintaining data + laziness. +* | Save a prepared regridder for re-use in subsequent runs. + | Regridders can even be re-used on sources with different masks - a + significant efficiency gain. + +.. _GeoVista: https://github.com/bjlittle/geovista +.. _PyVista: https://docs.pyvista.org/index.html +.. _iris-esmf-regrid: https://github.com/SciTools-incubator/iris-esmf-regrid diff --git a/docs/src/index.rst b/docs/src/index.rst index 8c3455aba9..e6a787a220 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -98,6 +98,15 @@ For **Iris 2.4** and earlier documentation please see the generated/gallery/index +.. toctree:: + :maxdepth: 1 + :caption: What's New in Iris + :hidden: + + whatsnew/latest + Archive + + .. toctree:: :maxdepth: 1 :caption: User Guide @@ -131,10 +140,11 @@ For **Iris 2.4** and earlier documentation please see the further_topics/metadata further_topics/lenient_metadata further_topics/lenient_maths + further_topics/ugrid/index .. toctree:: - :maxdepth: 1 + :maxdepth: 2 :caption: Developers Guide :name: development_index :hidden: @@ -153,6 +163,5 @@ For **Iris 2.4** and earlier documentation please see the :hidden: generated/api/iris - whatsnew/index techpapers/index copyright diff --git a/docs/src/installing.rst b/docs/src/installing.rst index e358bb42c9..37a8942ab3 100644 --- a/docs/src/installing.rst +++ b/docs/src/installing.rst @@ -85,10 +85,12 @@ local copy of Iris:: conda env create --force --file=requirements/ci/iris.yml conda activate iris-dev -The ``--force`` option is used when creating the environment, this is optional -and will force the any existing ``iris-dev`` conda environment to be deleted -first if present. This is useful when rebuilding your environment due to a -change in requirements. +.. note:: + + The ``--force`` option, used when creating the environment, first removes + any previously existing ``iris-dev`` environment of the same name. This is + particularly useful when rebuilding your environment due to a change in + requirements. The ``requirements/ci/iris.yml`` file defines the Iris development conda environment *name* and all the relevant *top level* `conda-forge` package diff --git a/docs/src/sphinxext/image_test_output.py b/docs/src/sphinxext/image_test_output.py new file mode 100644 index 0000000000..9e492a5be9 --- /dev/null +++ b/docs/src/sphinxext/image_test_output.py @@ -0,0 +1,78 @@ +# 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. + +import json +import re +from typing import Dict, List + +from docutils import nodes +from sphinx.application import Sphinx +from sphinx.util.docutils import SphinxDirective + +ImageRepo = Dict[str, List[str]] + +HASH_MATCH = re.compile(r"([^\/]+)\.png$") + + +def hash_from_url(url: str) -> str: + match = HASH_MATCH.search(url) + if not match: + raise ValueError(f"url {url} does not match form `http...hash.png`") + else: + return match.groups()[0] + + +class ImageTestDirective(SphinxDirective): + def run(self): + with open(self.config["image_test_json"], "r") as fh: + imagerepo = json.load(fh) + enum_list = nodes.enumerated_list() + nodelist = [] + nodelist.append(enum_list) + for test in sorted(imagerepo): + link_node = nodes.raw( + "", + f'{test}', + format="html", + ) + li_node = nodes.list_item("") + li_node += link_node + enum_list += li_node + return nodelist + + +def collect_imagehash_pages(app: Sphinx): + """Generate pages for each entry in the imagerepo.json""" + with open(app.config["image_test_json"], "r") as fh: + imagerepo: ImageRepo = json.load(fh) + pages = [] + for test, hashfiles in imagerepo.items(): + hashstrs = [hash_from_url(h) for h in hashfiles] + pages.append( + ( + f"generated/image_test/{test}", + {"test": test, "hashfiles": zip(hashstrs, hashfiles)}, + "imagehash.html", + ) + ) + return pages + + +def setup(app: Sphinx): + app.add_config_value( + "image_test_json", + "../../lib/iris/tests/results/imagerepo.json", + "html", + ) + + app.add_directive("imagetest-list", ImageTestDirective) + app.connect("html-collect-pages", collect_imagehash_pages) + + return { + "version": "0.1", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/src/userguide/cube_maths.rst b/docs/src/userguide/cube_maths.rst index 78490cd749..e8a1744a44 100644 --- a/docs/src/userguide/cube_maths.rst +++ b/docs/src/userguide/cube_maths.rst @@ -63,9 +63,9 @@ but with the data representing their difference: forecast_reference_time 1859-09-01 06:00:00 height 1.5 m Attributes: - Conventions CF-1.5 - Model scenario E1 - source Data from Met Office Unified Model 6.05 + Conventions 'CF-1.5' + Model scenario 'E1' + source 'Data from Met Office Unified Model 6.05' .. note:: diff --git a/docs/src/userguide/cube_statistics.rst b/docs/src/userguide/cube_statistics.rst index ac66ff4e53..980f1e132f 100644 --- a/docs/src/userguide/cube_statistics.rst +++ b/docs/src/userguide/cube_statistics.rst @@ -53,8 +53,8 @@ For instance, suppose we have a cube: forecast_reference_time 2009-11-19 04:00:00 Attributes: STASH m01s00i004 - source Data from Met Office Unified Model - um_version 7.3 + source 'Data from Met Office Unified Model' + um_version '7.3' In this case we have a 4 dimensional cube; @@ -84,8 +84,8 @@ we can pass the coordinate name and the aggregation definition to the mean model_level_number Attributes: STASH m01s00i004 - source Data from Met Office Unified Model - um_version 7.3 + source 'Data from Met Office Unified Model' + um_version '7.3' Similarly other analysis operators such as ``MAX``, ``MIN`` and ``STD_DEV`` @@ -143,8 +143,8 @@ These areas can now be passed to the ``collapsed`` method as weights: mean grid_longitude, grid_latitude Attributes: STASH m01s00i004 - source Data from Met Office Unified Model - um_version 7.3 + source 'Data from Met Office Unified Model' + um_version '7.3' Several examples of area averaging exist in the gallery which may be of interest, including an example on taking a :ref:`global area-weighted mean @@ -229,7 +229,7 @@ Printing this cube now shows that two extra coordinates exist on the cube: Cell methods: mean month, year Attributes: - Conventions CF-1.5 + Conventions 'CF-1.5' STASH m01s00i024 diff --git a/docs/src/userguide/interpolation_and_regridding.rst b/docs/src/userguide/interpolation_and_regridding.rst index 5573c4aa8e..f590485606 100644 --- a/docs/src/userguide/interpolation_and_regridding.rst +++ b/docs/src/userguide/interpolation_and_regridding.rst @@ -79,7 +79,7 @@ Let's take the air temperature cube we've seen previously: mean over years time Attributes: STASH m01s16i203 - source Data from Met Office Unified Model + source 'Data from Met Office Unified Model' We can interpolate specific values from the coordinates of the cube: @@ -98,7 +98,7 @@ We can interpolate specific values from the coordinates of the cube: mean over years time Attributes: STASH m01s16i203 - source Data from Met Office Unified Model + source 'Data from Met Office Unified Model' As we can see, the resulting cube is scalar and has longitude and latitude coordinates with the values defined in our sample points. diff --git a/docs/src/userguide/iris_cubes.rst b/docs/src/userguide/iris_cubes.rst index 64a9bfd822..d13dee369c 100644 --- a/docs/src/userguide/iris_cubes.rst +++ b/docs/src/userguide/iris_cubes.rst @@ -172,8 +172,8 @@ output as this is the quickest way of inspecting the contents of a cube. Here is forecast_reference_time 2009-11-19 04:00:00 Attributes: STASH m01s00i004 - source Data from Met Office Unified Model - um_version 7.3 + source 'Data from Met Office Unified Model' + um_version '7.3' Using this output we can deduce that: diff --git a/docs/src/userguide/loading_iris_cubes.rst b/docs/src/userguide/loading_iris_cubes.rst index a66af12b9c..fb938975e8 100644 --- a/docs/src/userguide/loading_iris_cubes.rst +++ b/docs/src/userguide/loading_iris_cubes.rst @@ -51,15 +51,14 @@ The ``air_potential_temperature`` cubes were 4 dimensional with: .. note:: - The result of :func:`iris.load` is **always** a - :class:`list of cubes `. - Anything that can be done with a Python :class:`list` can be done - with the resultant list of cubes. It is worth noting, however, that - there is no inherent order to this - :class:`list of cubes `. - Because of this, indexing may be inconsistent. A more consistent way to - extract a cube is by using the :class:`iris.Constraint` class as - described in :ref:`constrained-loading`. + The result of :func:`iris.load` is **always** a :class:`iris.cube.CubeList` + (even if it only contains one :class:`iris.cube.Cube` - see + :ref:`strict-loading`). Anything that can be done with a Python + :class:`list` can be done with an :class:`iris.cube.CubeList`. + + The order of this list should not be relied upon. Ways of loading a + specific cube or cubes are covered in :ref:`constrained-loading` and + :ref:`strict-loading`. .. hint:: @@ -101,8 +100,8 @@ list indexing can be used: forecast_reference_time 2009-11-19 04:00:00 Attributes: STASH m01s00i004 - source Data from Met Office Unified Model - um_version 7.3 + source 'Data from Met Office Unified Model' + um_version '7.3' Notice that the result of printing a **cube** is a little more verbose than it was when printing a **list of cubes**. In addition to the very short summary @@ -305,13 +304,21 @@ for ease of calendar-based testing. >>> cube_all = iris.load_cube(filename, 'air_potential_temperature') >>> print('All times :\n' + str(cube_all.coord('time'))) All times : - DimCoord([2009-11-19 10:00:00, 2009-11-19 11:00:00, 2009-11-19 12:00:00], standard_name='time', calendar='gregorian') + DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) + points: [2009-11-19 10:00:00, 2009-11-19 11:00:00, 2009-11-19 12:00:00] + shape: (3,) + dtype: float64 + standard_name: 'time' >>> # Define a function which accepts a datetime as its argument (this is simplified in later examples). >>> hour_11 = iris.Constraint(time=lambda cell: cell.point.hour == 11) >>> cube_11 = cube_all.extract(hour_11) >>> print('Selected times :\n' + str(cube_11.coord('time'))) Selected times : - DimCoord([2009-11-19 11:00:00], standard_name='time', calendar='gregorian') + DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) + points: [2009-11-19 11:00:00] + shape: (1,) + dtype: float64 + standard_name: 'time' Secondly, the :class:`iris.time` module provides flexible time comparison facilities. An :class:`iris.time.PartialDateTime` object can be compared to @@ -336,7 +343,11 @@ The previous constraint example can now be written as: >>> print(iris.load_cube( ... iris.sample_data_path('uk_hires.pp'), ... 'air_potential_temperature' & the_11th_hour).coord('time')) - DimCoord([2009-11-19 11:00:00], standard_name='time', calendar='gregorian') + DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) + points: [2009-11-19 11:00:00] + shape: (1,) + dtype: float64 + standard_name: 'time' It is common that a cube will need to be constrained between two given dates. In the following example we construct a time sequence representing the first @@ -356,10 +367,13 @@ day of every week for many years: :options: +NORMALIZE_WHITESPACE, +ELLIPSIS >>> print(long_ts.coord('time')) - DimCoord([2007-04-09 00:00:00, 2007-04-16 00:00:00, 2007-04-23 00:00:00, - ... - 2010-02-01 00:00:00, 2010-02-08 00:00:00, 2010-02-15 00:00:00], - standard_name='time', calendar='gregorian') + DimCoord : time / (days since 2007-04-09, gregorian calendar) + points: [ + 2007-04-09 00:00:00, 2007-04-16 00:00:00, ..., + 2010-02-08 00:00:00, 2010-02-15 00:00:00] + shape: (150,) + dtype: int64 + standard_name: 'time' Given two dates in datetime format, we can select all points between them. @@ -372,9 +386,13 @@ Given two dates in datetime format, we can select all points between them. ... time=lambda cell: d1 <= cell.point < d2) >>> within_st_swithuns_07 = long_ts.extract(st_swithuns_daterange_07) >>> print(within_st_swithuns_07.coord('time')) - DimCoord([2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, - 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00], - standard_name='time', calendar='gregorian') + DimCoord : time / (days since 2007-04-09, gregorian calendar) + points: [ + 2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, + 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00] + shape: (6,) + dtype: int64 + standard_name: 'time' Alternatively, we may rewrite this using :class:`iris.time.PartialDateTime` objects. @@ -388,9 +406,13 @@ objects. ... time=lambda cell: pdt1 <= cell.point < pdt2) >>> within_st_swithuns_07 = long_ts.extract(st_swithuns_daterange_07) >>> print(within_st_swithuns_07.coord('time')) - DimCoord([2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, - 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00], - standard_name='time', calendar='gregorian') + DimCoord : time / (days since 2007-04-09, gregorian calendar) + points: [ + 2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, + 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00] + shape: (6,) + dtype: int64 + standard_name: 'time' A more complex example might require selecting points over an annually repeating date range. We can select points within a certain part of the year, in this case @@ -403,17 +425,24 @@ PartialDateTime this becomes simple: ... time=lambda cell: PartialDateTime(month=7, day=15) <= cell < PartialDateTime(month=8, day=25)) >>> within_st_swithuns = long_ts.extract(st_swithuns_daterange) ... - >>> print(within_st_swithuns.coord('time')) - DimCoord([2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, - 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00, - 2008-07-21 00:00:00, 2008-07-28 00:00:00, 2008-08-04 00:00:00, - 2008-08-11 00:00:00, 2008-08-18 00:00:00, 2009-07-20 00:00:00, - 2009-07-27 00:00:00, 2009-08-03 00:00:00, 2009-08-10 00:00:00, - 2009-08-17 00:00:00, 2009-08-24 00:00:00], standard_name='time', calendar='gregorian') + >>> # Note: using summary(max_values) to show more of the points + >>> print(within_st_swithuns.coord('time').summary(max_values=100)) + DimCoord : time / (days since 2007-04-09, gregorian calendar) + points: [ + 2007-07-16 00:00:00, 2007-07-23 00:00:00, 2007-07-30 00:00:00, + 2007-08-06 00:00:00, 2007-08-13 00:00:00, 2007-08-20 00:00:00, + 2008-07-21 00:00:00, 2008-07-28 00:00:00, 2008-08-04 00:00:00, + 2008-08-11 00:00:00, 2008-08-18 00:00:00, 2009-07-20 00:00:00, + 2009-07-27 00:00:00, 2009-08-03 00:00:00, 2009-08-10 00:00:00, + 2009-08-17 00:00:00, 2009-08-24 00:00:00] + shape: (17,) + dtype: int64 + standard_name: 'time' Notice how the dates printed are between the range specified in the ``st_swithuns_daterange`` and that they span multiple years. +.. _strict-loading: Strict Loading -------------- diff --git a/docs/src/userguide/navigating_a_cube.rst b/docs/src/userguide/navigating_a_cube.rst index 74b47b258e..c5924a61c6 100644 --- a/docs/src/userguide/navigating_a_cube.rst +++ b/docs/src/userguide/navigating_a_cube.rst @@ -33,9 +33,9 @@ We have already seen a basic string representation of a cube when printing: forecast_reference_time 2006-06-15 00:00:00 time 2006-06-15 00:00:00 Attributes: - Conventions CF-1.5 + Conventions 'CF-1.5' STASH m01s16i222 - source Data from Met Office Unified Model 6.01 + source 'Data from Met Office Unified Model 6.01' This representation is equivalent to passing the cube to the :func:`str` function. This function can be used on @@ -169,9 +169,9 @@ We can add and remove coordinates via :func:`Cube.add_dim_coord`_ package in order to generate +`Matplotlib `_ package in order to generate high quality, production ready 1D and 2D plots. -The functionality of the Matplotlib -`pyplot `_ module has +The functionality of the Matplotlib :py:mod:`~matplotlib.pyplot` module has been extended within Iris to facilitate easy visualisation of a cube's data. @@ -218,7 +217,7 @@ Plotting 2-Dimensional Cubes Creating Maps ------------- Whenever a 2D plot is created using an :class:`iris.coord_systems.CoordSystem`, -a cartopy :class:`~cartopy.mpl.GeoAxes` instance is created, which can be +a cartopy :class:`~cartopy.mpl.geoaxes.GeoAxes` instance is created, which can be accessed with the :func:`matplotlib.pyplot.gca` function. Given the current map, you can draw gridlines and coastlines amongst other @@ -226,8 +225,8 @@ things. .. seealso:: - :meth:`cartopy's gridlines() `, - :meth:`cartopy's coastlines() `. + :meth:`cartopy's gridlines() `, + :meth:`cartopy's coastlines() `. Cube Contour diff --git a/docs/src/userguide/saving_iris_cubes.rst b/docs/src/userguide/saving_iris_cubes.rst index c801a1fbea..2ffc8c47d3 100644 --- a/docs/src/userguide/saving_iris_cubes.rst +++ b/docs/src/userguide/saving_iris_cubes.rst @@ -51,6 +51,7 @@ The :py:func:`iris.save` function passes all other keywords through to the saver >>> # Save a cube list to a PP file, appending to the contents of the file >>> # if it already exists >>> iris.save(cubes, "myfile.pp", append=True) + >>> # Save a cube to netCDF, defaults to NETCDF4 file format >>> iris.save(cubes[0], "myfile.nc") >>> # Save a cube list to netCDF, using the NETCDF3_CLASSIC storage option @@ -73,6 +74,12 @@ See for more details on supported arguments for the individual savers. +.. note:: + + The existence of a keyword argument for one saver does not guarantee the + same works for all savers. For example, it isn't possible to pass an + ``append`` keyword argument to the netCDF saver (see :ref:`netcdf_save`). + Customising the Save Process ---------------------------- @@ -102,6 +109,7 @@ Similarly a PP field may need to be written out with a specific value for LBEXP. yield field iris.fileformats.pp.save_fields(tweaked_fields(cubes[0]), '/tmp/app.pp') +.. _netcdf_save: NetCDF ^^^^^^ diff --git a/docs/src/userguide/subsetting_a_cube.rst b/docs/src/userguide/subsetting_a_cube.rst index 1c68cafb8d..5112d9689a 100644 --- a/docs/src/userguide/subsetting_a_cube.rst +++ b/docs/src/userguide/subsetting_a_cube.rst @@ -30,7 +30,7 @@ A subset of a cube can be "extracted" from a multi-dimensional cube in order to Scalar coordinates: grid_latitude 0.0 degrees Attributes: - Conventions CF-1.5 + Conventions 'CF-1.5' In this example we start with a 3 dimensional cube, with dimensions of ``height``, ``grid_latitude`` and ``grid_longitude``, @@ -97,8 +97,8 @@ same way as loading with constraints: time 2009-11-19 10:00:00 Attributes: STASH m01s00i004 - source Data from Met Office Unified Model - um_version 7.3 + source 'Data from Met Office Unified Model' + um_version '7.3' Cube Iteration diff --git a/docs/src/whatsnew/1.4.rst b/docs/src/whatsnew/1.4.rst index 858f985ec6..989198296c 100644 --- a/docs/src/whatsnew/1.4.rst +++ b/docs/src/whatsnew/1.4.rst @@ -182,8 +182,7 @@ Cubes With no Vertical Coord can now be Exported to GRIB -------------------------------------------------------- Iris can now export cubes with no vertical coord to GRIB. -The solution is still under discussion: See -https://github.com/SciTools/iris/issues/519. +The solution is still under discussion: See :issue:`519`. .. _simple_cfg: diff --git a/docs/src/whatsnew/3.0.rst b/docs/src/whatsnew/3.0.rst index 77458c70e9..771a602954 100644 --- a/docs/src/whatsnew/3.0.rst +++ b/docs/src/whatsnew/3.0.rst @@ -35,8 +35,8 @@ This document explains the changes made to Iris for this release :ref:`incompatible changes ` and :ref:`deprecations `. - And finally, get in touch with us on `GitHub`_ if you have any issues or - feature requests for improving Iris. Enjoy! + And finally, get in touch with us on :issue:`GitHub` if you have + any issues or feature requests for improving Iris. Enjoy! v3.0.1 (27 Jan 2021) @@ -617,7 +617,6 @@ v3.0.4 (22 July 2021) .. _xxHash: https://github.com/Cyan4973/xxHash .. _PyKE: https://pypi.org/project/scitools-pyke/ .. _@owena11: https://github.com/owena11 -.. _GitHub: https://github.com/SciTools/iris/issues/new/choose .. _readthedocs: https://readthedocs.org/ .. _CF Conventions and Metadata: https://cfconventions.org/ .. _flake8: https://flake8.pycqa.org/en/stable/ diff --git a/docs/src/whatsnew/3.1.rst b/docs/src/whatsnew/3.1.rst index 165e20d9bc..bd046a0a24 100644 --- a/docs/src/whatsnew/3.1.rst +++ b/docs/src/whatsnew/3.1.rst @@ -25,8 +25,8 @@ This document explains the changes made to Iris for this release * Multiple improvements to developer guide documentation. See entries in the :ref:`"Documentation" section `, below. - And finally, get in touch with us on `GitHub`_ if you have any issues or - feature requests for improving Iris. Enjoy! + And finally, get in touch with us on :issue:`GitHub` if you have + any issues or feature requests for improving Iris. Enjoy! 📢 Announcements @@ -315,7 +315,6 @@ This document explains the changes made to Iris for this release .. _blacken-docs: https://github.com/asottile/blacken-docs .. _conda-lock: https://github.com/conda-incubator/conda-lock .. _deprecated numpy 1.20 aliases for builtin types: https://numpy.org/doc/1.20/release/1.20.0-notes.html#using-the-aliases-of-builtin-types-like-np-int-is-deprecated -.. _GitHub: https://github.com/SciTools/iris/issues/new/choose .. _Met Office: https://www.metoffice.gov.uk/ .. _numpy: https://numpy.org/doc/stable/release/1.20.0-notes.html .. |pre-commit.ci| image:: https://results.pre-commit.ci/badge/github/SciTools/iris/main.svg diff --git a/docs/src/whatsnew/index.rst b/docs/src/whatsnew/index.rst index fabb056484..f425e649b9 100644 --- a/docs/src/whatsnew/index.rst +++ b/docs/src/whatsnew/index.rst @@ -10,7 +10,7 @@ Iris versions. .. toctree:: :maxdepth: 1 - latest.rst + 3.2.rst 3.1.rst 3.0.rst 2.4.rst diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst deleted file mode 100644 index 2ddcdb9b34..0000000000 --- a/docs/src/whatsnew/latest.rst +++ /dev/null @@ -1,265 +0,0 @@ -.. include:: ../common_links.inc - -|iris_version| |build_date| [unreleased] -**************************************** - -This document explains the changes made to Iris for this release -(:doc:`View all changes `.) - - -.. dropdown:: :opticon:`report` |iris_version| Release Highlights - :container: + shadow - :title: text-primary text-center font-weight-bold - :body: bg-light - :animate: fade-in - :open: - - The highlights for this minor release of Iris include: - - * We've added support for `UGRID`_ meshes which can now be loaded and attached - to a cube. - - And finally, get in touch with us on `GitHub`_ if you have any issues or - feature requests for improving Iris. Enjoy! - - -📢 Announcements -================ - -#. Welcome to `@wjbenfold`_, `@tinyendian`_, `@larsbarring`_, `@akuhnregnier`_, - `@bsherratt`_ and `@aaronspring`_ who made their first contributions to Iris. - The first of many we hope! -#. Congratulations to `@wjbenfold`_ who has become a core developer for Iris! 🎉 - - -✨ Features -=========== - -#. `@bjlittle`_, `@pp-mo`_, `@trexfeathers`_ and `@stephenworsley`_ added - support for unstructured meshes, as described by `UGRID`_. This involved - adding a data model (:pull:`3968`, :pull:`4014`, :pull:`4027`, :pull:`4036`, - :pull:`4053`, :pull:`4439`) and API (:pull:`4063`, :pull:`4064`), and - supporting representation (:pull:`4033`, :pull:`4054`) of data on meshes. - Most of this new API can be found in :mod:`iris.experimental.ugrid`. The key - objects introduced are :class:`iris.experimental.ugrid.mesh.Mesh`, - :class:`iris.experimental.ugrid.mesh.MeshCoord` and - :obj:`iris.experimental.ugrid.load.PARSE_UGRID_ON_LOAD`. - A :class:`iris.experimental.ugrid.mesh.Mesh` contains a full description of a UGRID - type mesh. :class:`~iris.experimental.ugrid.mesh.MeshCoord`\ s are coordinates that - reference and represent a :class:`~iris.experimental.ugrid.mesh.Mesh` for use - on a :class:`~iris.cube.Cube`. :class:`~iris.cube.Cube`\ s are also given the - property :attr:`~iris.cube.Cube.mesh` which returns a - :class:`~iris.experimental.ugrid.mesh.Mesh` if one is attached to the - :class:`~iris.cube.Cube` via a :class:`~iris.experimental.ugrid.mesh.MeshCoord`. - -#. `@trexfeathers`_ added support for loading unstructured mesh data from netcdf data, - for files using the `UGRID`_ conventions. - The context manager :obj:`~iris.experimental.ugrid.load.PARSE_UGRID_ON_LOAD` - provides a way to load UGRID files so that :class:`~iris.cube.Cube`\ s can be - returned with a :class:`~iris.experimental.ugrid.mesh.Mesh` attached. - (:pull:`4058`). - -#. `@pp-mo`_ added support to save cubes with meshes to netcdf files, using the - `UGRID`_ conventions. - The existing :meth:`iris.save` function now does this, when saving cubes with meshes. - A routine :meth:`iris.experimental.ugrid.save.save_mesh` allows saving - :class:`~iris.experimental.ugrid.mesh.Mesh` objects to netcdf *without* any associated data - (i.e. not attached to cubes). - (:pull:`4318` and :pull:`4339`). - -#. `@trexfeathers`_ added :meth:`iris.experimental.ugrid.mesh.Mesh.from_coords` - for inferring a :class:`~iris.experimental.ugrid.mesh.Mesh` from an - appropriate collection of :class:`iris.coords.Coord`\ s. - -#. `@larsbarring`_ updated :func:`~iris.util.equalise_attributes` to return a list of dictionaries - containing the attributes removed from each :class:`~iris.cube.Cube`. (:pull:`4357`) - -#. `@trexfeathers`_ enabled streaming of **all** lazy arrays when saving to - NetCDF files (was previously just :class:`~iris.cube.Cube` - :attr:`~iris.cube.Cube.data`). This is - important given the much greater size of - :class:`~iris.coords.AuxCoord` :attr:`~iris.coords.AuxCoord.points` and - :class:`~iris.experimental.ugrid.mesh.Connectivity` - :attr:`~iris.experimental.ugrid.mesh.Connectivity.indices` under the - `UGRID`_ model. (:pull:`4375`) - -#. `@bsherratt`_ added a `threshold` parameter to - :meth:`~iris.cube.Cube.intersection` (:pull:`4363`) - -#. `@wjbenfold`_ added test data to ci benchmarks so that it is accessible to - benchmark scripts. Also added a regridding benchmark that uses this data - (:pull:`4402`) - - -🐛 Bugs Fixed -============= - -#. `@rcomer`_ fixed :meth:`~iris.cube.Cube.intersection` for special cases where - one cell's bounds align with the requested maximum and negative minimum, fixing - :issue:`4221`. (:pull:`4278`) - -#. `@bsherratt`_ fixed further edge cases in - :meth:`~iris.cube.Cube.intersection`, including :issue:`3698` (:pull:`4363`) - -#. `@tinyendian`_ fixed the error message produced by :meth:`~iris.cube.CubeList.concatenate_cube` - when a cube list contains cubes with different names, which will no longer report - "Cube names differ: var1 != var1" if var1 appears multiple times in the list - (:issue:`4342`, :pull:`4345`) - -#. `@larsbarring`_ fixed :class:`~iris.coord_systems.GeoCS` to handle spherical ellipsoid - parameter inverse_flattening = 0 (:issue: `4146`, :pull:`4348`) - -#. `@pdearnshaw`_ fixed an error in the call to :class:`cftime.datetime` in - :mod:`~iris.fileformats.pp_save_rules` that prevented the saving to PP of climate - means for DJF (:pull:`4391`) - -#. `@wjbenfold`_ improved the error message for failure of :meth:`~iris.cube.CubeList.concatenate` - to indicate that the value of a scalar coordinate may be mismatched, rather than the metadata - (:issue:`4096`, :pull:`4387`) - -#. `@bsherratt`_ fixed a regression to the NAME file loader introduced in 3.0.4, - as well as some long-standing bugs with vertical coordinates and number - formats. (:pull:`4411`) - -#. `@rcomer`_ fixed :meth:`~iris.cube.Cube.subset` to alway return ``None`` if - no value match is found. (:pull:`4417`) - -#. `@wjbenfold`_ resolved an issue that previously caused regridding with lazy - data to take significantly longer than with real data. Relevant benchmark - shows a time decrease from >10s to 625ms. (:issue:`4280`, :pull:`4400`) - -#. `@wjbenfold`_ changed :meth:`iris.util.points_step` to stop it from warning - when applied to a single point (:issue:`4250`, :pull:`4367`) - -#. `@trexfeathers`_ changed :class:`~iris.coords._DimensionalMetadata` and - :class:`~iris.experimental.ugrid.Connectivity` equality methods to preserve - array laziness, allowing efficient comparisons even with larger-than-memory - objects. (:pull:`4439`) - - -💣 Incompatible Changes -======================= - -#. N/A - - -🚀 Performance Enhancements -=========================== - -#. N/A - - -🔥 Deprecations -=============== - -#. N/A - - -🔗 Dependencies -=============== - -#. `@bjlittle`_ introduced the ``cartopy >=0.20`` minimum pin. - (:pull:`4331`) - -#. `@trexfeathers`_ introduced the ``cf-units >=3`` and ``nc-time-axis >=1.3`` - minimum pins. (:pull:`4356`) - -#. `@bjlittle`_ introduced the ``numpy >=1.19`` minimum pin, in - accordance with `NEP-29`_ deprecation policy. (:pull:`4386`) - - -📚 Documentation -================ - -#. `@rcomer`_ updated the "Plotting Wind Direction Using Quiver" Gallery - example. (:pull:`4120`) - -#. `@trexfeathers`_ included `Iris GitHub Discussions`_ in - :ref:`get involved `. (:pull:`4307`) - -#. `@wjbenfold`_ improved readability in :ref:`userguide interpolation - section `. (:pull:`4314`) - -#. `@wjbenfold`_ added explanation about the absence of | operator for - :class:`iris.Constraint` to :ref:`userguide loading section - ` and to api reference documentation. (:pull:`4321`) - -#. `@trexfeathers`_ added more detail on making `iris-test-data`_ available - during :ref:`developer_running_tests`. (:pull:`4359`) - -#. `@lbdreyer`_ added a section to the release documentation outlining the role - of the :ref:`release_manager`. (:pull:`4413`) - -#. `@trexfeathers`_ encouraged contributors to include type hinting in code - they are working on - :ref:`code_formatting`. (:pull:`4390`) - - -💼 Internal -=========== - -#. `@trexfeathers`_ set the linkcheck to ignore - http://www.nationalarchives.gov.uk/doc/open-government-licence since this - always works locally, but never within CI. (:pull:`4307`) - -#. `@wjbenfold`_ netCDF integration tests now skip ``TestConstrainedLoad`` if - test data is missing (:pull:`4319`) - -#. `@wjbenfold`_ excluded ``Good First Issue`` labelled issues from being - marked stale. (:pull:`4317`) - -#. `@tkknight`_ added additional make targets for reducing the time of the - documentation build including ``html-noapi`` and ``html-quick``. - Useful for development purposes only. For more information see - :ref:`contributing.documentation.building` the documentation. (:pull:`4333`) - -#. `@rcomer`_ modified the ``animation`` test to prevent it throwing a warning - that sometimes interferes with unrelated tests. (:pull:`4330`) - -#. `@rcomer`_ removed a now redundant workaround in :func:`~iris.plot.contourf`. - (:pull:`4349`) - -#. `@trexfeathers`_ refactored :mod:`iris.experimental.ugrid` into sub-modules. - (:pull:`4347`). - -#. `@bjlittle`_ enabled the `sort-all`_ `pre-commit`_ hook to automatically - sort ``__all__`` entries into alphabetical order. (:pull:`4353`) - -#. `@rcomer`_ modified a NetCDF saver test to prevent it triggering a numpy - deprecation warning. (:issue:`4374`, :pull:`4376`) - -#. `@akuhnregnier`_ removed addition of period from - :func:`~iris.analysis.cartography.wrap_lons` and updated affected tests - using assertArrayAllClose following :issue:`3993`. - (:pull:`4421`) - -#. `@rcomer`_ updated some tests to work with Matplotlib v3.5. (:pull:`4428`) - -#. `@rcomer`_ applied minor fixes to some regridding tests. (:pull:`4432`) - -#. `@lbdreyer`_ corrected the license PyPI classifier. (:pull:`4435`) - -#. `@aaronspring `_ exchanged `dask` with - `dask-core` in testing environments reducing the number of dependencies - installed for testing. (:pull:`4434`) - -#. `@wjbenfold`_ prevented github action runs in forks (:issue:`4441`, - :pull:`4444`) - -.. comment - Whatsnew author names (@github name) in alphabetical order. Note that, - core dev names are automatically included by the common_links.inc: - -.. _@aaronspring: https://github.com/aaronspring -.. _@akuhnregnier: https://github.com/akuhnregnier -.. _@bsherratt: https://github.com/bsherratt -.. _@larsbarring: https://github.com/larsbarring -.. _@pdearnshaw: https://github.com/pdearnshaw -.. _@tinyendian: https://github.com/tinyendian - -.. comment - Whatsnew resources in alphabetical order: - -.. _GitHub: https://github.com/SciTools/iris/issues/new/choose -.. _NEP-29: https://numpy.org/neps/nep-0029-deprecation_policy.html -.. _UGRID: http://ugrid-conventions.github.io/ugrid-conventions/ -.. _sort-all: https://github.com/aio-libs/sort-all diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst new file mode 120000 index 0000000000..2bdbea5d85 --- /dev/null +++ b/docs/src/whatsnew/latest.rst @@ -0,0 +1 @@ +3.2.rst \ No newline at end of file diff --git a/docs/src/whatsnew/latest.rst.template b/docs/src/whatsnew/latest.rst.template deleted file mode 100644 index ced0778069..0000000000 --- a/docs/src/whatsnew/latest.rst.template +++ /dev/null @@ -1,112 +0,0 @@ -.. include:: ../common_links.inc - -|iris_version| |build_date| [unreleased] -**************************************** - -This document explains the changes made to Iris for this release -(:doc:`View all changes `.) - - -.. dropdown:: :opticon:`report` |iris_version| Release Highlights - :container: + shadow - :title: text-primary text-center font-weight-bold - :body: bg-light - :animate: fade-in - :open: - - The highlights for this major/minor release of Iris include: - - * N/A - - And finally, get in touch with us on `GitHub`_ if you have any issues or - feature requests for improving Iris. Enjoy! - - -NOTE: section below is a template for bugfix patches -==================================================== - (Please remove this section when creating an initial 'latest.rst') - -v3.X.X (DD MMM YYYY) -==================== - -.. dropdown:: :opticon:`alert` v3.X.X Patches - :container: + shadow - :title: text-primary text-center font-weight-bold - :body: bg-light - :animate: fade-in - - The patches in this release of Iris include: - - #. N/A - -NOTE: section above is a template for bugfix patches -==================================================== - (Please remove this section when creating an initial 'latest.rst') - - - -📢 Announcements -================ - -#. N/A - - -✨ Features -=========== - -#. N/A - - -🐛 Bugs Fixed -============= - -#. N/A - - -💣 Incompatible Changes -======================= - -#. N/A - - -🚀 Performance Enhancements -=========================== - -#. N/A - - -🔥 Deprecations -=============== - -#. N/A - - -🔗 Dependencies -=============== - -#. N/A - - -📚 Documentation -================ - -#. N/A - - -💼 Internal -=========== - -#. N/A - - -.. comment - Whatsnew author names (@github name) in alphabetical order. Note that, - core dev names are automatically included by the common_links.inc: - - - - -.. comment - Whatsnew resources in alphabetical order: - -.. _GitHub: https://github.com/SciTools/iris/issues/new/choose diff --git a/etc/cf-standard-name-table.xml b/etc/cf-standard-name-table.xml index 5a19f8d5b1..bd76168192 100644 --- a/etc/cf-standard-name-table.xml +++ b/etc/cf-standard-name-table.xml @@ -1,7 +1,7 @@ - 77 - 2021-01-19T13:38:50Z + 78 + 2021-09-21T11:55:06Z Centre for Environmental Data Analysis support@ceda.ac.uk @@ -489,6 +489,13 @@ + + m2 s-2 + + + One-half the scalar product of the air velocity and vorticity vectors, where vorticity refers to the standard name atmosphere_upward_absolute_vorticity. Helicity is proportional to the strength of the flow, the amount of vertical wind shear, and the amount of turning in the flow. + + m2 s-1 35 @@ -2467,7 +2474,7 @@ 1 - The "beam_consistency_indicator" is the degree to which the magnitudes of a collection (ensemble) of acoustic signals from multiple underwater acoustic transceivers relate to each other. It is used as a data quality assessment parameter in ADCP (acoustic doppler current profiler) instruments and is frequently referred to as "correlation magnitude". Convention is that the larger the value, the higher the signal to noise ratio and therefore the better the quality of the current vector measurements; the maximum value of the indicator is 128. + The "beam_consistency_indicator" is the degree to which the received acoustic pulse is correlated with the transmitted pulse. It is used as a data quality assessment parameter in ADCP (acoustic doppler current profiler) instruments and is frequently referred to as "correlation magnitude". Convention is that the larger the value, the higher the signal to noise ratio and therefore the better the quality of the current vector measurements; the maximum value of the indicator is 128. @@ -2491,11 +2498,11 @@ The specification of a physical process by the phrase due_to_process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. Altitude is the (geometric) height above the geoid, which is the reference geopotential surface. The geoid is similar to mean sea level. "Bedrock" is the solid Earth surface beneath land ice, ocean water or soil. The zero of bedrock altitude change is arbitrary. Isostatic adjustment is the vertical movement of the lithosphere due to changing surface ice and water loads. - + - "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. The quantity with standard name biological_taxon_identifier is the machine-readable identifier for the taxon registration in either WoRMS (the AphiaID) or ITIS (the taxonomic serial number or TSN), including namespace. The namespace strings are 'aphia:' or 'tsn:'. For example, Calanus finmarchicus is encoded as either 'aphia:104464' or 'tsn:85272'. For the marine domain WoRMS has more complete coverage and so aphia Ids are preferred. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. The quantity with standard name biological_taxon_lsid is the machine-readable identifier based on a taxon registration system using the syntax convention specified for the Life Science Identifier (LSID) - urn:lsid:<Authority>:<Namespace>:<ObjectID>[:<Version>]. This includes the reference classification in the element and these are restricted by the LSID governance. It is strongly recommended in CF that the authority chosen is World Register of Marine Species (WoRMS) for oceanographic data and Integrated Taxonomic Information System (ITIS) for freshwater and terrestrial data. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. This identifier is a narrower equivalent to the scientificNameID field in the Darwin Core Standard. @@ -2687,6 +2694,13 @@ "Amount" means mass per unit area. Zero change in land ice amount is an arbitrary level. "Land ice" means glaciers, ice-caps and ice-sheets resting on bedrock and also includes ice-shelves. + + kg + + + Zero change in land ice mass is an arbitrary level. "Land ice" means glaciers, ice-caps and ice-sheets resting on bedrock and also includes ice-shelves. The horizontal domain over which the quantity is calculated is described by the associated coordinate variables and coordinate bounds or by a coordinate variable or scalar coordinate variable with the standard name of "region" supplied according to section 6.1.1 of the CF conventions. + + kg m-2 @@ -2922,7 +2936,7 @@ m-3 - "Colony forming unit" means an estimate of the viable bacterial or fungal numbers determined by counting colonies grown from a sample. "Number concentration" means the number of particles or other specified objects per unit volume. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Colony forming unit" means an estimate of the viable bacterial or fungal numbers determined by counting colonies grown from a sample. "Number concentration" means the number of particles or other specified objects per unit volume. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. @@ -3079,6 +3093,13 @@ Depth is the vertical distance below the surface. + + m + + + The phrase depth_at_base_of_unfrozen_ground is the instantaneous depth of the downward penetration of thaw from the ground surface at a given time. Permafrost is soil or rock that has remained at a temperature at or below zero degrees Celsius throughout the seasonal cycle for two or more consecutive years. The maximum measurable depth_at_base_of_unfrozen_ground value as recorded at the end of a thawing season corresponds to the permafrost_active_layer_thickness. + + m @@ -3142,6 +3163,13 @@ "Aerosol" means the system of suspended liquid or solid particles in air (except cloud droplets) and their carrier gas, the air itself. "Ambient_aerosol" means that the aerosol is measured or modelled at the ambient state of pressure, temperature and relative humidity that exists in its immediate environment. "Ambient aerosol particles" are aerosol particles that have taken up ambient water through hygroscopic growth. The extent of hygroscopic growth depends on the relative humidity and the composition of the particles. To specify the relative humidity and temperature at which the quantity described by the standard name applies, provide scalar coordinate variables with standard names of "relative_humidity" and "air_temperature". + + K + + + Sea surface temperature is usually abbreviated as "SST". It is the temperature of sea water near the surface (including the part under sea-ice, if any), not the skin or interface temperature, whose standard names are sea_surface_skin_temperature and surface_temperature, respectively. For the temperature of sea water at a particular depth or layer, a data variable of "sea_water_temperature" with a vertical coordinate axis should be used. Air temperature is the bulk temperature of the air, not the surface (skin) temperature. + + Pa @@ -3723,6 +3751,13 @@ A velocity is a vector quantity. "Eastward" indicates a vector component which is positive when directed eastward (negative westward). Flood water is water that covers land which is normally not covered by water. + + m s-1 + + + A velocity is a vector quantity. "Eastward" indicates a vector component which is positive when directed eastward (negative westward). Friction velocity is a reference wind velocity derived from the relationship between air density and downward stress and is usually applied at a level close to the surface where stress is assumed to independent of height and approximately proportional to the square of mean velocity. + + m s-1 @@ -4577,6 +4612,13 @@ "Area fraction" is the fraction of a grid cell's horizontal area that has some characteristic of interest. It is evaluated as the area of interest divided by the grid cell area. It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. "Layer" means any layer with upper and lower boundaries that have constant values in some vertical coordinate. There must be a vertical coordinate variable indicating the extent of the layer(s). If the layers are model layers, the vertical coordinate can be "model_level_number", but it is recommended to specify a physical coordinate (in a scalar or auxiliary coordinate variable) as well. Standard names also exist for high, medium and low cloud types. Standard names referring only to "cloud_area_fraction" should be used for quantities for the whole atmosphere column. Cloud area fraction is also called "cloud amount" and "cloud cover". + + 1 + + + ice_volume_in_frozen_ground_in_excess_of_pore_volume_in_unfrozen_ground_expressed_as_fraction_of_frozen_ground_volume represents the fractional amount of "excess ice" in frozen ground. Excess ice is the volume of ice in the ground which exceeds the total pore volume that the ground would have under natural unfrozen conditions. Due to the presence of ground ice, the total water content of a frozen soil may exceed that corresponding to its normally consolidated state when unfrozen. As a result, upon thawing, a soil containing excess ice will settle under its own weight until it attains its consolidated state. Reference: van Everdingen, R. O. editor 1998: Multi-language glossary of permafrost and related ground ice terms. International Permafrost Association. + + m3 s-1 @@ -4588,7 +4630,7 @@ m s-1 - Sea water velocity is a vector quantity that is the speed at which water travels in a specified direction. The "indicative error" is an estimate of the quality of a sea water velocity profile measured using an ADCP (acoustic doppler current profiler). It is determined by differencing duplicate error velocity measurements made using different pairs of beams. The parameter is frequently referred to as the "error velocity". + Sea water velocity is a vector quantity that is the speed at which water travels in a specified direction. The "indicative error" is an estimate of the quality of a sea water velocity profile measured using an ADCP (acoustic doppler current profiler). It is determined by the difference between the vertical velocity calculated from two 3-beam solutions. The parameter is frequently referred to as the "error velocity". @@ -7671,6 +7713,13 @@ "Content" indicates a quantity per unit area. + + J Kg-1 + + + The lightning_potential_index measures the potential for charge generation and separation that leads to lightning flashes in convective thunderstorms. It is derived from the model simulated grid-scale updraft velocity and the mass mixing-ratios of liquid water, cloud ice, snow, and graupel. + + J @@ -8081,21 +8130,21 @@ kg m-3 - "Mass concentration" means mass per unit volume and is used in the construction "mass_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. Mass concentration of biota expressed as carbon is also referred to as "carbon biomass". "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Mass concentration" means mass per unit volume and is used in the construction "mass_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. Mass concentration of biota expressed as carbon is also referred to as "carbon biomass". "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. kg m-3 - "Mass concentration" means mass per unit volume and is used in the construction "mass_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. Chlorophylls are the green pigments found in most plants, algae and cyanobacteria; their presence is essential for photosynthesis to take place. There are several different forms of chlorophyll that occur naturally. All contain a chlorin ring (chemical formula C20H16N4) which gives the green pigment and a side chain whose structure varies. The naturally occurring forms of chlorophyll contain between 35 and 55 carbon atoms. + "Mass concentration" means mass per unit volume and is used in the construction "mass_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. Chlorophylls are the green pigments found in most plants, algae and cyanobacteria; their presence is essential for photosynthesis to take place. There are several different forms of chlorophyll that occur naturally. All contain a chlorin ring (chemical formula C20H16N4) which gives the green pigment and a side chain whose structure varies. The naturally occurring forms of chlorophyll contain between 35 and 55 carbon atoms. kg m-3 - "Mass concentration" means mass per unit volume and is used in the construction "mass_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. Mass concentration of biota expressed as nitrogen is also referred to as "nitrogen biomass". "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Mass concentration" means mass per unit volume and is used in the construction "mass_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. Mass concentration of biota expressed as nitrogen is also referred to as "nitrogen biomass". "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. @@ -9449,6 +9498,13 @@ Mass fraction is used in the construction mass_fraction_of_X_in_Y, where X is a material constituent of Y. It means the ratio of the mass of X to the mass of Y (including X). A chemical species denoted by X may be described by a single term such as 'nitrogen' or a phrase such as 'nox_expressed_as_nitrogen'. Chlorophylls are the green pigments found in most plants, algae and cyanobacteria; their presence is essential for photosynthesis to take place. There are several different forms of chlorophyll that occur naturally; all contain a chlorin ring which gives the green pigment and a side chain whose structure varies. Chlorophyll-a is the most commonly occurring form of natural chlorophyll. + + 1 + + + "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y", where X is a material constituent of Y. It is evaluated as the mass of X divided by the mass of Y (including X). It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. Grain-size class distribution is based on the Udden-Wentworth scale. + + 1 @@ -9610,6 +9666,13 @@ Mass fraction is used in the construction mass_fraction_of_X_in_Y, where X is a material constituent of Y. It means the ratio of the mass of X to the mass of Y (including X). Graupel consists of heavily rimed snow particles, often called snow pellets; often indistinguishable from very small soft hail except when the size convention that hail must have a diameter greater than 5 mm is adopted. Reference: American Meteorological Society Glossary http://glossary.ametsoc.org/wiki/Graupel. There are also separate standard names for hail. Standard names for "graupel_and_hail" should be used to describe data produced by models that do not distinguish between hail and graupel. + + 1 + + + "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y'', where X is a material constituent of Y. It is evaluated as the mass of X divided by the mass of Y (including X). It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. Grain-size class distribution is based on the Udden-Wentworth scale. + + 1 @@ -9918,6 +9981,13 @@ "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y", where X is a material constituent of Y. It means the ratio of the mass of X to the mass of Y (including X). A chemical or biological species denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction A_expressed_as_B, where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Noy" describes a family of chemical species. The family usually includes atomic nitrogen (N), nitrogen monoxide (NO), nitrogen dioxide (NO2), dinitrogen pentoxide (N2O5), nitric acid (HNO3), peroxynitric acid (HNO4), bromine nitrate (BrONO2) , chlorine nitrate (ClONO2) and organic nitrates (most notably peroxyacetyl nitrate, sometimes referred to as PAN, (CH3COO2NO2)). The list of individual species that are included in a quantity having a group chemical standard name can vary between models. Where possible, the data variable should be accompanied by a complete description of the species represented, for example, by using a comment attribute. + + 1 + + + "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y", where X is a material constituent of Y. It is evaluated as the mass of X divided by the mass of Y (including X). It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. + + 1 @@ -10191,6 +10261,13 @@ The quantity with standard name mass_fraction_of_rainfall_falling_onto_surface_snow is the mass of rainfall falling onto snow as a fraction of the mass of rainfall falling within the area of interest. Surface snow refers to the snow on the solid ground or on surface ice cover, but excludes, for example, falling snowflakes and snow on plants. The surface called "surface" means the lower boundary of the atmosphere. Unless indicated in the cell_methods attribute, a quantity is assumed to apply to the whole area of each horizontal grid box. + + 1 + + + "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y'', where X is a material constituent of Y. It is evaluated as the mass of X divided by the mass of Y (including X). It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. Grain-size class distribution is based on the Udden-Wentworth scale. + + 1 @@ -10219,6 +10296,13 @@ "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y", where X is a material constituent of Y. It means the ratio of the mass of X to the mass of Y (including X). A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". Shallow convective cloud is nonprecipitating cumulus cloud with a cloud top below 3000m above the surface produced by the convection schemes in an atmosphere model. Some atmosphere models differentiate between shallow and deep convection. "Cloud liquid water" refers to the liquid phase of cloud water. A diameter of 0.2 mm has been suggested as an upper limit to the size of drops that shall be regarded as cloud drops; larger drops fall rapidly enough so that only very strong updrafts can sustain them. Any such division is somewhat arbitrary, and active cumulus clouds sometimes contain cloud drops much larger than this. Reference: AMS Glossary http://glossary.ametsoc.org/wiki/Cloud_drop. + + 1 + + + "Mass fraction" is used in the construction "mass_fraction_of_X_in_Y'', where X is a material constituent of Y. It is evaluated as the mass of X divided by the mass of Y (including X). It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. Grain-size class distribution is based on the Udden-Wentworth scale. + + 1 @@ -10755,14 +10839,14 @@ mol m-3 - "Mole concentration" means number of moles per unit volume, also called "molarity", and is used in the construction "mole_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Mole concentration" means number of moles per unit volume, also called "molarity", and is used in the construction "mole_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. mol m-3 - "Mole concentration" means number of moles per unit volume, also called "molarity", and is used in the construction "mole_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Mole concentration" means number of moles per unit volume, also called "molarity", and is used in the construction "mole_concentration_of_X_in_Y", where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The phrase "expressed_as" is used in the construction "A_expressed_as_B", where B is a chemical constituent of A. It means that the quantity indicated by the standard name is calculated solely with respect to the B contained in A, neglecting all other chemical constituents of A. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. @@ -12648,6 +12732,13 @@ The construction "moles_of_X_per_unit_mass_in_Y" is also called "molality" of X in Y, where X is a material constituent of Y. A chemical or biological species denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". The chemical formula of CFC11 is CFCl3. The IUPAC name for CFC11 is trichloro(fluoro)methane. + + mol kg-1 + + + The construction "moles_of_X_per_unit_mass_in_Y" is also called "molality" of X in Y, where X is a material constituent of Y. A chemical species or biological group denoted by X may be described by a single term such as "nitrogen" or a phrase such as "nox_expressed_as_nitrogen". "Dissolved inorganic carbon" describes a family of chemical species in solution, including carbon dioxide, carbonic acid and the carbonate and bicarbonate anions. "Dissolved inorganic carbon" is the term used in standard names for all species belonging to the family that are represented within a given model. The list of individual species that are included in a quantity having a group chemical standard name can vary between models. Where possible, the data variable should be accompanied by a complete description of the species represented, for example, by using a comment attribute. + + mol kg-1 @@ -13054,6 +13145,13 @@ A velocity is a vector quantity. "Northward" indicates a vector component which is positive when directed northward (negative southward). Flood water is water that covers land which is normally not covered by water. + + m s-1 + + + A velocity is a vector quantity. "Northward" indicates a vector component which is positive when directed northward (negative southward). Friction velocity is a reference wind velocity derived from the relationship between air density and downward stress and is usually applied at a level close to the surface where stress is assumed to independent of height and approximately proportional to the square of mean velocity. + + W m-2 @@ -13345,7 +13443,7 @@ m-3 - "Number concentration" means the number of particles or other specified objects per unit volume. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_identifier to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. + "Number concentration" means the number of particles or other specified objects per unit volume. "Biological taxon" is a name or other label identifying an organism or a group of organisms as belonging to a unit of classification in a hierarchical taxonomy. There must be an auxiliary coordinate variable with standard name biological_taxon_name to identify the taxon in human readable format and optionally an auxiliary coordinate variable with standard name biological_taxon_lsid to provide a machine-readable identifier. See Section 6.1.2 of the CF convention (version 1.8 or later) for information about biological taxon auxiliary coordinate variables. @@ -13436,21 +13534,21 @@ 1 - Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". + Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". 1 - Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". + Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". 1 - The construction lwe_thickness_of_X_amount or _content means the vertical extent of a layer of liquid water having the same mass per unit area. "Precipitation" in the earth's atmosphere means precipitation of water in all phases. The abbreviation "lwe" means liquid water equivalent. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". + The construction lwe_thickness_of_X_amount or _content means the vertical extent of a layer of liquid water having the same mass per unit area. "Precipitation" in the earth's atmosphere means precipitation of water in all phases. The abbreviation "lwe" means liquid water equivalent. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". @@ -13464,7 +13562,7 @@ 1 - Wind is defined as a two-dimensional (horizontal) air velocity vector, with no vertical component. (Vertical motion in the atmosphere has the standard name upward_air_velocity.) The wind speed is the magnitude of the wind velocity. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". + Wind is defined as a two-dimensional (horizontal) air velocity vector, with no vertical component. (Vertical motion in the atmosphere has the standard name upward_air_velocity.) The wind speed is the magnitude of the wind velocity. A variable whose standard name has the form number_of_days_with_X_below|above_threshold is a count of the number of days on which the condition X_below|above_threshold is satisfied. It must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_methods entry for within days which describes the processing of quantity X before the threshold is applied. A number_of_days is an extensive quantity in time, and the cell_methods entry for over days should be "sum". @@ -17359,6 +17457,13 @@ The "reaction rate" is the rate at which the reactants of a chemical reaction form the products. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. The rate of "hydroxyl radical destruction due to reaction with nmvoc" is the nmvoc reactivity with regard to reactions with OH. It is the weighted sum of the reactivity of all individual nmvoc species with OH. The chemical formula for the hydroxyl radical is OH. In chemistry, a "radical" is a highly reactive, and therefore short lived, species. The abbreviation "nmvoc" means non methane volatile organic compounds; "nmvoc" is the term used in standard names to describe the group of chemical species having this classification that are represented within a given model. The list of individual species that are included in a quantity having a group chemical standard name can vary between models. Where possible, the data variable should be accompanied by a complete description of the species represented, for example, by using a comment attribute. + + 1 + + + The phrase "ratio_of_X_to_Y" means X/Y. "X_volume" means the volume occupied by X within the grid cell. Pore volume is the volume of the porosity of the ground under natural, unfrozen conditions. This is often known as "ice saturation index". + + K s-1 @@ -17391,7 +17496,7 @@ 1 - Realization is used to label a dimension that can be thought of asa statistical sample, e.g., labelling members of a model ensemble. + Realization is used to label a dimension that can be thought of as a statistical sample, e.g., labelling members of a model ensemble. @@ -17597,6 +17702,13 @@ The sea_floor_depth_below_sea_surface is the vertical distance between the sea surface and the seabed as measured at a given point in space including the variance caused by tides and possibly waves. + + m + + + The average size of grains (also known as particles) in a sediment sample. + + 1 @@ -17681,6 +17793,13 @@ The term sea_ice_extent means the total area of all grid cells in which the sea ice area fraction equals or exceeds a threshold, often chosen to be 15 per cent. The threshold must be specified by supplying a coordinate variable or scalar coordinate variable with the standard name of sea_ice_area_fraction. The horizontal domain over which sea ice extent is calculated is described by the associated coordinate variables and coordinate bounds or by a coordinate variable or scalar coordinate variable with the standard name of "region" supplied according to section 6.1.1 of the CF conventions. "Sea ice" means all ice floating in the sea which has formed from freezing sea water, rather than by other processes such as calving of land ice to form icebergs. + + m + + + "Sea ice" means all ice floating in the sea which has formed from freezing sea water, rather than by other processes such as calving of land ice to form icebergs. An ice floe is a flat expanse of sea ice, generally taken to be less than 10 km across. ice_floe_diameter corresponds to the diameter of a circle with the same area as the ice floe. + + m @@ -17856,6 +17975,20 @@ Sea surface density is the density of sea water near the surface (including the part under sea-ice, if any). + + Pa + + + The surface called "sea surface" means the upper boundary of the ocean. "Surface stress" means the shear stress (force per unit area) exerted at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, surface stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Eastward" indicates a vector component which is positive when directed northward (negative southward). "Downward eastward" indicates the ZX component of a tensor. A downward eastward stress is a downward flux of eastward momentum, which accelerates the lower medium eastward and the upper medium westward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. The phrase "dissipation_of_sea_surface_waves" means the stress associated with sea surface waves dissipation processes such as whitecapping. + + + + Pa + + + The surface called "sea surface" means the upper boundary of the ocean. "Surface stress" means the shear stress (force per unit area) exerted at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, surface stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Northward" indicates a vector component which is positive when directed northward (negative southward). "Downward northward" indicates the ZY component of a tensor. A downward northward stress is a downward flux of northward momentum, which accelerates the lower medium northward and the upper medium southward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. The phrase "dissipation_of_sea_surface_waves" means the stress associated with sea surface waves dissipation processes such as whitecapping. + + K @@ -18367,6 +18500,13 @@ Wave slope describes an aspect of sea surface wave geometry related to sea surface roughness. Mean square slope describes a derivation over multiple waves within a sea-state, for example calculated from moments of the wave directional spectrum. The phrase "y_slope" indicates that slope values are derived from vector components along the grid y-axis. + + m-1 + + + The wave directional spectrum can be written as a five dimensional function S(t,x,y,k,theta) where t is time, x and y are horizontal coordinates (such as longitude and latitude), k is wavenumber and theta is direction. S has the standard name sea_surface_wave_directional_variance_spectral_density. S can be integrated over direction to give S1= integral(S dtheta) and this quantity has the standard name sea_surface_wave_variance_spectral_density. Wavenumber is the number of oscillations of a wave per unit distance. Wavenumber moments, M(n) of S1 can then be calculated as follows: M(n) = integral(S1 k^n dk), where k^n is k to the power of n. The mean wavenumber, k(1), is calculated as the ratio M(1)/M(0). + + s @@ -18451,6 +18591,27 @@ Sea surface wave variance spectral density is the variance of wave amplitude within a range of wave frequency. + + Pa + + + "Sea surface wave radiation stress" describes the excess momentum flux caused by sea surface waves. Radiation stresses behave as a second-order tensor. "xx" indicates the component of the tensor along the grid x_ axis. + + + + Pa + + + "Sea surface wave radiation stress" describes the excess momentum flux caused by sea surface waves. Radiation stresses behave as a second-order tensor. "xy" indicates the lateral contributions to x_ and y_ components of the tensor. + + + + Pa + + + "Sea surface wave radiation stress" describes the excess momentum flux caused by sea surface waves. Radiation stresses behave as a second-order tensor. "yy" indicates the component of the tensor along the grid y_ axis. + + degree @@ -19088,6 +19249,13 @@ "Content" indicates a quantity per unit area. The "soil content" of a quantity refers to the vertical integral from the surface down to the bottom of the soil model. For the content between specified levels in the soil, standard names including content_of_soil_layer are used. Soil carbon is returned to the atmosphere as the organic matter decays. The decay process takes varying amounts of time depending on the composition of the organic matter, the temperature and the availability of moisture. A carbon "soil pool" means the carbon contained in organic matter which has a characteristic period over which it decays and releases carbon into the atmosphere. "Slow soil pool" refers to the decay of organic matter in soil with a characteristic period of more than a hundred years under reference climate conditions of a temperature of 20 degrees Celsius and no water limitations. + + 1 + + + "Area fraction" is the fraction of a grid cell's horizontal area that has some characteristic of interest. It is evaluated as the area of interest divided by the grid cell area. It may be expressed as a fraction, a percentage, or any other dimensionless representation of a fraction. Snow "viewable from above" refers to the snow on objects or the ground as viewed from above, which excludes, for example, falling snow flakes and snow obscured by a canopy, vegetative cover, or other features resting on the surface. + + kg m-2 @@ -19225,7 +19393,7 @@ K 85 - Soil temperature is the bulk temperature of the soil, not the surface (skin) temperature. "Soil" means the near-surface layer where plants sink their roots. For subsurface temperatures that extend beneath the soil layer or in areas where there is no surface soil layer, the standard name solid_earth_subsurface_temperature should be used. + Soil temperature is the bulk temperature of the soil, not the surface (skin) temperature. "Soil" means the near-surface layer where plants sink their roots. For subsurface temperatures that extend beneath the soil layer or in areas where there is no surface soil layer, the standard name temperature_in_ground should be used. @@ -19249,6 +19417,13 @@ A variable with the standard name of soil_type contains strings which indicate the character of the soil e.g. clay. These strings have not yet been standardised. Alternatively, the data variable may contain integers which can be translated to strings using flag_values and flag_meanings attributes. + + 1 + + + soil_water_ph is the measure of acidity of soil moisture, defined as the negative logarithm of the concentration of dissolved hydrogen ions in soil water. + + degree @@ -19284,13 +19459,6 @@ Solar zenith angle is the the angle between the line of sight to the sun and the local vertical. - - K - - - The quantity with standard name solid_earth_subsurface_temperature is the temperature at any depth (or in a layer) of the "solid" earth, excluding surficial snow and ice (but not permafrost or soil). For temperatures in surface lying snow and ice, the more specific standard names temperature_in_surface_snow and land_ice_temperature should be used. For temperatures measured or modelled specifically in the soil layer (the near-surface layer where plants sink their roots) the standard name soil_temperature should be used. - - kg m-2 s-1 @@ -19410,6 +19578,13 @@ "specific" means per unit mass. Potential energy is the sum of the gravitational potential energy relative to the geoid and the centripetal potential energy. (The geopotential is the specific potential energy.) + + J kg-1 K-1 + + + Thermal capacity, or heat capacity, is the amount of heat energy required to increase the temperature of 1 kg of material by 1 K. It is a property of the material. + + J kg-1 K-1 @@ -19470,28 +19645,28 @@ day - Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". + Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". day - Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". + Air temperature is the bulk temperature of the air, not the surface (skin) temperature. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". day - "Amount" means mass per unit area. "Precipitation" in the earth's atmosphere means precipitation of water in all phases. The construction lwe_thickness_of_X_amount or _content means the vertical extent of a layer of liquid water having the same mass per unit area. The abbreviation "lwe" means liquid water equivalent. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". + "Amount" means mass per unit area. "Precipitation" in the earth's atmosphere means precipitation of water in all phases. The construction lwe_thickness_of_X_amount or _content means the vertical extent of a layer of liquid water having the same mass per unit area. The abbreviation "lwe" means liquid water equivalent. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". day - "Amount" means mass per unit area. "Precipitation" in the earth's atmosphere means precipitation of water in all phases.The construction lwe_thickness_of_X_amount or _content means the vertical extent of a layer of liquid water having the same mass per unit area. The abbreviation "lwe" means liquid water equivalent. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the a standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". + "Amount" means mass per unit area. "Precipitation" in the earth's atmosphere means precipitation of water in all phases.The construction lwe_thickness_of_X_amount or _content means the vertical extent of a layer of liquid water having the same mass per unit area. The abbreviation "lwe" means liquid water equivalent. A spell is the number of consecutive days on which the condition X_below|above_threshold is satisfied. A variable whose standard name has the form spell_length_of_days_with_X_below|above_threshold must have a coordinate variable or scalar coordinate variable with the standard name of X to supply the threshold(s). It must have a climatological time variable, and a cell_method entry for within days which describes the processing of quantity X before the threshold is applied. A spell_length_of_days is an intensive quantity in time, and the cell_methods entry for over days can be any of the methods listed in Appendix E appropriate for intensive quantities e.g. "maximum", "minimum" or "mean". @@ -19627,6 +19802,13 @@ "Sea surface height" is a time-varying quantity. The steric change in sea surface height is the change in height that a water column of standard temperature zero degrees Celsius and practical salinity S=35.0 would undergo when its temperature and salinity are changed to the observed values. The sum of the quantities with standard names thermosteric_change_in_sea_surface_height and halosteric_change_in_sea_surface_height is the total steric change in the water column height, which has the standard name of steric_change_in_sea_surface_height. The sum of the quantities with standard names sea_water_mass_per_unit_area_expressed_as_thickness and steric_change_in_sea_surface_height is the total thickness of the sea water column. + + m s-1 + + + Storm motion speed is defined as a two dimensional velocity vector, with no vertical component. (Vertical motion in the atmosphere has the standard name upward_air_velocity.) It is defined as the average speed of a supercell, and the direction the storm will move from. It is not dependent on the orientation of the ground-relative winds. Storm motion speed generally follows the methodology outlined in Bunkers et al. (2000). + + 1 @@ -19928,6 +20110,20 @@ The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted by the wind at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, wind stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Eastward" indicates a vector component which is positive when directed eastward (negative westward). "Downward eastward" indicates the ZX component of a tensor. A downward eastward stress is a downward flux of eastward momentum, which accelerates the lower medium eastward and the upper medium westward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Boundary layer mixing" means turbulent motions that transport heat, water, momentum and chemical constituents within the atmospheric boundary layer and affect exchanges between the surface and the atmosphere. The atmospheric boundary layer is typically characterised by a well-mixed sub-cloud layer of order 500 metres, and by a more extended conditionally unstable layer with boundary-layer clouds up to 2 km. (Reference: IPCC Third Assessment Report, Working Group 1: The Scientific Basis, 7.2.2.3, https://archive.ipcc.ch/ipccreports/tar/wg1/273.htm). + + Pa + + + The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted by the wind at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, wind stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Eastward" indicates a vector component which is positive when directed northward (negative southward). "Downward eastward" indicates the ZX component of a tensor. A downward eastward stress is a downward flux of eastward momentum, which accelerates the lower medium eastward and the upper medium westward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Viscosity" means the stress associated with viscous effects at the sea surface and is equivalent to the turbulent stress just outside the viscous sublayer. + + + + Pa + + + The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted by the wind at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, wind stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Eastward" indicates a vector component which is positive when directed northward (negative southward). "Downward eastward" indicates the ZX component of a tensor. A downward eastward stress is a downward flux of eastward momentum, which accelerates the lower medium eastward and the upper medium westward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Sea surface waves" means the stress associated with form drag over sea surface waves. + + W m-2 @@ -20068,6 +20264,20 @@ The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted by the wind at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, wind stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Northward" indicates a vector component which is positive when directed northward (negative southward). "Downward northward" indicates the ZY component of a tensor. A downward northward stress is a downward flux of northward momentum, which accelerates the lower medium northward and the upper medium southward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Boundary layer mixing" means turbulent motions that transport heat, water, momentum and chemical constituents within the atmospheric boundary layer and affect exchanges between the surface and the atmosphere. The atmospheric boundary layer is typically characterised by a well-mixed sub-cloud layer of order 500 metres, and by a more extended conditionally unstable layer with boundary-layer clouds up to 2 km. (Reference: IPCC Third Assessment Report, Working Group 1: The Scientific Basis, 7.2.2.3, https://archive.ipcc.ch/ipccreports/tar/wg1/273.htm). + + Pa + + + The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted by the wind at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, wind stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Northward" indicates a vector component which is positive when directed northward (negative southward). "Downward northward" indicates the ZY component of a tensor. A downward northward stress is a downward flux of northward momentum, which accelerates the lower medium northward and the upper medium southward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Viscosity" means the stress associated with viscous effects at the sea surface and is equivalent to the turbulent stress just outside the viscous sublayer. + + + + Pa + + + The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted by the wind at the surface. A downward stress is a downward flux of momentum. Over large bodies of water, wind stress can drive near-surface currents. "Downward" indicates a vector component which is positive when directed downward (negative upward). "Northward" indicates a vector component which is positive when directed northward (negative southward). "Downward northward" indicates the ZY component of a tensor. A downward northward stress is a downward flux of northward momentum, which accelerates the lower medium northward and the upper medium southward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Sea surface waves" means the stress associated with form drag over sea surface waves. + + W m-2 @@ -23323,6 +23533,13 @@ The surface called "surface" means the lower boundary of the atmosphere. "anomaly" means difference from climatology. The surface temperature is the (skin) temperature at the interface, not the bulk temperature of the medium above or below. + + Pa + + + The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted at the surface. An upward stress is an upward flux of momentum into the atmosphere. "Upward" indicates a vector component which is positive when directed upward (negative downward). "Eastward" indicates a vector component which is positive when directed northward (negative southward). "Upward eastward" indicates the ZX component of a tensor. An upward eastward stress is an upward flux of eastward momentum, which accelerates the upper medium eastward and the lower medium westward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Sea surface waves" means the stress associated with oscillatory motions of a wavy sea surface. + + W m-2 @@ -23596,6 +23813,13 @@ The surface called "surface" means the lower boundary of the atmosphere. "Upward" indicates a vector component which is positive when directed upward (negative downward). In accordance with common usage in geophysical disciplines, "flux" implies per unit area, called "flux density" in physics. The chemical formula for dimethyl sulfide is (CH3)2S. Dimethyl sulfide is sometimes referred to as DMS. + + Pa + + + The surface called "surface" means the lower boundary of the atmosphere. "Surface stress" means the shear stress (force per unit area) exerted at the surface. An upward stress is an upward flux of momentum into the atmosphere. "Upward" indicates a vector component which is positive when directed upward (negative downward). "Northward" indicates a vector component which is positive when directed northward (negative southward). "Upward northward" indicates the ZY component of a tensor. An upward northward stress is an upward flux of northward momentum, which accelerates the upper medium northward and the lower medium southward. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "Sea surface waves" means the stress associated with oscillatory motions of a wavy sea surface. + + W m-2 122 E146 @@ -23799,6 +24023,13 @@ The quantity with standard name temperature_flux_due_to_runoff_expressed_as_heat_flux_into_sea_water is the heat carried by the transfer of water into the liquid ocean by the process of runoff. This quantity additionally includes melt water from sea ice and icebergs. It is calculated relative to the heat that would be transported by runoff water entering the sea at zero degrees Celsius. It is calculated as the product QrunoffCpTrunoff, where Q runoff is the mass flux of liquid runoff entering the sea water (kg m-2 s-1), Cp is the specific heat capacity of water, and Trunoff is the temperature in degrees Celsius of the runoff water. In accordance with common usage in geophysical disciplines, "flux" implies per unit area, called "flux density" in physics. The specification of a physical process by the phrase "due_to_" process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. Runoff is the liquid water which drains from land. If not specified, "runoff" refers to the sum of surface runoff and subsurface drainage. + + K + + + The temperature at any given depth (or in a layer) below the surface of the ground, excluding surficial snow and ice (but not permafrost or soil). For temperatures in surface lying snow and ice, the more specific standard names temperature_in_surface_snow and land_ice_temperature should be used. For temperatures measured or modelled specifically for the soil layer (the near-surface layer where plants sink their roots) the standard name soil_temperature should be used. + + K E238 @@ -28041,6 +28272,13 @@ The specification of a physical process by the phrase due_to_process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "tendency_of_X" means derivative of X with respect to time. "Content" indicates a quantity per unit area. "Layer" means any layer with upper and lower boundaries that have constant values in some vertical coordinate. There must be a vertical coordinate variable indicating the extent of the layer(s). If the layers are model layers, the vertical coordinate can be model_level_number, but it is recommended to specify a physical coordinate (in a scalar or auxiliary coordinate variable) as well. + + kg s-1 + + + The phrase "tendency_of_X" means derivative of X with respect to time. "Land ice" means glaciers, ice-caps and ice-sheets resting on bedrock and also includes ice-shelves. The horizontal domain over which the quantity is calculated is described by the associated coordinate variables and coordinate bounds or by a coordinate variable or scalar coordinate variable with the standard name of "region" supplied according to section 6.1.1 of the CF conventions. + + kg s-1 @@ -29623,6 +29861,13 @@ The specification of a physical process by the phrase due_to_process means that the quantity named is a single term in a sum of terms which together compose the general quantity named by omitting the phrase. "tendency_of_X" means derivative of X with respect to time. Speed is the magnitude of velocity. Wind is defined as a two-dimensional (horizontal) air velocity vector, with no vertical component. (Vertical motion in the atmosphere has the standard name upward_air_velocity.) The wind speed is the magnitude of the wind velocity. + + W m-1 K-1 + + + Thermal conductivity is the constant k in the formula q = -k grad T where q is the heat transfer per unit time per unit area of a surface normal to the direction of transfer and grad T is the temperature gradient. Thermal conductivity is a property of the material. + + J m-2 @@ -31227,14 +31472,110 @@ - - integral_wrt_time_of_surface_downward_northward_stress + + biological_taxon_lsid + + + + temperature_in_ground + + + + surface_snow_density + + + + soot_content_of_surface_snow + + + + liquid_water_content_of_surface_snow + + + + surface_snow_thickness + + + + thermal_energy_content_of_surface_snow + + + + temperature_in_surface_snow integral_wrt_time_of_surface_downward_eastward_stress + + integral_wrt_time_of_surface_downward_northward_stress + + + + tendency_of_atmosphere_mass_content_of_water_vapor_due_to_sublimation_of_surface_snow_and_ice + + + + atmosphere_upward_absolute_vorticity + + + + atmosphere_upward_relative_vorticity + + + + area_type + + + + area_type + + + + iron_growth_limitation_of_diazotrophic_phytoplankton + + + + growth_limitation_of_diazotrophic_phytoplankton_due_to_solar_irradiance + + + + tendency_of_mole_concentration_of_particulate_organic_matter_expressed_as_carbon_in_sea_water_due_to_net_primary_production_by_diazotrophic_phytoplankton + + + + mole_concentration_of_diazotrophic_phytoplankton_expressed_as_carbon_in_sea_water + + + + mass_fraction_of_liquid_precipitation_in_air + + + + mass_fraction_of_liquid_precipitation_in_air + + + + mass_concentration_of_diazotrophic_phytoplankton_expressed_as_chlorophyll_in_sea_water + + + + air_pseudo_equivalent_potential_temperature + + + + tendency_of_mass_fraction_of_stratiform_cloud_ice_in_air_due_to_melting_to_cloud_liquid_water + + + + tendency_of_mass_fraction_of_stratiform_cloud_ice_in_air_due_to_heterogeneous_nucleation_from_cloud_liquid_water + + + + tendency_of_mass_fraction_of_stratiform_cloud_ice_in_air_due_to_riming_from_cloud_liquid_water + + nitrogen_growth_limitation_of_diazotrophic_phytoplankton @@ -31263,42 +31604,6 @@ effective_radius_of_cloud_liquid_water_particles_at_liquid_water_cloud_top - - mass_content_of_cloud_liquid_water_in_atmosphere_layer - - - - air_equivalent_potential_temperature - - - - number_concentration_of_stratiform_cloud_liquid_water_particles_at_stratiform_liquid_water_cloud_top - - - - number_concentration_of_convective_cloud_liquid_water_particles_at_convective_liquid_water_cloud_top - - - - wave_frequency - - - - upward_eastward_momentum_flux_in_air_due_to_nonorographic_eastward_gravity_waves - - - - tendency_of_troposphere_moles_of_carbon_monoxide - - - - tendency_of_atmosphere_moles_of_sulfate_dry_aerosol_particles - - - - tendency_of_atmosphere_mass_content_of_nitrate_dry_aerosol_particles_due_to_dry_deposition - - northward_heat_flux_in_air_due_to_eddy_advection @@ -31355,72 +31660,56 @@ atmosphere_mass_content_of_cloud_liquid_water - - mass_concentration_of_coarse_mode_ambient_aerosol_particles_in_air - - - - sea_water_velocity_to_direction - - - - sea_water_velocity_to_direction - - - - gross_primary_productivity_of_biomass_expressed_as_carbon - - - - eastward_water_vapor_flux_in_air + + mass_fraction_of_sulfate_dry_aerosol_particles_in_air - - sea_water_velocity_from_direction + + mass_fraction_of_nitric_acid_trihydrate_ambient_aerosol_particles_in_air - - thickness_of_stratiform_snowfall_amount + + mass_fraction_of_ammonium_dry_aerosol_particles_in_air - - optical_thickness_of_atmosphere_layer_due_to_ambient_aerosol_particles + + tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_shallow_convection - - optical_thickness_of_atmosphere_layer_due_to_ambient_aerosol_particles + + tendency_of_mass_content_of_water_vapor_in_atmosphere_layer - - lwe_thickness_of_stratiform_snowfall_amount + + mass_content_of_cloud_ice_in_atmosphere_layer - - equivalent_thickness_at_stp_of_atmosphere_ozone_content + + mass_concentration_of_secondary_particulate_organic_matter_dry_aerosol_particles_in_air - - atmosphere_optical_thickness_due_to_water_in_ambient_aerosol_particles + + mass_concentration_of_mercury_dry_aerosol_particles_in_air - - atmosphere_optical_thickness_due_to_dust_dry_aerosol_particles + + mass_concentration_of_coarse_mode_ambient_aerosol_particles_in_air - - atmosphere_optical_thickness_due_to_dust_ambient_aerosol_particles + + sea_water_velocity_to_direction - - atmosphere_optical_thickness_due_to_ambient_aerosol_particles + + sea_water_velocity_to_direction - - atmosphere_optical_thickness_due_to_ambient_aerosol_particles + + gross_primary_productivity_of_biomass_expressed_as_carbon - - atmosphere_net_upward_convective_mass_flux + + eastward_water_vapor_flux_in_air @@ -31435,94 +31724,6 @@ tendency_of_atmosphere_mass_content_of_water_vapor_due_to_advection - - thermal_energy_content_of_surface_snow - - - - liquid_water_content_of_surface_snow - - - - temperature_in_surface_snow - - - - tendency_of_atmosphere_mass_content_of_water_vapor_due_to_sublimation_of_surface_snow_and_ice - - - - surface_snow_thickness - - - - surface_snow_density - - - - soot_content_of_surface_snow - - - - atmosphere_upward_absolute_vorticity - - - - atmosphere_upward_relative_vorticity - - - - area_type - - - - area_type - - - - iron_growth_limitation_of_diazotrophic_phytoplankton - - - - growth_limitation_of_diazotrophic_phytoplankton_due_to_solar_irradiance - - - - tendency_of_mole_concentration_of_particulate_organic_matter_expressed_as_carbon_in_sea_water_due_to_net_primary_production_by_diazotrophic_phytoplankton - - - - mole_concentration_of_diazotrophic_phytoplankton_expressed_as_carbon_in_sea_water - - - - mass_fraction_of_liquid_precipitation_in_air - - - - mass_fraction_of_liquid_precipitation_in_air - - - - mass_concentration_of_diazotrophic_phytoplankton_expressed_as_chlorophyll_in_sea_water - - - - air_pseudo_equivalent_potential_temperature - - - - tendency_of_mass_fraction_of_stratiform_cloud_ice_in_air_due_to_melting_to_cloud_liquid_water - - - - tendency_of_mass_fraction_of_stratiform_cloud_ice_in_air_due_to_heterogeneous_nucleation_from_cloud_liquid_water - - - - tendency_of_mass_fraction_of_stratiform_cloud_ice_in_air_due_to_riming_from_cloud_liquid_water - - tendency_of_atmosphere_mass_content_of_water_vapor @@ -31611,256 +31812,68 @@ atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_waste_treatment_and_disposal - - - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_savanna_and_grassland_fires - - - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_maritime_transport - - - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_land_transport - - - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_forest_fires - - - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_agricultural_waste_burning - - - - tendency_of_atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles_due_to_wet_deposition - - - - moles_of_cfc11_per_unit_mass_in_sea_water - - - - atmosphere_moles_of_cfc11 - - - - tendency_of_atmosphere_moles_of_cfc113 - - - - atmosphere_moles_of_cfc113 - - - - tendency_of_atmosphere_moles_of_cfc114 - - - - atmosphere_moles_of_cfc114 - - - - tendency_of_atmosphere_moles_of_cfc115 - - - - atmosphere_moles_of_cfc115 - - - - tendency_of_atmosphere_moles_of_cfc12 - - - - atmosphere_moles_of_cfc12 - - - - tendency_of_atmosphere_moles_of_halon1202 - - - - atmosphere_moles_of_halon1202 - - - - tendency_of_atmosphere_moles_of_halon1211 - - - - atmosphere_moles_of_halon1211 - - - - tendency_of_atmosphere_moles_of_halon1301 - - - - atmosphere_moles_of_halon1301 - - - - tendency_of_atmosphere_moles_of_halon2402 - - - - atmosphere_moles_of_halon2402 - - - - tendency_of_atmosphere_moles_of_hcc140a - - - - effective_radius_of_convective_cloud_rain_particles - - - - tendency_of_troposphere_moles_of_hcc140a - - - - tendency_of_middle_atmosphere_moles_of_hcc140a - - - - tendency_of_troposphere_moles_of_hcfc22 - - - - tendency_of_atmosphere_moles_of_hcfc22 - - - - atmosphere_moles_of_hcfc22 - - - - tendency_of_atmosphere_number_content_of_aerosol_particles_due_to_turbulent_deposition - - - - lagrangian_tendency_of_atmosphere_sigma_coordinate - - - - lagrangian_tendency_of_atmosphere_sigma_coordinate - - - - diameter_of_ambient_aerosol_particles - - - - effective_radius_of_stratiform_cloud_ice_particles - - - - effective_radius_of_convective_cloud_ice_particles - - - - effective_radius_of_stratiform_cloud_graupel_particles - - - - effective_radius_of_stratiform_cloud_rain_particles - - - - effective_radius_of_convective_cloud_snow_particles - - - - mass_fraction_of_sulfate_dry_aerosol_particles_in_air - - - - mass_fraction_of_nitric_acid_trihydrate_ambient_aerosol_particles_in_air - - - - mass_fraction_of_ammonium_dry_aerosol_particles_in_air - - - - tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_shallow_convection - - - - tendency_of_mass_content_of_water_vapor_in_atmosphere_layer - - - - mass_content_of_cloud_ice_in_atmosphere_layer - - - - mass_concentration_of_secondary_particulate_organic_matter_dry_aerosol_particles_in_air + + mass_content_of_cloud_liquid_water_in_atmosphere_layer - - mass_concentration_of_mercury_dry_aerosol_particles_in_air + + air_equivalent_potential_temperature - - product_of_eastward_wind_and_lagrangian_tendency_of_air_pressure + + number_concentration_of_stratiform_cloud_liquid_water_particles_at_stratiform_liquid_water_cloud_top - - carbon_mass_flux_into_litter_and_soil_due_to_anthropogenic_land_use_or_land_cover_change + + number_concentration_of_convective_cloud_liquid_water_particles_at_convective_liquid_water_cloud_top - - stratiform_cloud_area_fraction + + wave_frequency - - mass_fraction_of_mercury_dry_aerosol_particles_in_air + + upward_eastward_momentum_flux_in_air_due_to_nonorographic_eastward_gravity_waves - - atmosphere_moles_of_hcc140a + + tendency_of_troposphere_moles_of_carbon_monoxide - - floating_ice_shelf_area_fraction + + tendency_of_atmosphere_moles_of_sulfate_dry_aerosol_particles - - atmosphere_moles_of_carbon_tetrachloride + + tendency_of_atmosphere_mass_content_of_nitrate_dry_aerosol_particles_due_to_dry_deposition - - net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_miscellaneous_phytoplankton + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_waste_treatment_and_disposal - - mole_fraction_of_inorganic_bromine_in_air + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_savanna_and_grassland_fires - - water_vapor_saturation_deficit_in_air + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_maritime_transport - - tendency_of_atmosphere_mass_content_of_elemental_carbon_dry_aerosol_particles_due_to_emission_from_agricultural_waste_burning + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_land_transport - - tendency_of_atmosphere_moles_of_carbon_tetrachloride + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_forest_fires - - tendency_of_atmosphere_moles_of_carbon_monoxide + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_agricultural_waste_burning - - tendency_of_atmosphere_mass_content_of_nitrogen_compounds_expressed_as_nitrogen_due_to_wet_deposition + + tendency_of_atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles_due_to_wet_deposition @@ -32015,104 +32028,152 @@ tendency_of_atmosphere_moles_of_cfc11 - - mole_concentration_of_phytoplankton_expressed_as_nitrogen_in_sea_water + + moles_of_cfc11_per_unit_mass_in_sea_water - - net_primary_mole_productivity_of_biomass_expressed_as_carbon_due_to_nitrate_utilization + + atmosphere_moles_of_cfc11 - - net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_picophytoplankton + + tendency_of_atmosphere_moles_of_hcc140a - - net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_phytoplankton + + effective_radius_of_convective_cloud_rain_particles - - net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_diatoms + + tendency_of_troposphere_moles_of_hcc140a - - net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_calcareous_phytoplankton + + tendency_of_middle_atmosphere_moles_of_hcc140a - - mole_concentration_of_diatoms_expressed_as_nitrogen_in_sea_water + + tendency_of_troposphere_moles_of_hcfc22 - - tendency_of_mole_concentration_of_dissolved_inorganic_phosphorus_in_sea_water_due_to_biological_processes + + tendency_of_atmosphere_moles_of_hcfc22 - - tendency_of_mole_concentration_of_dissolved_inorganic_silicon_in_sea_water_due_to_biological_processes + + atmosphere_moles_of_hcfc22 - - tendency_of_atmosphere_mole_concentration_of_carbon_monoxide_due_to_chemical_destruction + + tendency_of_atmosphere_number_content_of_aerosol_particles_due_to_turbulent_deposition - - volume_extinction_coefficient_in_air_due_to_ambient_aerosol_particles + + lagrangian_tendency_of_atmosphere_sigma_coordinate - - platform_name + + lagrangian_tendency_of_atmosphere_sigma_coordinate - - platform_id + + diameter_of_ambient_aerosol_particles - - platform_pitch + + effective_radius_of_stratiform_cloud_ice_particles - - tendency_of_specific_humidity_due_to_stratiform_precipitation + + effective_radius_of_convective_cloud_ice_particles - - tendency_of_air_temperature_due_to_stratiform_precipitation + + effective_radius_of_stratiform_cloud_graupel_particles - - water_evaporation_amount_from_canopy + + effective_radius_of_stratiform_cloud_rain_particles - - tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_turbulent_deposition + + effective_radius_of_convective_cloud_snow_particles - - tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_gravitational_settling + + product_of_eastward_wind_and_lagrangian_tendency_of_air_pressure - - tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_emission + + carbon_mass_flux_into_litter_and_soil_due_to_anthropogenic_land_use_or_land_cover_change - - atmosphere_mass_content_of_cloud_ice + + stratiform_cloud_area_fraction - - stratiform_precipitation_amount + + sea_water_velocity_from_direction - - tendency_of_atmosphere_moles_of_nitrous_oxide + + thickness_of_stratiform_snowfall_amount - - tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_dry_deposition + + optical_thickness_of_atmosphere_layer_due_to_ambient_aerosol_particles - - atmosphere_mass_content_of_convective_cloud_condensed_water + + optical_thickness_of_atmosphere_layer_due_to_ambient_aerosol_particles + + + + lwe_thickness_of_stratiform_snowfall_amount + + + + equivalent_thickness_at_stp_of_atmosphere_ozone_content + + + + atmosphere_optical_thickness_due_to_water_in_ambient_aerosol_particles + + + + atmosphere_optical_thickness_due_to_dust_dry_aerosol_particles + + + + atmosphere_optical_thickness_due_to_dust_ambient_aerosol_particles + + + + atmosphere_optical_thickness_due_to_ambient_aerosol_particles + + + + atmosphere_optical_thickness_due_to_ambient_aerosol_particles + + + + atmosphere_net_upward_convective_mass_flux + + + + mass_fraction_of_mercury_dry_aerosol_particles_in_air + + + + atmosphere_moles_of_hcc140a + + + + floating_ice_shelf_area_fraction + + + + atmosphere_moles_of_carbon_tetrachloride @@ -32127,12 +32188,144 @@ mole_fraction_of_noy_expressed_as_nitrogen_in_air - - tendency_of_atmosphere_moles_of_methane + + net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_miscellaneous_phytoplankton - - rate_of_hydroxyl_radical_destruction_due_to_reaction_with_nmvoc + + mole_fraction_of_inorganic_bromine_in_air + + + + water_vapor_saturation_deficit_in_air + + + + tendency_of_atmosphere_mass_content_of_elemental_carbon_dry_aerosol_particles_due_to_emission_from_agricultural_waste_burning + + + + tendency_of_atmosphere_moles_of_carbon_tetrachloride + + + + tendency_of_atmosphere_moles_of_carbon_monoxide + + + + tendency_of_atmosphere_moles_of_cfc113 + + + + atmosphere_moles_of_cfc113 + + + + tendency_of_atmosphere_moles_of_cfc114 + + + + atmosphere_moles_of_cfc114 + + + + tendency_of_atmosphere_moles_of_cfc115 + + + + atmosphere_moles_of_cfc115 + + + + tendency_of_atmosphere_moles_of_cfc12 + + + + atmosphere_moles_of_cfc12 + + + + tendency_of_atmosphere_moles_of_halon1202 + + + + atmosphere_moles_of_halon1202 + + + + tendency_of_atmosphere_moles_of_halon1211 + + + + atmosphere_moles_of_halon1211 + + + + tendency_of_atmosphere_moles_of_halon1301 + + + + atmosphere_moles_of_halon1301 + + + + tendency_of_atmosphere_moles_of_halon2402 + + + + atmosphere_moles_of_halon2402 + + + + tendency_of_atmosphere_mass_content_of_nitrogen_compounds_expressed_as_nitrogen_due_to_wet_deposition + + + + mole_concentration_of_phytoplankton_expressed_as_nitrogen_in_sea_water + + + + net_primary_mole_productivity_of_biomass_expressed_as_carbon_due_to_nitrate_utilization + + + + net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_picophytoplankton + + + + net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_phytoplankton + + + + net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_diatoms + + + + net_primary_mole_productivity_of_biomass_expressed_as_carbon_by_calcareous_phytoplankton + + + + mole_concentration_of_diatoms_expressed_as_nitrogen_in_sea_water + + + + tendency_of_mole_concentration_of_dissolved_inorganic_phosphorus_in_sea_water_due_to_biological_processes + + + + tendency_of_mole_concentration_of_dissolved_inorganic_silicon_in_sea_water_due_to_biological_processes + + + + tendency_of_atmosphere_mole_concentration_of_carbon_monoxide_due_to_chemical_destruction + + + + volume_extinction_coefficient_in_air_due_to_ambient_aerosol_particles + + + + atmosphere_mass_content_of_convective_cloud_condensed_water @@ -32207,6 +32400,78 @@ lwe_thickness_of_stratiform_precipitation_amount + + tendency_of_atmosphere_moles_of_methane + + + + rate_of_hydroxyl_radical_destruction_due_to_reaction_with_nmvoc + + + + magnitude_of_sea_ice_displacement + + + + surface_downwelling_radiative_flux_per_unit_wavelength_in_sea_water + + + + surface_downwelling_radiative_flux_per_unit_wavelength_in_air + + + + surface_downwelling_shortwave_flux_in_air_assuming_clear_sky_and_no_aerosol + + + + surface_downwelling_photon_spherical_irradiance_per_unit_wavelength_in_sea_water + + + + surface_downwelling_photon_flux_per_unit_wavelength_in_sea_water + + + + surface_downwelling_longwave_flux_in_air + + + + integral_wrt_time_of_surface_downwelling_shortwave_flux_in_air + + + + integral_wrt_time_of_surface_downwelling_longwave_flux_in_air + + + + downwelling_spherical_irradiance_per_unit_wavelength_in_sea_water + + + + downwelling_radiative_flux_per_unit_wavelength_in_sea_water + + + + downwelling_radiative_flux_per_unit_wavelength_in_air + + + + downwelling_shortwave_flux_in_air_assuming_clear_sky_and_no_aerosol + + + + downwelling_photon_spherical_irradiance_per_unit_wavelength_in_sea_water + + + + downwelling_radiance_per_unit_wavelength_in_air + + + + downwelling_photon_radiance_per_unit_wavelength_in_sea_water + + surface_upwelling_shortwave_flux_in_air_assuming_clear_sky @@ -32263,26 +32528,6 @@ surface_upwelling_radiance_per_unit_wavelength_in_air_reflected_by_sea_water - - surface_water_evaporation_flux - - - - water_evapotranspiration_flux - - - - water_volume_transport_into_sea_water_from_rivers - - - - stratiform_graupel_flux - - - - toa_outgoing_shortwave_flux_assuming_clear_sky_and_no_aerosol - - wood_debris_mass_content_of_carbon @@ -32311,18 +32556,6 @@ volume_scattering_coefficient_of_radiative_flux_in_air_due_to_ambient_aerosol_particles - - platform_yaw - - - - platform_roll - - - - water_vapor_partial_pressure_in_air - - volume_scattering_coefficient_of_radiative_flux_in_air_due_to_dried_aerosol_particles @@ -32339,68 +32572,68 @@ integral_wrt_height_of_product_of_eastward_wind_and_specific_humidity - - magnitude_of_sea_ice_displacement + + platform_yaw - - surface_downwelling_radiative_flux_per_unit_wavelength_in_sea_water + + platform_roll - - surface_downwelling_radiative_flux_per_unit_wavelength_in_air + + water_vapor_partial_pressure_in_air - - surface_downwelling_shortwave_flux_in_air_assuming_clear_sky_and_no_aerosol + + platform_name - - surface_downwelling_photon_spherical_irradiance_per_unit_wavelength_in_sea_water + + platform_id - - surface_downwelling_photon_flux_per_unit_wavelength_in_sea_water + + platform_pitch - - surface_downwelling_longwave_flux_in_air + + tendency_of_specific_humidity_due_to_stratiform_precipitation - - integral_wrt_time_of_surface_downwelling_shortwave_flux_in_air + + tendency_of_air_temperature_due_to_stratiform_precipitation - - integral_wrt_time_of_surface_downwelling_longwave_flux_in_air + + water_evaporation_amount_from_canopy - - downwelling_spherical_irradiance_per_unit_wavelength_in_sea_water + + tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_turbulent_deposition - - downwelling_radiative_flux_per_unit_wavelength_in_sea_water + + tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_gravitational_settling - - downwelling_radiative_flux_per_unit_wavelength_in_air + + tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_emission - - downwelling_shortwave_flux_in_air_assuming_clear_sky_and_no_aerosol + + atmosphere_mass_content_of_cloud_ice - - downwelling_photon_spherical_irradiance_per_unit_wavelength_in_sea_water + + stratiform_precipitation_amount - - downwelling_radiance_per_unit_wavelength_in_air + + tendency_of_atmosphere_moles_of_nitrous_oxide - - downwelling_photon_radiance_per_unit_wavelength_in_sea_water + + tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_dry_deposition @@ -32507,6 +32740,26 @@ lwe_stratiform_precipitation_rate + + surface_water_evaporation_flux + + + + water_evapotranspiration_flux + + + + water_volume_transport_into_sea_water_from_rivers + + + + stratiform_graupel_flux + + + + toa_outgoing_shortwave_flux_assuming_clear_sky_and_no_aerosol + + ocean_y_overturning_mass_streamfunction_due_to_parameterized_eddy_advection @@ -32535,6 +32788,94 @@ tendency_of_sea_water_salinity_due_to_parameterized_eddy_advection + + integral_wrt_time_of_surface_net_downward_shortwave_flux + + + + tendency_of_ocean_eddy_kinetic_energy_content_due_to_parameterized_eddy_advection + + + + sea_water_y_velocity_due_to_parameterized_mesoscale_eddies + + + + ocean_tracer_biharmonic_diffusivity_due_to_parameterized_mesoscale_eddy_advection + + + + eastward_sea_water_velocity_due_to_parameterized_mesoscale_eddies + + + + northward_sea_water_velocity_due_to_parameterized_mesoscale_eddies + + + + ocean_heat_y_transport_due_to_parameterized_eddy_advection + + + + ocean_meridional_overturning_mass_streamfunction_due_to_parameterized_eddy_advection + + + + ocean_mass_y_transport_due_to_advection_and_parameterized_eddy_advection + + + + ocean_mass_x_transport_due_to_advection_and_parameterized_eddy_advection + + + + ocean_heat_x_transport_due_to_parameterized_eddy_advection + + + + northward_ocean_freshwater_transport_due_to_parameterized_eddy_advection + + + + northward_ocean_salt_transport_due_to_parameterized_eddy_advection + + + + integral_wrt_time_of_toa_outgoing_longwave_flux + + + + integral_wrt_time_of_toa_net_downward_shortwave_flux + + + + integral_wrt_time_of_surface_net_downward_longwave_flux + + + + integral_wrt_time_of_surface_downward_sensible_heat_flux + + + + integral_wrt_time_of_surface_downward_latent_heat_flux + + + + integral_wrt_time_of_air_temperature_excess + + + + integral_wrt_time_of_air_temperature_deficit + + + + tendency_of_atmosphere_mass_content_of_ammonium_dry_aerosol_particles_due_to_wet_deposition + + + + tendency_of_atmosphere_mass_content_of_ammonium_dry_aerosol_particles_due_to_dry_deposition + + atmosphere_absorption_optical_thickness_due_to_sulfate_ambient_aerosol_particles @@ -32711,392 +33052,392 @@ surface_geostrophic_sea_water_x_velocity_assuming_mean_sea_level_for_geoid - - tendency_of_atmosphere_mass_content_of_pm2p5_sea_salt_dry_aerosol_particles_due_to_emission + + air_pressure_at_mean_sea_level - - tendency_of_atmosphere_mass_content_of_pm2p5_sea_salt_dry_aerosol_particles_due_to_dry_deposition + + sea_floor_depth_below_mean_sea_level - - tendency_of_sea_surface_height_above_mean_sea_level + + ocean_mixed_layer_thickness_defined_by_vertical_tracer_diffusivity_deficit - - mass_fraction_of_pm10_ambient_aerosol_particles_in_air + + sea_surface_wind_wave_mean_period - - mass_fraction_of_pm10_ambient_aerosol_particles_in_air + + sea_surface_wave_mean_period - - mass_concentration_of_pm10_ambient_aerosol_particles_in_air + + sea_surface_swell_wave_mean_period - - atmosphere_optical_thickness_due_to_pm10_ambient_aerosol_particles + + sea_surface_wind_wave_to_direction - - surface_geostrophic_eastward_sea_water_velocity + + sea_surface_swell_wave_to_direction - - mass_fraction_of_pm2p5_ambient_aerosol_particles_in_air + + mass_content_of_water_in_soil_layer - - mass_fraction_of_pm2p5_ambient_aerosol_particles_in_air + + mass_content_of_water_in_soil - - mass_concentration_of_pm2p5_ambient_aerosol_particles_in_air + + sea_surface_wind_wave_significant_height - - atmosphere_optical_thickness_due_to_pm2p5_ambient_aerosol_particles + + sea_surface_swell_wave_significant_height - - mass_fraction_of_pm1_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_expressed_as_sulfur_due_to_turbulent_deposition - - mass_fraction_of_pm1_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_expressed_as_sulfur_due_to_turbulent_deposition - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_wet_deposition + + tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_due_to_emission - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_wet_deposition + + atmosphere_optical_thickness_due_to_particulate_organic_matter_ambient_aerosol_particles - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_gravitational_settling + + mass_concentration_of_nitric_acid_trihydrate_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_gravitational_settling + + atmosphere_mass_content_of_water_in_ambient_aerosol_particles - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_turbulent_deposition + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_residential_and_commercial_combustion - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_turbulent_deposition + + tendency_of_atmosphere_mass_content_of_mercury_dry_aerosol_particles_due_to_wet_deposition - - mass_concentration_of_pm1_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_mercury_dry_aerosol_particles_due_to_dry_deposition - - atmosphere_optical_thickness_due_to_pm1_ambient_aerosol_particles + + mass_fraction_of_nitrate_dry_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_dry_deposition + + mass_concentration_of_sulfate_dry_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_dry_deposition + + mass_fraction_of_water_in_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_pm2p5_sea_salt_dry_aerosol_particles_due_to_wet_deposition + + mass_fraction_of_secondary_particulate_organic_matter_dry_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_pm10_sea_salt_dry_aerosol_particles_due_to_wet_deposition + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_industrial_processes_and_combustion - - tendency_of_atmosphere_mass_content_of_pm10_sea_salt_dry_aerosol_particles_due_to_dry_deposition + + tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_energy_production_and_distribution - - mass_fraction_of_sea_salt_dry_aerosol_particles_in_air + + mass_concentration_of_sulfate_ambient_aerosol_particles_in_air - - mass_fraction_of_sea_salt_dry_aerosol_particles_in_air + + mass_concentration_of_sulfate_ambient_aerosol_particles_in_air - - mass_concentration_of_sea_salt_dry_aerosol_particles_in_air + + mass_concentration_of_dust_dry_aerosol_particles_in_air - - mass_concentration_of_sea_salt_dry_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles_due_to_emission - - atmosphere_optical_thickness_due_to_sea_salt_ambient_aerosol_particles + + tendency_of_atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles_due_to_dry_deposition - - atmosphere_optical_thickness_due_to_sea_salt_ambient_aerosol_particles + + mass_fraction_of_primary_particulate_organic_matter_dry_aerosol_particles_in_air - - atmosphere_mass_content_of_sea_salt_dry_aerosol_particles + + mass_fraction_of_particulate_organic_matter_dry_aerosol_particles_in_air - - atmosphere_mass_content_of_sea_salt_dry_aerosol_particles + + number_concentration_of_coarse_mode_ambient_aerosol_particles_in_air - - air_pressure_at_mean_sea_level + + sea_surface_wave_significant_height - - sea_floor_depth_below_mean_sea_level + + tendency_of_atmosphere_moles_of_nitric_acid_trihydrate_ambient_aerosol_particles - - ocean_mixed_layer_thickness_defined_by_vertical_tracer_diffusivity_deficit + + tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_due_to_dry_deposition - - sea_surface_wind_wave_mean_period + + tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_wet_deposition - - sea_surface_wave_mean_period + + number_concentration_of_nucleation_mode_ambient_aerosol_particles_in_air - - sea_surface_swell_wave_mean_period + + number_concentration_of_ambient_aerosol_particles_in_air - - sea_surface_wind_wave_to_direction + + mole_fraction_of_nitric_acid_trihydrate_ambient_aerosol_particles_in_air - - sea_surface_swell_wave_to_direction + + mass_fraction_of_dust_dry_aerosol_particles_in_air - - mass_content_of_water_in_soil_layer + + mass_concentration_of_water_in_ambient_aerosol_particles_in_air - - mass_content_of_water_in_soil + + mass_concentration_of_nitrate_dry_aerosol_particles_in_air - - sea_surface_wind_wave_significant_height + + mass_concentration_of_particulate_organic_matter_dry_aerosol_particles_in_air - - sea_surface_swell_wave_significant_height + + mass_concentration_of_ammonium_dry_aerosol_particles_in_air - - integral_wrt_time_of_surface_net_downward_shortwave_flux + + atmosphere_mass_content_of_sulfate_ambient_aerosol_particles - - tendency_of_ocean_eddy_kinetic_energy_content_due_to_parameterized_eddy_advection + + atmosphere_mass_content_of_sulfate_ambient_aerosol_particles - - sea_water_y_velocity_due_to_parameterized_mesoscale_eddies + + atmosphere_mass_content_of_dust_dry_aerosol_particles - - ocean_tracer_biharmonic_diffusivity_due_to_parameterized_mesoscale_eddy_advection + + atmosphere_absorption_optical_thickness_due_to_ambient_aerosol_particles - - eastward_sea_water_velocity_due_to_parameterized_mesoscale_eddies + + atmosphere_mass_content_of_sulfate_dry_aerosol_particles - - northward_sea_water_velocity_due_to_parameterized_mesoscale_eddies + + tendency_of_atmosphere_mass_content_of_water_vapor_due_to_turbulence - - ocean_heat_y_transport_due_to_parameterized_eddy_advection + + surface_upward_mole_flux_of_carbon_dioxide - - ocean_meridional_overturning_mass_streamfunction_due_to_parameterized_eddy_advection + + surface_downward_mole_flux_of_carbon_dioxide - - ocean_mass_y_transport_due_to_advection_and_parameterized_eddy_advection + + atmosphere_mass_content_of_cloud_condensed_water - - ocean_mass_x_transport_due_to_advection_and_parameterized_eddy_advection + + northward_water_vapor_flux_in_air - - ocean_heat_x_transport_due_to_parameterized_eddy_advection + + lwe_stratiform_snowfall_rate - - northward_ocean_freshwater_transport_due_to_parameterized_eddy_advection + + stratiform_snowfall_amount - - northward_ocean_salt_transport_due_to_parameterized_eddy_advection + + stratiform_rainfall_rate - - integral_wrt_time_of_toa_outgoing_longwave_flux + + stratiform_rainfall_flux - - integral_wrt_time_of_toa_net_downward_shortwave_flux + + stratiform_rainfall_amount - - integral_wrt_time_of_surface_net_downward_longwave_flux + + tendency_of_atmosphere_mass_content_of_pm2p5_sea_salt_dry_aerosol_particles_due_to_emission - - integral_wrt_time_of_surface_downward_sensible_heat_flux + + tendency_of_atmosphere_mass_content_of_pm2p5_sea_salt_dry_aerosol_particles_due_to_dry_deposition - - integral_wrt_time_of_surface_downward_latent_heat_flux + + tendency_of_sea_surface_height_above_mean_sea_level - - integral_wrt_time_of_air_temperature_excess + + mass_fraction_of_pm10_ambient_aerosol_particles_in_air - - integral_wrt_time_of_air_temperature_deficit + + mass_fraction_of_pm10_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_ammonium_dry_aerosol_particles_due_to_wet_deposition + + mass_concentration_of_pm10_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_ammonium_dry_aerosol_particles_due_to_dry_deposition + + atmosphere_optical_thickness_due_to_pm10_ambient_aerosol_particles - - tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_expressed_as_sulfur_due_to_turbulent_deposition + + surface_geostrophic_eastward_sea_water_velocity - - tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_expressed_as_sulfur_due_to_turbulent_deposition + + mass_fraction_of_pm2p5_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_due_to_emission + + mass_fraction_of_pm2p5_ambient_aerosol_particles_in_air - - atmosphere_optical_thickness_due_to_particulate_organic_matter_ambient_aerosol_particles + + mass_concentration_of_pm2p5_ambient_aerosol_particles_in_air - - mass_concentration_of_nitric_acid_trihydrate_ambient_aerosol_particles_in_air + + atmosphere_optical_thickness_due_to_pm2p5_ambient_aerosol_particles - - atmosphere_mass_content_of_water_in_ambient_aerosol_particles + + mass_fraction_of_pm1_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_residential_and_commercial_combustion + + mass_fraction_of_pm1_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_mercury_dry_aerosol_particles_due_to_wet_deposition + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_wet_deposition - - tendency_of_atmosphere_mass_content_of_mercury_dry_aerosol_particles_due_to_dry_deposition + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_wet_deposition - - mass_fraction_of_nitrate_dry_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_gravitational_settling - - mass_concentration_of_sulfate_dry_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_gravitational_settling - - mass_fraction_of_water_in_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_turbulent_deposition - - mass_fraction_of_secondary_particulate_organic_matter_dry_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_turbulent_deposition - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_industrial_processes_and_combustion + + mass_concentration_of_pm1_ambient_aerosol_particles_in_air - - tendency_of_atmosphere_mass_content_of_particulate_organic_matter_dry_aerosol_particles_expressed_as_carbon_due_to_emission_from_energy_production_and_distribution + + atmosphere_optical_thickness_due_to_pm1_ambient_aerosol_particles - - mass_concentration_of_sulfate_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_dry_deposition - - mass_concentration_of_sulfate_ambient_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_sea_salt_dry_aerosol_particles_due_to_dry_deposition - - mass_concentration_of_dust_dry_aerosol_particles_in_air + + tendency_of_atmosphere_mass_content_of_pm2p5_sea_salt_dry_aerosol_particles_due_to_wet_deposition - - tendency_of_atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles_due_to_emission + + tendency_of_atmosphere_mass_content_of_pm10_sea_salt_dry_aerosol_particles_due_to_wet_deposition - - tendency_of_atmosphere_mass_content_of_primary_particulate_organic_matter_dry_aerosol_particles_due_to_dry_deposition + + tendency_of_atmosphere_mass_content_of_pm10_sea_salt_dry_aerosol_particles_due_to_dry_deposition - - mass_fraction_of_primary_particulate_organic_matter_dry_aerosol_particles_in_air + + mass_fraction_of_sea_salt_dry_aerosol_particles_in_air - - mass_fraction_of_particulate_organic_matter_dry_aerosol_particles_in_air + + mass_fraction_of_sea_salt_dry_aerosol_particles_in_air - - number_concentration_of_coarse_mode_ambient_aerosol_particles_in_air + + mass_concentration_of_sea_salt_dry_aerosol_particles_in_air - - mass_concentration_of_primary_particulate_organic_matter_dry_aerosol_particles_in_air + + mass_concentration_of_sea_salt_dry_aerosol_particles_in_air - - atmosphere_mass_content_of_ammonium_dry_aerosol_particles + + atmosphere_optical_thickness_due_to_sea_salt_ambient_aerosol_particles - - stratiform_rainfall_rate + + atmosphere_optical_thickness_due_to_sea_salt_ambient_aerosol_particles - - stratiform_rainfall_flux + + atmosphere_mass_content_of_sea_salt_dry_aerosol_particles - - stratiform_rainfall_amount + + atmosphere_mass_content_of_sea_salt_dry_aerosol_particles @@ -33139,22 +33480,6 @@ upward_eastward_momentum_flux_in_air_due_to_nonorographic_westward_gravity_waves - - mass_fraction_of_ozone_in_air - - - - mass_fraction_of_convective_cloud_condensed_water_in_air - - - - sea_surface_swell_wave_period - - - - surface_drag_coefficient_in_air - - specific_gravitational_potential_energy @@ -33175,6 +33500,14 @@ isotropic_longwave_radiance_in_air + + mass_concentration_of_primary_particulate_organic_matter_dry_aerosol_particles_in_air + + + + atmosphere_mass_content_of_ammonium_dry_aerosol_particles + + stratiform_snowfall_flux @@ -33183,108 +33516,120 @@ thickness_of_stratiform_rainfall_amount - - sea_surface_wave_significant_height + + sea_surface_wind_wave_period - - tendency_of_atmosphere_moles_of_nitric_acid_trihydrate_ambient_aerosol_particles + + omnidirectional_spherical_irradiance_per_unit_wavelength_in_sea_water - - tendency_of_atmosphere_mass_content_of_sulfate_dry_aerosol_particles_due_to_dry_deposition + + tendency_of_middle_atmosphere_moles_of_molecular_hydrogen - - tendency_of_atmosphere_mass_content_of_dust_dry_aerosol_particles_due_to_wet_deposition + + tendency_of_middle_atmosphere_moles_of_methyl_chloride - - number_concentration_of_nucleation_mode_ambient_aerosol_particles_in_air + + tendency_of_middle_atmosphere_moles_of_methane - - number_concentration_of_ambient_aerosol_particles_in_air + + sea_water_y_velocity - - mole_fraction_of_nitric_acid_trihydrate_ambient_aerosol_particles_in_air + + sea_water_x_velocity - - mass_fraction_of_dust_dry_aerosol_particles_in_air + + mole_fraction_of_hypochlorous_acid_in_air - - mass_concentration_of_water_in_ambient_aerosol_particles_in_air + + tendency_of_troposphere_moles_of_molecular_hydrogen - - mass_concentration_of_nitrate_dry_aerosol_particles_in_air + + tendency_of_troposphere_moles_of_methyl_chloride - - mass_concentration_of_particulate_organic_matter_dry_aerosol_particles_in_air + + mass_content_of_water_vapor_in_atmosphere_layer - - mass_concentration_of_ammonium_dry_aerosol_particles_in_air + + mass_content_of_water_in_atmosphere_layer - - atmosphere_mass_content_of_sulfate_ambient_aerosol_particles + + tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_turbulence - - atmosphere_mass_content_of_sulfate_ambient_aerosol_particles + + tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_deep_convection - - atmosphere_mass_content_of_dust_dry_aerosol_particles + + tendency_of_troposphere_moles_of_methyl_bromide - - atmosphere_absorption_optical_thickness_due_to_ambient_aerosol_particles + + tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_convection - - atmosphere_mass_content_of_sulfate_dry_aerosol_particles + + tendency_of_atmosphere_mass_content_of_water_vapor_due_to_shallow_convection - - tendency_of_atmosphere_mass_content_of_water_vapor_due_to_turbulence + + radiation_wavelength - - surface_upward_mole_flux_of_carbon_dioxide + + tendency_of_troposphere_moles_of_methane - - surface_downward_mole_flux_of_carbon_dioxide + + tendency_of_atmosphere_mass_content_of_water_due_to_advection - - atmosphere_mass_content_of_cloud_condensed_water + + mole_fraction_of_chlorine_monoxide_in_air - - northward_water_vapor_flux_in_air + + mole_fraction_of_chlorine_dioxide_in_air - - lwe_stratiform_snowfall_rate + + mass_fraction_of_ozone_in_air - - stratiform_snowfall_amount + + mass_fraction_of_convective_cloud_condensed_water_in_air - - sea_surface_wind_wave_period + + sea_surface_swell_wave_period - - omnidirectional_spherical_irradiance_per_unit_wavelength_in_sea_water + + surface_drag_coefficient_in_air + + + + mass_content_of_cloud_condensed_water_in_atmosphere_layer + + + + mole_concentration_of_organic_detritus_expressed_as_silicon_in_sea_water + + + + mole_concentration_of_organic_detritus_expressed_as_nitrogen_in_sea_water @@ -33347,98 +33692,6 @@ tendency_of_atmosphere_moles_of_methyl_chloride - - tendency_of_middle_atmosphere_moles_of_molecular_hydrogen - - - - tendency_of_middle_atmosphere_moles_of_methyl_chloride - - - - tendency_of_middle_atmosphere_moles_of_methane - - - - sea_water_y_velocity - - - - sea_water_x_velocity - - - - mole_fraction_of_hypochlorous_acid_in_air - - - - tendency_of_troposphere_moles_of_molecular_hydrogen - - - - tendency_of_troposphere_moles_of_methyl_chloride - - - - mass_content_of_water_vapor_in_atmosphere_layer - - - - mass_content_of_water_in_atmosphere_layer - - - - tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_turbulence - - - - tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_deep_convection - - - - tendency_of_troposphere_moles_of_methyl_bromide - - - - tendency_of_mass_content_of_water_vapor_in_atmosphere_layer_due_to_convection - - - - tendency_of_atmosphere_mass_content_of_water_vapor_due_to_shallow_convection - - - - radiation_wavelength - - - - tendency_of_troposphere_moles_of_methane - - - - tendency_of_atmosphere_mass_content_of_water_due_to_advection - - - - mole_fraction_of_chlorine_monoxide_in_air - - - - mole_fraction_of_chlorine_dioxide_in_air - - - - mass_content_of_cloud_condensed_water_in_atmosphere_layer - - - - mole_concentration_of_organic_detritus_expressed_as_silicon_in_sea_water - - - - mole_concentration_of_organic_detritus_expressed_as_nitrogen_in_sea_water - - surface_drag_coefficient_for_momentum_in_air diff --git a/lib/iris/_merge.py b/lib/iris/_merge.py index 6758e9f55d..bc12080523 100644 --- a/lib/iris/_merge.py +++ b/lib/iris/_merge.py @@ -1809,7 +1809,8 @@ def key_func(coord): # Order the coordinates by hints, axis, and definition. for coord in sorted(coords, key=key_func): - if not cube.coord_dims(coord) and coord.shape == (1,): + dims = tuple(cube.coord_dims(coord)) + if not dims and coord.shape == (1,): # Extract the scalar coordinate data and metadata. scalar_defns.append(coord.metadata) # Because we know there's a single Cell in the @@ -1834,11 +1835,11 @@ def key_func(coord): # Extract the vector coordinate and metadata. if id(coord) in cube_aux_coord_ids: vector_aux_coords_and_dims.append( - _CoordAndDims(coord, tuple(cube.coord_dims(coord))) + _CoordAndDims(coord, dims) ) else: vector_dim_coords_and_dims.append( - _CoordAndDims(coord, tuple(cube.coord_dims(coord))) + _CoordAndDims(coord, dims) ) factory_defns = [] diff --git a/lib/iris/_representation/cube_summary.py b/lib/iris/_representation/cube_summary.py index 68f86832f5..1e78a92fd1 100644 --- a/lib/iris/_representation/cube_summary.py +++ b/lib/iris/_representation/cube_summary.py @@ -207,7 +207,7 @@ def __init__(self, title, attributes): self.values = [] self.contents = [] for name, value in sorted(attributes.items()): - value = value_repr(value) + value = value_repr(value, quote_strings=True) value = iris.util.clip_string(value) self.names.append(name) self.values.append(value) diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index dc8ad78080..b1a9e1d259 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -1394,7 +1394,7 @@ def _lazy_rms(array, axis, **kwargs): # all. Thus trying to use this aggregator with weights will currently # raise an error in dask due to the unexpected keyword `weights`, # rather than silently returning the wrong answer. - return da.sqrt(da.mean(array ** 2, axis=axis, **kwargs)) + return da.sqrt(da.mean(array**2, axis=axis, **kwargs)) @_build_dask_mdtol_function @@ -2170,15 +2170,20 @@ def _compute_groupby_coords(self): def _compute_shared_coords(self): """Create the new shared coordinates given the group slices.""" + groupby_indices = [] groupby_bounds = [] - # Iterate over the ordered dictionary in order to construct - # a list of tuple group boundary indexes. + # Iterate over the ordered dictionary in order to construct a list of + # tuple group indices, and a list of the respective bounds of those + # indices. for key_slice in self._slices_by_key.values(): if isinstance(key_slice, tuple): - groupby_bounds.append((key_slice[0], key_slice[-1])) + indices = key_slice else: - groupby_bounds.append((key_slice.start, key_slice.stop - 1)) + indices = tuple(range(*key_slice.indices(self._stop))) + + groupby_indices.append(indices) + groupby_bounds.append((indices[0], indices[-1])) # Create new shared bounded coordinates. for coord, dim in self._shared_coords: @@ -2197,15 +2202,9 @@ def _compute_shared_coords(self): new_shape += shape[:-1] work_arr = work_arr.reshape(work_shape) - for key_slice in self._slices_by_key.values(): - if isinstance(key_slice, slice): - indices = key_slice.indices( - coord.points.shape[dim] - ) - key_slice = range(*indices) - + for indices in groupby_indices: for arr in work_arr: - new_points.append("|".join(arr.take(key_slice))) + new_points.append("|".join(arr.take(indices))) # Reinstate flattened dimensions. Aggregated dim now leads. new_points = np.array(new_points).reshape(new_shape) @@ -2220,48 +2219,54 @@ def _compute_shared_coords(self): raise ValueError(msg) else: new_bounds = [] + if coord.has_bounds(): + # Derive new coord's bounds from bounds. + item = coord.bounds + maxmin_axis = (dim, -1) + first_choices = coord.bounds.take(0, -1) + last_choices = coord.bounds.take(1, -1) + else: + # Derive new coord's bounds from points. + item = coord.points + maxmin_axis = dim + first_choices = last_choices = coord.points + + # Check whether item is monotonic along the dimension of interest. + deltas = np.diff(item, 1, dim) + monotonic = np.all(deltas >= 0) or np.all(deltas <= 0) # Construct list of coordinate group boundary pairs. - for start, stop in groupby_bounds: - if coord.has_bounds(): - # Collapse group bounds into bounds. + if monotonic: + # Use first and last bound or point for new bounds. + for start, stop in groupby_bounds: if ( getattr(coord, "circular", False) - and (stop + 1) == coord.shape[dim] + and (stop + 1) == self._stop ): new_bounds.append( [ - coord.bounds.take(start, dim).take(0, -1), - coord.bounds.take(0, dim).take(0, -1) - + coord.units.modulus, - ] - ) - else: - new_bounds.append( - [ - coord.bounds.take(start, dim).take(0, -1), - coord.bounds.take(stop, dim).take(1, -1), - ] - ) - else: - # Collapse group points into bounds. - if getattr(coord, "circular", False) and ( - stop + 1 - ) == len(coord.points): - new_bounds.append( - [ - coord.points.take(start, dim), - coord.points.take(0, dim) + first_choices.take(start, dim), + first_choices.take(0, dim) + coord.units.modulus, ] ) else: new_bounds.append( [ - coord.points.take(start, dim), - coord.points.take(stop, dim), + first_choices.take(start, dim), + last_choices.take(stop, dim), ] ) + else: + # Use min and max bound or point for new bounds. + for indices in groupby_indices: + item_slice = item.take(indices, dim) + new_bounds.append( + [ + item_slice.min(axis=maxmin_axis), + item_slice.max(axis=maxmin_axis), + ] + ) # Bounds needs to be an array with the length 2 start-stop # dimension last, and the aggregated dimension back in its @@ -2686,14 +2691,6 @@ class UnstructuredNearest: .. Note:: Currently only supports regridding, not interpolation. - .. Note:: - This scheme performs essentially the same job as - :class:`iris.experimental.regrid.ProjectedUnstructuredNearest`. - That scheme is faster, but only works well on data in a limited - region of the globe, covered by a specified projection. - This approach is more rigorously correct and can be applied to global - datasets. - """ # Note: the argument requirements are simply those of the underlying @@ -2764,6 +2761,9 @@ class PointInCell: This class describes the point-in-cell regridding scheme for use typically with :meth:`iris.cube.Cube.regrid()`. + Each result datapoint is an average over all source points that fall inside + that (bounded) target cell. + The PointInCell regridder can regrid data from a source grid of any dimensionality and in any coordinate system. The location of each source point is specified by X and Y coordinates @@ -2781,8 +2781,12 @@ class PointInCell: def __init__(self, weights=None): """ - Point-in-cell regridding scheme suitable for regridding over one - or more orthogonal coordinates. + Point-in-cell regridding scheme suitable for regridding from a source + cube with X and Y coordinates all on the same dimensions, to a target + cube with bounded X and Y coordinates on separate X and Y dimensions. + + Each result datapoint is an average over all source points that fall + inside that (bounded) target cell. Optional Args: diff --git a/lib/iris/analysis/_area_weighted.py b/lib/iris/analysis/_area_weighted.py index ae162f6c53..8381185e58 100644 --- a/lib/iris/analysis/_area_weighted.py +++ b/lib/iris/analysis/_area_weighted.py @@ -3,8 +3,18 @@ # 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. +import functools +import cf_units +import numpy as np +import numpy.ma as ma + +from iris._lazy_data import map_complete_blocks from iris.analysis._interpolation import get_xy_dim_coords, snapshot_grid +from iris.analysis._regrid import RectilinearRegridder +import iris.analysis.cartography +import iris.coord_systems +from iris.util import _meshgrid class AreaWeightedRegridder: @@ -43,10 +53,6 @@ def __init__(self, src_grid_cube, target_grid_cube, mdtol=1): the same coordinate system. """ - from iris.experimental.regrid import ( - _regrid_area_weighted_rectilinear_src_and_grid__prepare, - ) - # Snapshot the state of the source cube to ensure that the regridder is # impervious to external changes to the original cubes. self._src_grid = snapshot_grid(src_grid_cube) @@ -103,10 +109,6 @@ def __call__(self, cube): in the horizontal dimensions will be combined before regridding. """ - from iris.experimental.regrid import ( - _regrid_area_weighted_rectilinear_src_and_grid__perform, - ) - src_x, src_y = get_xy_dim_coords(cube) if (src_x, src_y) != self._src_grid: raise ValueError( @@ -130,3 +132,1007 @@ def __call__(self, cube): return _regrid_area_weighted_rectilinear_src_and_grid__perform( cube, _regrid_info, mdtol=self._mdtol ) + + +# +# Support routines, all originally in iris.experimental.regrid +# + + +def _get_xy_coords(cube): + """ + Return the x and y coordinates from a cube. + + This function will preferentially return a pair of dimension + coordinates (if there are more than one potential x or y dimension + coordinates a ValueError will be raised). If the cube does not have + a pair of x and y dimension coordinates it will return 1D auxiliary + coordinates (including scalars). If there is not one and only one set + of x and y auxiliary coordinates a ValueError will be raised. + + Having identified the x and y coordinates, the function checks that they + have equal coordinate systems and that they do not occupy the same + dimension on the cube. + + Args: + + * cube: + An instance of :class:`iris.cube.Cube`. + + Returns: + A tuple containing the cube's x and y coordinates. + + """ + # Look for a suitable dimension coords first. + x_coords = cube.coords(axis="x", dim_coords=True) + if not x_coords: + # If there is no x coord in dim_coords look for scalars or + # monotonic coords in aux_coords. + x_coords = [ + coord + for coord in cube.coords(axis="x", dim_coords=False) + if coord.ndim == 1 and coord.is_monotonic() + ] + if len(x_coords) != 1: + raise ValueError( + "Cube {!r} must contain a single 1D x " + "coordinate.".format(cube.name()) + ) + x_coord = x_coords[0] + + # Look for a suitable dimension coords first. + y_coords = cube.coords(axis="y", dim_coords=True) + if not y_coords: + # If there is no y coord in dim_coords look for scalars or + # monotonic coords in aux_coords. + y_coords = [ + coord + for coord in cube.coords(axis="y", dim_coords=False) + if coord.ndim == 1 and coord.is_monotonic() + ] + if len(y_coords) != 1: + raise ValueError( + "Cube {!r} must contain a single 1D y " + "coordinate.".format(cube.name()) + ) + y_coord = y_coords[0] + + if x_coord.coord_system != y_coord.coord_system: + raise ValueError( + "The cube's x ({!r}) and y ({!r}) " + "coordinates must have the same coordinate " + "system.".format(x_coord.name(), y_coord.name()) + ) + + # The x and y coordinates must describe different dimensions + # or be scalar coords. + x_dims = cube.coord_dims(x_coord) + x_dim = None + if x_dims: + x_dim = x_dims[0] + + y_dims = cube.coord_dims(y_coord) + y_dim = None + if y_dims: + y_dim = y_dims[0] + + if x_dim is not None and y_dim == x_dim: + raise ValueError( + "The cube's x and y coords must not describe the " + "same data dimension." + ) + + return x_coord, y_coord + + +def _within_bounds(src_bounds, tgt_bounds, orderswap=False): + """ + Determine which target bounds lie within the extremes of the source bounds. + + Args: + + * src_bounds (ndarray): + An (n, 2) shaped array of monotonic contiguous source bounds. + * tgt_bounds (ndarray): + An (n, 2) shaped array corresponding to the target bounds. + + Kwargs: + + * orderswap (bool): + A Boolean indicating whether the target bounds are in descending order + (True). Defaults to False. + + Returns: + Boolean ndarray, indicating whether each target bound is within the + extremes of the source bounds. + + """ + min_bound = np.min(src_bounds) - 1e-14 + max_bound = np.max(src_bounds) + 1e-14 + + # Swap upper-lower is necessary. + if orderswap is True: + upper, lower = tgt_bounds.T + else: + lower, upper = tgt_bounds.T + + return ((lower <= max_bound) * (lower >= min_bound)) * ( + (upper <= max_bound) * (upper >= min_bound) + ) + + +def _cropped_bounds(bounds, lower, upper): + """ + Return a new bounds array and corresponding slice object (or indices) of + the original data array, resulting from cropping the provided bounds + between the specified lower and upper values. The bounds at the + extremities will be truncated so that they start and end with lower and + upper. + + This function will return an empty NumPy array and slice if there is no + overlap between the region covered by bounds and the region from lower to + upper. + + If lower > upper the resulting bounds may not be contiguous and the + indices object will be a tuple of indices rather than a slice object. + + Args: + + * bounds: + An (n, 2) shaped array of monotonic contiguous bounds. + * lower: + Lower bound at which to crop the bounds array. + * upper: + Upper bound at which to crop the bounds array. + + Returns: + A tuple of the new bounds array and the corresponding slice object or + indices from the zeroth axis of the original array. + + """ + reversed_flag = False + # Ensure order is increasing. + if bounds[0, 0] > bounds[-1, 0]: + # Reverse bounds + bounds = bounds[::-1, ::-1] + reversed_flag = True + + # Number of bounds. + n = bounds.shape[0] + + if lower <= upper: + if lower > bounds[-1, 1] or upper < bounds[0, 0]: + new_bounds = bounds[0:0] + indices = slice(0, 0) + else: + # A single region lower->upper. + if lower < bounds[0, 0]: + # Region extends below bounds so use first lower bound. + lindex = 0 + lower = bounds[0, 0] + else: + # Index of last lower bound less than or equal to lower. + lindex = np.nonzero(bounds[:, 0] <= lower)[0][-1] + if upper > bounds[-1, 1]: + # Region extends above bounds so use last upper bound. + uindex = n - 1 + upper = bounds[-1, 1] + else: + # Index of first upper bound greater than or equal to + # upper. + uindex = np.nonzero(bounds[:, 1] >= upper)[0][0] + # Extract the bounds in our region defined by lower->upper. + new_bounds = np.copy(bounds[lindex : (uindex + 1), :]) + # Replace first and last values with specified bounds. + new_bounds[0, 0] = lower + new_bounds[-1, 1] = upper + if reversed_flag: + indices = slice(n - (uindex + 1), n - lindex) + else: + indices = slice(lindex, uindex + 1) + else: + # Two regions [0]->upper, lower->[-1] + # [0]->upper + if upper < bounds[0, 0]: + # Region outside src bounds. + new_bounds_left = bounds[0:0] + indices_left = tuple() + slice_left = slice(0, 0) + else: + if upper > bounds[-1, 1]: + # Whole of bounds. + uindex = n - 1 + upper = bounds[-1, 1] + else: + # Index of first upper bound greater than or equal to upper. + uindex = np.nonzero(bounds[:, 1] >= upper)[0][0] + # Extract the bounds in our region defined by [0]->upper. + new_bounds_left = np.copy(bounds[0 : (uindex + 1), :]) + # Replace last value with specified bound. + new_bounds_left[-1, 1] = upper + if reversed_flag: + indices_left = tuple(range(n - (uindex + 1), n)) + slice_left = slice(n - (uindex + 1), n) + else: + indices_left = tuple(range(0, uindex + 1)) + slice_left = slice(0, uindex + 1) + # lower->[-1] + if lower > bounds[-1, 1]: + # Region is outside src bounds. + new_bounds_right = bounds[0:0] + indices_right = tuple() + slice_right = slice(0, 0) + else: + if lower < bounds[0, 0]: + # Whole of bounds. + lindex = 0 + lower = bounds[0, 0] + else: + # Index of last lower bound less than or equal to lower. + lindex = np.nonzero(bounds[:, 0] <= lower)[0][-1] + # Extract the bounds in our region defined by lower->[-1]. + new_bounds_right = np.copy(bounds[lindex:, :]) + # Replace first value with specified bound. + new_bounds_right[0, 0] = lower + if reversed_flag: + indices_right = tuple(range(0, n - lindex)) + slice_right = slice(0, n - lindex) + else: + indices_right = tuple(range(lindex, n)) + slice_right = slice(lindex, None) + + if reversed_flag: + # Flip everything around. + indices_left, indices_right = indices_right, indices_left + slice_left, slice_right = slice_right, slice_left + + # Combine regions. + new_bounds = np.concatenate((new_bounds_left, new_bounds_right)) + # Use slices if possible, but if we have two regions use indices. + if indices_left and indices_right: + indices = indices_left + indices_right + elif indices_left: + indices = slice_left + elif indices_right: + indices = slice_right + else: + indices = slice(0, 0) + + if reversed_flag: + new_bounds = new_bounds[::-1, ::-1] + + return new_bounds, indices + + +def _cartesian_area(y_bounds, x_bounds): + """ + Return an array of the areas of each cell given two arrays + of cartesian bounds. + + Args: + + * y_bounds: + An (n, 2) shaped NumPy array. + * x_bounds: + An (m, 2) shaped NumPy array. + + Returns: + An (n, m) shaped Numpy array of areas. + + """ + heights = y_bounds[:, 1] - y_bounds[:, 0] + widths = x_bounds[:, 1] - x_bounds[:, 0] + return np.abs(np.outer(heights, widths)) + + +def _spherical_area(y_bounds, x_bounds, radius=1.0): + """ + Return an array of the areas of each cell on a sphere + given two arrays of latitude and longitude bounds in radians. + + Args: + + * y_bounds: + An (n, 2) shaped NumPy array of latitide bounds in radians. + * x_bounds: + An (m, 2) shaped NumPy array of longitude bounds in radians. + * radius: + Radius of the sphere. Default is 1.0. + + Returns: + An (n, m) shaped Numpy array of areas. + + """ + return iris.analysis.cartography._quadrant_area(y_bounds, x_bounds, radius) + + +def _get_bounds_in_units(coord, units, dtype): + """Return a copy of coord's bounds in the specified units and dtype.""" + # The bounds are cast to dtype before conversion to prevent issues when + # mixing float32 and float64 types. + return coord.units.convert(coord.bounds.astype(dtype), units).astype(dtype) + + +def _weighted_mean_with_mdtol(data, weights, axis=None, mdtol=0): + """ + Return the weighted mean of an array over the specified axis + using the provided weights (if any) and a permitted fraction of + masked data. + + Args: + + * data (array-like): + Data to be averaged. + + * weights (array-like): + An array of the same shape as the data that specifies the contribution + of each corresponding data element to the calculated mean. + + Kwargs: + + * axis (int or tuple of ints): + Axis along which the mean is computed. The default is to compute + the mean of the flattened array. + + * mdtol (float): + Tolerance of missing data. The value returned in each element of the + returned array will be masked if the fraction of masked data exceeds + mdtol. This fraction is weighted by the `weights` array if one is + provided. mdtol=0 means no missing data is tolerated + while mdtol=1 will mean the resulting element will be masked if and + only if all the contributing elements of data are masked. + Defaults to 0. + + Returns: + Numpy array (possibly masked) or scalar. + + """ + if ma.is_masked(data): + res, unmasked_weights_sum = ma.average( + data, weights=weights, axis=axis, returned=True + ) + if mdtol < 1: + weights_sum = weights.sum(axis=axis) + frac_masked = 1 - np.true_divide(unmasked_weights_sum, weights_sum) + mask_pt = frac_masked > mdtol + if np.any(mask_pt) and not isinstance(res, ma.core.MaskedConstant): + if np.isscalar(res): + res = ma.masked + elif ma.isMaskedArray(res): + res.mask |= mask_pt + else: + res = ma.masked_array(res, mask=mask_pt) + else: + res = np.average(data, weights=weights, axis=axis) + return res + + +def _regrid_area_weighted_array( + src_data, x_dim, y_dim, weights_info, index_info, mdtol=0 +): + """ + Regrid the given data from its source grid to a new grid using + an area weighted mean to determine the resulting data values. + + .. note:: + + Elements in the returned array that lie either partially + or entirely outside of the extent of the source grid will + be masked irrespective of the value of mdtol. + + Args: + + * src_data: + An N-dimensional NumPy array. + * x_dim: + The X dimension within `src_data`. + * y_dim: + The Y dimension within `src_data`. + * weights_info: + The area weights information to be used for area-weighted + regridding. + + Kwargs: + + * mdtol: + Tolerance of missing data. The value returned in each element of the + returned array will be masked if the fraction of missing data exceeds + mdtol. This fraction is calculated based on the area of masked cells + within each target cell. mdtol=0 means no missing data is tolerated + while mdtol=1 will mean the resulting element will be masked if and + only if all the overlapping elements of the source grid are masked. + Defaults to 0. + + Returns: + The regridded data as an N-dimensional NumPy array. The lengths + of the X and Y dimensions will now match those of the target + grid. + + """ + ( + blank_weights, + src_area_weights, + new_data_mask_basis, + ) = weights_info + + ( + result_x_extent, + result_y_extent, + square_data_indices_y, + square_data_indices_x, + src_area_datas_required, + ) = index_info + + # Ensure we have x_dim and y_dim. + x_dim_orig = x_dim + y_dim_orig = y_dim + if y_dim is None: + src_data = np.expand_dims(src_data, axis=src_data.ndim) + y_dim = src_data.ndim - 1 + if x_dim is None: + src_data = np.expand_dims(src_data, axis=src_data.ndim) + x_dim = src_data.ndim - 1 + # Move y_dim and x_dim to last dimensions + if not x_dim == src_data.ndim - 1: + src_data = np.moveaxis(src_data, x_dim, -1) + if not y_dim == src_data.ndim - 2: + if x_dim < y_dim: + # note: y_dim was shifted along by one position when + # x_dim was moved to the last dimension + src_data = np.moveaxis(src_data, y_dim - 1, -2) + elif x_dim > y_dim: + src_data = np.moveaxis(src_data, y_dim, -2) + x_dim = src_data.ndim - 1 + y_dim = src_data.ndim - 2 + + # Create empty "pre-averaging" data array that will enable the + # src_data data coresponding to a given target grid point, + # to be stacked per point. + # Note that dtype is not preserved and that the array mask + # allows for regions that do not overlap. + new_shape = list(src_data.shape) + new_shape[x_dim] = result_x_extent + new_shape[y_dim] = result_y_extent + + # Use input cube dtype or convert values to the smallest possible float + # dtype when necessary. + dtype = np.promote_types(src_data.dtype, np.float16) + + # Axes of data over which the weighted mean is calculated. + axis = (y_dim, x_dim) + + # Use previously established indices + + src_area_datas_square = src_data[ + ..., square_data_indices_y, square_data_indices_x + ] + + _, src_area_datas_required = np.broadcast_arrays( + src_area_datas_square, src_area_datas_required + ) + + src_area_datas = np.where( + src_area_datas_required, src_area_datas_square, 0 + ) + + # Flag to indicate whether the original data was a masked array. + src_masked = src_data.mask.any() if ma.isMaskedArray(src_data) else False + if src_masked: + src_area_masks_square = src_data.mask[ + ..., square_data_indices_y, square_data_indices_x + ] + src_area_masks = np.where( + src_area_datas_required, src_area_masks_square, True + ) + + else: + # If the weights were originally blank, set the weights to all 1 to + # avoid divide by 0 error and set the new data mask for making the + # values 0 + src_area_weights = np.where(blank_weights, 1, src_area_weights) + + new_data_mask = np.broadcast_to(new_data_mask_basis, new_shape) + + # Broadcast the weights array to allow numpy's ma.average + # to be called. + # Assign new shape to raise error on copy. + src_area_weights.shape = src_area_datas.shape[-3:] + # Broadcast weights to match shape of data. + _, src_area_weights = np.broadcast_arrays(src_area_datas, src_area_weights) + + # Mask the data points + if src_masked: + src_area_datas = np.ma.array(src_area_datas, mask=src_area_masks) + + # Calculate weighted mean taking into account missing data. + new_data = _weighted_mean_with_mdtol( + src_area_datas, weights=src_area_weights, axis=axis, mdtol=mdtol + ) + new_data = new_data.reshape(new_shape) + if src_masked: + new_data_mask = new_data.mask + + # Mask the data if originally masked or if the result has masked points + if ma.isMaskedArray(src_data): + new_data = ma.array( + new_data, + mask=new_data_mask, + fill_value=src_data.fill_value, + dtype=dtype, + ) + elif new_data_mask.any(): + new_data = ma.array(new_data, mask=new_data_mask, dtype=dtype) + else: + new_data = new_data.astype(dtype) + + # Restore data to original form + if x_dim_orig is None and y_dim_orig is None: + new_data = np.squeeze(new_data, axis=x_dim) + new_data = np.squeeze(new_data, axis=y_dim) + elif y_dim_orig is None: + new_data = np.squeeze(new_data, axis=y_dim) + new_data = np.moveaxis(new_data, -1, x_dim_orig) + elif x_dim_orig is None: + new_data = np.squeeze(new_data, axis=x_dim) + new_data = np.moveaxis(new_data, -1, y_dim_orig) + elif x_dim_orig < y_dim_orig: + # move the x_dim back first, so that the y_dim will + # then be moved to its original position + new_data = np.moveaxis(new_data, -1, x_dim_orig) + new_data = np.moveaxis(new_data, -1, y_dim_orig) + else: + # move the y_dim back first, so that the x_dim will + # then be moved to its original position + new_data = np.moveaxis(new_data, -2, y_dim_orig) + new_data = np.moveaxis(new_data, -1, x_dim_orig) + + return new_data + + +def _regrid_area_weighted_rectilinear_src_and_grid__prepare( + src_cube, grid_cube +): + """ + First (setup) part of 'regrid_area_weighted_rectilinear_src_and_grid'. + + Check inputs and calculate related info. The 'regrid info' returned + can be re-used over many 2d slices. + + """ + # Get the 1d monotonic (or scalar) src and grid coordinates. + src_x, src_y = _get_xy_coords(src_cube) + grid_x, grid_y = _get_xy_coords(grid_cube) + + # Condition 1: All x and y coordinates must have contiguous bounds to + # define areas. + if ( + not src_x.is_contiguous() + or not src_y.is_contiguous() + or not grid_x.is_contiguous() + or not grid_y.is_contiguous() + ): + raise ValueError( + "The horizontal grid coordinates of both the source " + "and grid cubes must have contiguous bounds." + ) + + # Condition 2: Everything must have the same coordinate system. + src_cs = src_x.coord_system + grid_cs = grid_x.coord_system + if src_cs != grid_cs: + raise ValueError( + "The horizontal grid coordinates of both the source " + "and grid cubes must have the same coordinate " + "system." + ) + + # Condition 3: cannot create vector coords from scalars. + src_x_dims = src_cube.coord_dims(src_x) + src_x_dim = None + if src_x_dims: + src_x_dim = src_x_dims[0] + src_y_dims = src_cube.coord_dims(src_y) + src_y_dim = None + if src_y_dims: + src_y_dim = src_y_dims[0] + if ( + src_x_dim is None + and grid_x.shape[0] != 1 + or src_y_dim is None + and grid_y.shape[0] != 1 + ): + raise ValueError( + "The horizontal grid coordinates of source cube " + "includes scalar coordinates, but the new grid does " + "not. The new grid must not require additional data " + "dimensions to be created." + ) + + # Determine whether to calculate flat or spherical areas. + # Don't only rely on coord system as it may be None. + spherical = ( + isinstance( + src_cs, + (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS), + ) + or src_x.units == "degrees" + or src_x.units == "radians" + ) + + # Get src and grid bounds in the same units. + x_units = cf_units.Unit("radians") if spherical else src_x.units + y_units = cf_units.Unit("radians") if spherical else src_y.units + + # Operate in highest precision. + src_dtype = np.promote_types(src_x.bounds.dtype, src_y.bounds.dtype) + grid_dtype = np.promote_types(grid_x.bounds.dtype, grid_y.bounds.dtype) + dtype = np.promote_types(src_dtype, grid_dtype) + + src_x_bounds = _get_bounds_in_units(src_x, x_units, dtype) + src_y_bounds = _get_bounds_in_units(src_y, y_units, dtype) + grid_x_bounds = _get_bounds_in_units(grid_x, x_units, dtype) + grid_y_bounds = _get_bounds_in_units(grid_y, y_units, dtype) + + # Create 2d meshgrids as required by _create_cube func. + meshgrid_x, meshgrid_y = _meshgrid(grid_x.points, grid_y.points) + + # Determine whether target grid bounds are decreasing. This must + # be determined prior to wrap_lons being called. + grid_x_decreasing = grid_x_bounds[-1, 0] < grid_x_bounds[0, 0] + grid_y_decreasing = grid_y_bounds[-1, 0] < grid_y_bounds[0, 0] + + # Wrapping of longitudes. + if spherical: + base = np.min(src_x_bounds) + modulus = x_units.modulus + # Only wrap if necessary to avoid introducing floating + # point errors. + if np.min(grid_x_bounds) < base or np.max(grid_x_bounds) > ( + base + modulus + ): + grid_x_bounds = iris.analysis.cartography.wrap_lons( + grid_x_bounds, base, modulus + ) + + # Determine whether the src_x coord has periodic boundary conditions. + circular = getattr(src_x, "circular", False) + + # Use simple cartesian area function or one that takes into + # account the curved surface if coord system is spherical. + if spherical: + area_func = _spherical_area + else: + area_func = _cartesian_area + + def _calculate_regrid_area_weighted_weights( + src_x_bounds, + src_y_bounds, + grid_x_bounds, + grid_y_bounds, + grid_x_decreasing, + grid_y_decreasing, + area_func, + circular=False, + ): + """ + Compute the area weights used for area-weighted regridding. + Args: + * src_x_bounds: + A NumPy array of bounds along the X axis defining the source grid. + * src_y_bounds: + A NumPy array of bounds along the Y axis defining the source grid. + * grid_x_bounds: + A NumPy array of bounds along the X axis defining the new grid. + * grid_y_bounds: + A NumPy array of bounds along the Y axis defining the new grid. + * grid_x_decreasing: + Boolean indicating whether the X coordinate of the new grid is + in descending order. + * grid_y_decreasing: + Boolean indicating whether the Y coordinate of the new grid is + in descending order. + * area_func: + A function that returns an (p, q) array of weights given an (p, 2) + shaped array of Y bounds and an (q, 2) shaped array of X bounds. + Kwargs: + * circular: + A boolean indicating whether the `src_x_bounds` are periodic. + Default is False. + Returns: + The area weights to be used for area-weighted regridding. + """ + # Determine which grid bounds are within src extent. + y_within_bounds = _within_bounds( + src_y_bounds, grid_y_bounds, grid_y_decreasing + ) + x_within_bounds = _within_bounds( + src_x_bounds, grid_x_bounds, grid_x_decreasing + ) + + # Cache which src_bounds are within grid bounds + cached_x_bounds = [] + cached_x_indices = [] + max_x_indices = 0 + for (x_0, x_1) in grid_x_bounds: + if grid_x_decreasing: + x_0, x_1 = x_1, x_0 + x_bounds, x_indices = _cropped_bounds(src_x_bounds, x_0, x_1) + cached_x_bounds.append(x_bounds) + cached_x_indices.append(x_indices) + # Keep record of the largest slice + if isinstance(x_indices, slice): + x_indices_size = np.sum(x_indices.stop - x_indices.start) + else: # is tuple of indices + x_indices_size = len(x_indices) + if x_indices_size > max_x_indices: + max_x_indices = x_indices_size + + # Cache which y src_bounds areas and weights are within grid bounds + cached_y_indices = [] + cached_weights = [] + max_y_indices = 0 + for j, (y_0, y_1) in enumerate(grid_y_bounds): + # Reverse lower and upper if dest grid is decreasing. + if grid_y_decreasing: + y_0, y_1 = y_1, y_0 + y_bounds, y_indices = _cropped_bounds(src_y_bounds, y_0, y_1) + cached_y_indices.append(y_indices) + # Keep record of the largest slice + if isinstance(y_indices, slice): + y_indices_size = np.sum(y_indices.stop - y_indices.start) + else: # is tuple of indices + y_indices_size = len(y_indices) + if y_indices_size > max_y_indices: + max_y_indices = y_indices_size + + weights_i = [] + for i, (x_0, x_1) in enumerate(grid_x_bounds): + # Reverse lower and upper if dest grid is decreasing. + if grid_x_decreasing: + x_0, x_1 = x_1, x_0 + x_bounds = cached_x_bounds[i] + x_indices = cached_x_indices[i] + + # Determine whether element i, j overlaps with src and hence + # an area weight should be computed. + # If x_0 > x_1 then we want [0]->x_1 and x_0->[0] + mod in the case + # of wrapped longitudes. However if the src grid is not global + # (i.e. circular) this new cell would include a region outside of + # the extent of the src grid and thus the weight is therefore + # invalid. + outside_extent = x_0 > x_1 and not circular + if ( + outside_extent + or not y_within_bounds[j] + or not x_within_bounds[i] + ): + weights = False + else: + # Calculate weights based on areas of cropped bounds. + if isinstance(x_indices, tuple) and isinstance( + y_indices, tuple + ): + raise RuntimeError( + "Cannot handle split bounds " "in both x and y." + ) + weights = area_func(y_bounds, x_bounds) + weights_i.append(weights) + cached_weights.append(weights_i) + return ( + tuple(cached_x_indices), + tuple(cached_y_indices), + max_x_indices, + max_y_indices, + tuple(cached_weights), + ) + + ( + cached_x_indices, + cached_y_indices, + max_x_indices, + max_y_indices, + cached_weights, + ) = _calculate_regrid_area_weighted_weights( + src_x_bounds, + src_y_bounds, + grid_x_bounds, + grid_y_bounds, + grid_x_decreasing, + grid_y_decreasing, + area_func, + circular, + ) + + # Go further, calculating the full weights array that we'll need in the + # perform step and the indices we'll need to extract from the cube we're + # regridding (src_data) + + result_y_extent = len(grid_y_bounds) + result_x_extent = len(grid_x_bounds) + + # Total number of points + num_target_pts = result_y_extent * result_x_extent + + # Create empty array to hold weights + src_area_weights = np.zeros( + list((max_y_indices, max_x_indices, num_target_pts)) + ) + + # Built for the case where the source cube isn't masked + blank_weights = np.zeros((num_target_pts,)) + new_data_mask_basis = np.full( + (len(cached_y_indices), len(cached_x_indices)), False, dtype=np.bool_ + ) + + # To permit fancy indexing, we need to store our data in an array whose + # first two dimensions represent the indices needed for the target cell. + # Since target cells can require a different number of indices, the size of + # these dimensions should be the maximum of this number. + # This means we need to track whether the data in + # that array is actually required and build those squared-off arrays + # TODO: Consider if a proper mask would be better + src_area_datas_required = np.full( + (max_y_indices, max_x_indices, num_target_pts), False + ) + square_data_indices_y = np.zeros( + (max_y_indices, max_x_indices, num_target_pts), dtype=int + ) + square_data_indices_x = np.zeros( + (max_y_indices, max_x_indices, num_target_pts), dtype=int + ) + + # Stack the weights for each target point and build the indices we'll need + # to extract the src_area_data + target_pt_ji = -1 + for j, y_indices in enumerate(cached_y_indices): + for i, x_indices in enumerate(cached_x_indices): + target_pt_ji += 1 + # Determine whether to mask element i, j based on whether + # there are valid weights. + weights = cached_weights[j][i] + if weights is False: + # Prepare for the src_data not being masked by storing the + # information that will let us fill the data with zeros and + # weights as one. The weighted average result will be the same, + # but we avoid dividing by zero. + blank_weights[target_pt_ji] = True + new_data_mask_basis[j, i] = True + else: + # Establish which indices are actually in y_indices and x_indices + if isinstance(y_indices, slice): + y_indices = list( + range( + y_indices.start, + y_indices.stop, + y_indices.step or 1, + ) + ) + else: + y_indices = list(y_indices) + + if isinstance(x_indices, slice): + x_indices = list( + range( + x_indices.start, + x_indices.stop, + x_indices.step or 1, + ) + ) + else: + x_indices = list(x_indices) + + # For the weights, we just need the lengths of these as we're + # dropping them into a pre-made array + + len_y = len(y_indices) + len_x = len(x_indices) + + src_area_weights[0:len_y, 0:len_x, target_pt_ji] = weights + + # To build the indices for the source cube, we need equal + # shaped array so we pad with 0s and record the need to mask + # them in src_area_datas_required + padded_y_indices = y_indices + [0] * (max_y_indices - len_y) + padded_x_indices = x_indices + [0] * (max_x_indices - len_x) + + square_data_indices_y[..., target_pt_ji] = np.array( + padded_y_indices + )[:, np.newaxis] + square_data_indices_x[..., target_pt_ji] = padded_x_indices + + src_area_datas_required[0:len_y, 0:len_x, target_pt_ji] = True + + # Package up the return data + + weights_info = ( + blank_weights, + src_area_weights, + new_data_mask_basis, + ) + + index_info = ( + result_x_extent, + result_y_extent, + square_data_indices_y, + square_data_indices_x, + src_area_datas_required, + ) + + # Now return it + + return ( + src_x, + src_y, + src_x_dim, + src_y_dim, + grid_x, + grid_y, + meshgrid_x, + meshgrid_y, + weights_info, + index_info, + ) + + +def _regrid_area_weighted_rectilinear_src_and_grid__perform( + src_cube, regrid_info, mdtol +): + """ + Second (regrid) part of 'regrid_area_weighted_rectilinear_src_and_grid'. + + Perform the prepared regrid calculation on a single 2d cube. + + """ + ( + src_x, + src_y, + src_x_dim, + src_y_dim, + grid_x, + grid_y, + meshgrid_x, + meshgrid_y, + weights_info, + index_info, + ) = regrid_info + + # Calculate new data array for regridded cube. + regrid = functools.partial( + _regrid_area_weighted_array, + x_dim=src_x_dim, + y_dim=src_y_dim, + weights_info=weights_info, + index_info=index_info, + mdtol=mdtol, + ) + + new_data = map_complete_blocks( + src_cube, regrid, (src_y_dim, src_x_dim), meshgrid_x.shape + ) + + # Wrap up the data as a Cube. + regrid_callback = RectilinearRegridder._regrid + new_cube = RectilinearRegridder._create_cube( + new_data, + src_cube, + src_x_dim, + src_y_dim, + src_x, + src_y, + grid_x, + grid_y, + meshgrid_x, + meshgrid_y, + regrid_callback, + ) + + # Slice out any length 1 dimensions. + indices = [slice(None, None)] * new_data.ndim + if src_x_dim is not None and new_cube.shape[src_x_dim] == 1: + indices[src_x_dim] = 0 + if src_y_dim is not None and new_cube.shape[src_y_dim] == 1: + indices[src_y_dim] = 0 + if 0 in indices: + new_cube = new_cube[tuple(indices)] + + return new_cube diff --git a/lib/iris/analysis/_grid_angles.py b/lib/iris/analysis/_grid_angles.py index 127aec7c1e..0b52f54568 100644 --- a/lib/iris/analysis/_grid_angles.py +++ b/lib/iris/analysis/_grid_angles.py @@ -120,7 +120,7 @@ def _angle(p, q, r): mid_lons = np.deg2rad(q[0]) pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) - pr_norm = np.sqrt(np.sum(pr ** 2, axis=0)) + pr_norm = np.sqrt(np.sum(pr**2, axis=0)) pr_top = pr[1] * np.cos(mid_lons) - pr[0] * np.sin(mid_lons) index = pr_norm == 0 diff --git a/lib/iris/analysis/_scipy_interpolate.py b/lib/iris/analysis/_scipy_interpolate.py index c6b33c56a4..fc64249729 100644 --- a/lib/iris/analysis/_scipy_interpolate.py +++ b/lib/iris/analysis/_scipy_interpolate.py @@ -229,7 +229,7 @@ def compute_interp_weights(self, xi, method=None): xi_shape, method, indices, norm_distances, out_of_bounds = prepared # Allocate arrays for describing the sparse matrix. - n_src_values_per_result_value = 2 ** ndim + n_src_values_per_result_value = 2**ndim n_result_values = len(indices[0]) n_non_zero = n_result_values * n_src_values_per_result_value weights = np.ones(n_non_zero, dtype=norm_distances[0].dtype) diff --git a/lib/iris/analysis/calculus.py b/lib/iris/analysis/calculus.py index 409782f256..4630f47967 100644 --- a/lib/iris/analysis/calculus.py +++ b/lib/iris/analysis/calculus.py @@ -629,14 +629,10 @@ def curl(i_cube, j_cube, k_cube=None): # (d/dtheta (i_cube * sin(lat)) - d_j_cube_dphi) # phi_cmpt = 1/r * ( d/dr (r * j_cube) - d_k_cube_dtheta) # theta_cmpt = 1/r * ( 1/cos(lat) * d_k_cube_dphi - d/dr (r * i_cube) - if ( - y_coord.name() - not in [ - "latitude", - "grid_latitude", - ] - or x_coord.name() not in ["longitude", "grid_longitude"] - ): + if y_coord.name() not in [ + "latitude", + "grid_latitude", + ] or x_coord.name() not in ["longitude", "grid_longitude"]: raise ValueError( "Expecting latitude as the y coord and " "longitude as the x coord for spherical curl." diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index 373487af53..f704468e33 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -335,7 +335,7 @@ def _quadrant_area(radian_lat_bounds, radian_lon_bounds, radius_of_earth): raise ValueError("Bounds must be [n,2] array") # fill in a new array of areas - radius_sqr = radius_of_earth ** 2 + radius_sqr = radius_of_earth**2 radian_lat_64 = radian_lat_bounds.astype(np.float64) radian_lon_64 = radian_lon_bounds.astype(np.float64) @@ -1010,8 +1010,8 @@ def _transform_distance_vectors_tolerance_mask( # Squared magnitudes should be equal to one within acceptable tolerance. # A value of atol=2e-3 is used, which corresponds to a change in magnitude # of approximately 0.1%. - sqmag_1_0 = u_one_t ** 2 + v_zero_t ** 2 - sqmag_0_1 = u_zero_t ** 2 + v_one_t ** 2 + sqmag_1_0 = u_one_t**2 + v_zero_t**2 + sqmag_0_1 = u_zero_t**2 + v_one_t**2 mask = np.logical_not( np.logical_and( np.isclose(sqmag_1_0, ones, atol=2e-3), diff --git a/lib/iris/analysis/maths.py b/lib/iris/analysis/maths.py index 571a66b756..1cbc90cc60 100644 --- a/lib/iris/analysis/maths.py +++ b/lib/iris/analysis/maths.py @@ -19,6 +19,7 @@ import numpy as np from numpy import ma +from iris._deprecation import warn_deprecated import iris.analysis from iris.common import SERVICES, Resolve from iris.common.lenient import _lenient_client @@ -138,10 +139,35 @@ def intersection_of_cubes(cube, other_cube): An instance of :class:`iris.cube.Cube`. Returns: - A pair of :class:`iris.cube.Cube` instances in a tuple corresponding - to the original cubes restricted to their intersection. + A pair of :class:`iris.cube.Cube` instances in a tuple corresponding to + the original cubes restricted to their intersection. + + .. deprecated:: 3.2.0 + + Instead use :meth:`iris.cube.CubeList.extract_overlapping`. For example, + rather than calling + + .. code:: + + cube1, cube2 = intersection_of_cubes(cube1, cube2) + + replace with + + .. code:: + + cubes = CubeList([cube1, cube2]) + coords = ["latitude", "longitude"] # Replace with relevant coords + intersections = cubes.extract_overlapping(coords) + cube1, cube2 = (intersections[0], intersections[1]) """ + wmsg = ( + "iris.analysis.maths.intersection_of_cubes has been deprecated and will " + "be removed, please use iris.cube.CubeList.extract_overlapping " + "instead. See intersection_of_cubes docstring for more information." + ) + warn_deprecated(wmsg) + # Take references of the original cubes (which will be copied when # slicing later). new_cube_self = cube @@ -514,7 +540,7 @@ def power(data, out=None): return _math_op_common( cube, power, - cube.units ** exponent, + cube.units**exponent, new_dtype=new_dtype, in_place=in_place, ) diff --git a/lib/iris/analysis/stats.py b/lib/iris/analysis/stats.py index 89dde1818b..711e3c5bfb 100644 --- a/lib/iris/analysis/stats.py +++ b/lib/iris/analysis/stats.py @@ -168,10 +168,10 @@ def _ones_like(cube): covar = (s1 * s2).collapsed( corr_coords, iris.analysis.SUM, weights=weights_1, mdtol=mdtol ) - var_1 = (s1 ** 2).collapsed( + var_1 = (s1**2).collapsed( corr_coords, iris.analysis.SUM, weights=weights_1 ) - var_2 = (s2 ** 2).collapsed( + var_2 = (s2**2).collapsed( corr_coords, iris.analysis.SUM, weights=weights_2 ) diff --git a/lib/iris/common/metadata.py b/lib/iris/common/metadata.py index 29ae910e38..cb5f53f5f4 100644 --- a/lib/iris/common/metadata.py +++ b/lib/iris/common/metadata.py @@ -204,7 +204,7 @@ def func(field): lambda field: field not in ( "circular", - "src_dim", + "location_axis", "node_dimension", "edge_dimension", "face_dimension", diff --git a/lib/iris/common/resolve.py b/lib/iris/common/resolve.py index 6eb79a65f9..12db64cafe 100644 --- a/lib/iris/common/resolve.py +++ b/lib/iris/common/resolve.py @@ -112,10 +112,10 @@ class Resolve: Cell methods: mean time (6 hour) Attributes: - Conventions CF-1.5 - Model scenario A1B + Conventions 'CF-1.5' + Model scenario 'A1B' STASH m01s03i236 - source Data from Met Office Unified Model 6.05 + source 'Data from Met Office Unified Model 6.05' >>> print(cube2) air_temperature / (K) (longitude: 49; latitude: 37) @@ -130,10 +130,10 @@ class Resolve: Cell methods: mean time (6 hour) Attributes: - Conventions CF-1.5 - Model scenario E1 + Conventions 'CF-1.5' + Model scenario 'E1' STASH m01s03i236 - source Data from Met Office Unified Model 6.05 + source 'Data from Met Office Unified Model 6.05' >>> print(data.shape) (240, 37, 49) @@ -153,9 +153,9 @@ class Resolve: Cell methods: mean time (6 hour) Attributes: - Conventions CF-1.5 + Conventions 'CF-1.5' STASH m01s03i236 - source Data from Met Office Unified Model 6.05 + source 'Data from Met Office Unified Model 6.05' Secondly, creating an *empty* ``resolver`` instance, that may be called *multiple* times with *different* :class:`~iris.cube.Cube` operands and *different* ``data``, @@ -2413,10 +2413,10 @@ def mapped(self): Cell methods: mean time (6 hour) Attributes: - Conventions CF-1.5 - Model scenario A1B + Conventions 'CF-1.5' + Model scenario 'A1B' STASH m01s03i236 - source Data from Met Office Unified Model 6.05 + source 'Data from Met Office Unified Model 6.05' >>> print(cube2) air_temperature / (K) (longitude: 49; latitude: 37) Dimension coordinates: @@ -2430,10 +2430,10 @@ def mapped(self): Cell methods: mean time (6 hour) Attributes: - Conventions CF-1.5 - Model scenario E1 + Conventions 'CF-1.5' + Model scenario 'E1' STASH m01s03i236 - source Data from Met Office Unified Model 6.05 + source 'Data from Met Office Unified Model 6.05' >>> Resolve().mapped is None True >>> resolver = Resolve(cube1, cube2) @@ -2481,10 +2481,10 @@ def shape(self): Cell methods: mean time (6 hour) Attributes: - Conventions CF-1.5 - Model scenario A1B + Conventions 'CF-1.5' + Model scenario 'A1B' STASH m01s03i236 - source Data from Met Office Unified Model 6.05 + source 'Data from Met Office Unified Model 6.05' >>> print(cube2) air_temperature / (K) (longitude: 49; latitude: 37) Dimension coordinates: @@ -2498,10 +2498,10 @@ def shape(self): Cell methods: mean time (6 hour) Attributes: - Conventions CF-1.5 - Model scenario E1 + Conventions 'CF-1.5' + Model scenario 'E1' STASH m01s03i236 - source Data from Met Office Unified Model 6.05 + source 'Data from Met Office Unified Model 6.05' >>> Resolve().shape is None True >>> Resolve(cube1, cube2).shape diff --git a/lib/iris/coord_systems.py b/lib/iris/coord_systems.py index 300f49014a..2f875bb159 100644 --- a/lib/iris/coord_systems.py +++ b/lib/iris/coord_systems.py @@ -297,7 +297,10 @@ def as_cartopy_crs(self): return ccrs.Geodetic(self.as_cartopy_globe()) def as_cartopy_projection(self): - return ccrs.PlateCarree() + return ccrs.PlateCarree( + central_longitude=self.longitude_of_prime_meridian, + globe=self.as_cartopy_globe(), + ) def as_cartopy_globe(self): # Explicitly set `ellipse` to None as a workaround for diff --git a/lib/iris/coords.py b/lib/iris/coords.py index db193d0046..b236d407da 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -10,7 +10,7 @@ from abc import ABCMeta, abstractmethod from collections import namedtuple -from collections.abc import Iterator +from collections.abc import Container, Iterator import copy from itertools import chain, zip_longest import operator @@ -57,6 +57,10 @@ class _DimensionalMetadata(CFVariableMixin, metaclass=ABCMeta): _MODE_RDIV: "/", } + # Used by printout methods : __str__ and __repr__ + # Overridden in subclasses : Coord->'points', Connectivity->'indices' + _values_array_name = "data" + @abstractmethod def __init__( self, @@ -268,78 +272,332 @@ def _has_lazy_values(self): """ return self._values_dm.has_lazy_data() - def _repr_other_metadata(self): - fmt = "" - if self.long_name: - fmt = ", long_name={self.long_name!r}" - if self.var_name: - fmt += ", var_name={self.var_name!r}" - if len(self.attributes) > 0: - fmt += ", attributes={self.attributes}" - result = fmt.format(self=self) - return result + def summary( + self, + shorten=False, + max_values=None, + edgeitems=2, + linewidth=None, + precision=None, + convert_dates=True, + _section_indices=None, + ): + r""" + Make a printable text summary. + + Parameters + ---------- + shorten : bool, default = False + If True, produce an abbreviated one-line summary. + If False, produce a multi-line summary, with embedded newlines. + max_values : int or None, default = None + If more than this many data values, print truncated data arrays + instead of full contents. + If 0, print only the shape. + The default is 5 if :attr:`shorten`\ =True, or 15 otherwise. + This overrides ``numpy.get_printoptions['threshold']``\ . + linewidth : int or None, default = None + Character-width controlling line splitting of array outputs. + If unset, defaults to ``numpy.get_printoptions['linewidth']``\ . + edgeitems : int = 2 + Controls truncated array output. + Overrides ``numpy.getprintoptions['edgeitems']``\ . + precision : int or None, default = None + Controls number decimal formatting. + When :attr:`shorten`\ =True this is defaults to 3, in which case it + overrides ``numpy.get_printoptions()['precision']``\ . + convert_dates : bool, default = True + If the units has a calendar, then print array values as date + strings instead of the actual numbers. + + Returns + ------- + result : str + Output text, with embedded newlines when :attr:`shorten`\ =False. - def _str_dates(self, dates_as_numbers): - date_obj_array = self.units.num2date(dates_as_numbers) - kwargs = {"separator": ", ", "prefix": " "} - return np.core.arrayprint.array2string( - date_obj_array, formatter={"all": str}, **kwargs - ) - def __str__(self): - # Note: this method includes bounds handling code, but it only runs - # within Coord type instances, as only these allow bounds to be set. - if self.units.is_time_reference(): - fmt = ( - "{cls}({values}{bounds}" - ", standard_name={self.standard_name!r}" - ", calendar={self.units.calendar!r}{other_metadata})" + .. note:: + Arrays are formatted using :meth:`numpy.array2string`. Some aspects + of the array formatting are controllable in the usual way, via + :meth:`numpy.printoptions`, but others are overridden as detailed + above. + Control of those aspects is still available, but only via the call + arguments. + + """ + # NOTE: the *private* key "_section_indices" can be set to a dict, to + # return details of which (line, character) each particular section of + # the output text begins at. + # Currently only used by MeshCoord.summary(), which needs this info to + # modify the result string, for idiosyncratic reasons. + + def array_summary(data, n_max, n_edge, linewidth, precision): + # Return a text summary of an array. + # Take account of strings, dates and masked data. + result = "" + formatter = None + if convert_dates and self.units.is_time_reference(): + # Account for dates, if enabled. + # N.B. a time unit with a long time interval ("months" + # or "years") cannot be converted to a date using + # `num2date`, so gracefully fall back to printing + # values as numbers. + if not self.units.is_long_time_interval(): + # Otherwise ... replace all with strings. + if ma.is_masked(data): + mask = data.mask + else: + mask = None + data = np.array(self.units.num2date(data)) + data = data.astype(str) + # Masked datapoints do not survive num2date. + if mask is not None: + data = np.ma.masked_array(data, mask) + + if ma.is_masked(data): + # Masks are not handled by np.array2string, whereas + # MaskedArray.__str__ is using a private method to convert to + # objects. + # Our preferred solution is to convert to strings *and* fill + # with '--'. This is not ideal because numbers will not align + # with a common numeric format, but there is no *public* logic + # in numpy to arrange that, so let's not overcomplicate. + # It happens that array2string *also* does not use a common + # format (width) for strings, but we fix that below... + data = data.astype(str).filled("--") + + if data.dtype.kind == "U": + # Strings : N.B. includes all missing data + # find the longest. + length = max(len(str(x)) for x in data.flatten()) + # Pre-apply a common formatting width. + formatter = {"all": lambda x: str(x).ljust(length)} + + result = np.array2string( + data, + separator=", ", + edgeitems=n_edge, + threshold=n_max, + max_line_width=linewidth, + formatter=formatter, + precision=precision, ) - if self.units.is_long_time_interval(): - # A time unit with a long time interval ("months" or "years") - # cannot be converted to a date using `num2date` so gracefully - # fall back to printing points as numbers, not datetimes. - values = self._values + + return result + + units_str = str(self.units) + if self.units.calendar and not shorten: + units_str += f", {self.units.calendar} calendar" + title_str = f"{self.name()} / ({units_str})" + cls_str = type(self).__name__ + shape_str = str(self.shape) + + # Implement conditional defaults for control args. + if max_values is None: + max_values = 5 if shorten else 15 + precision = 3 if shorten else None + n_indent = 4 + indent = " " * n_indent + newline_indent = "\n" + indent + if linewidth is not None: + given_array_width = linewidth + else: + given_array_width = np.get_printoptions()["linewidth"] + using_array_width = given_array_width - n_indent * 2 + # Make a printout of the main data array (or maybe not, if lazy). + if self._has_lazy_values(): + data_str = "" + elif max_values == 0: + data_str = "[...]" + else: + data_str = array_summary( + self._values, + n_max=max_values, + n_edge=edgeitems, + linewidth=using_array_width, + precision=precision, + ) + + # The output under construction, divided into lines for convenience. + output_lines = [""] + + def add_output(text, section=None): + # Append output text and record locations of named 'sections' + if section and _section_indices is not None: + # defined a named 'section', recording the current line number + # and character position as its start position + i_line = len(output_lines) - 1 + i_char = len(output_lines[-1]) + _section_indices[section] = (i_line, i_char) + # Split the text-to-add into lines + lines = text.split("\n") + # Add initial text (before first '\n') to the current line + output_lines[-1] += lines[0] + # Add subsequent lines as additional output lines + for line in lines[1:]: + output_lines.append(line) # Add new lines + + if shorten: + add_output(f"<{cls_str}: ") + add_output(f"{title_str} ", section="title") + + if data_str != "": + # Flatten to a single line, reducing repeated spaces. + def flatten_array_str(array_str): + array_str = array_str.replace("\n", " ") + array_str = array_str.replace("\t", " ") + while " " in array_str: + array_str = array_str.replace(" ", " ") + return array_str + + data_str = flatten_array_str(data_str) + # Adjust maximum-width to allow for the title width in the + # repr form. + current_line_len = len(output_lines[-1]) + using_array_width = given_array_width - current_line_len + # Work out whether to include a summary of the data values + if len(data_str) > using_array_width: + # Make one more attempt, printing just the *first* point, + # as this is useful for dates. + data_str = data_str = array_summary( + self._values[:1], + n_max=max_values, + n_edge=edgeitems, + linewidth=using_array_width, + precision=precision, + ) + data_str = flatten_array_str(data_str) + data_str = data_str[:-1] + ", ...]" + if len(data_str) > using_array_width: + # Data summary is still too long : replace with array + # "placeholder" representation. + data_str = "[...]" + + if self.has_bounds(): + data_str += "+bounds" + + if self.shape != (1,): + # Anything non-scalar : show shape as well. + data_str += f" shape{shape_str}" + + # single-line output in 'shorten' mode + add_output(f"{data_str}>", section="data") + + else: + # Long (multi-line) output format. + add_output(f"{cls_str} : ") + add_output(f"{title_str}", section="title") + + def reindent_data_string(text, n_indent): + lines = [line for line in text.split("\n")] + indent = " " * (n_indent - 1) # allow 1 for the initial '[' + # Indent all but the *first* line. + line_1, rest_lines = lines[0], lines[1:] + rest_lines = ["\n" + indent + line for line in rest_lines] + result = line_1 + "".join(rest_lines) + return result + + data_array_str = reindent_data_string(data_str, 2 * n_indent) + + # NOTE: actual section name is variable here : data/points/indices + data_text = f"{self._values_array_name}: " + if "\n" in data_array_str: + # Put initial '[' here, and the rest on subsequent lines + data_text += "[" + newline_indent + indent + data_array_str[1:] else: - values = self._str_dates(self._values) - bounds = "" + # All on one line + data_text += data_array_str + + # N.B. indent section and record section start after that + add_output(newline_indent) + add_output(data_text, section="data") + if self.has_bounds(): - if self.units.is_long_time_interval(): - bounds_vals = self.bounds + # Add a bounds section : basically just like the 'data'. + if self._bounds_dm.has_lazy_data(): + bounds_array_str = "" + elif max_values == 0: + bounds_array_str = "[...]" else: - bounds_vals = self._str_dates(self.bounds) - bounds = ", bounds={vals}".format(vals=bounds_vals) - result = fmt.format( - self=self, - cls=type(self).__name__, - values=values, - bounds=bounds, - other_metadata=self._repr_other_metadata(), - ) - else: - result = repr(self) + bounds_array_str = array_summary( + self._bounds_dm.data, + n_max=max_values, + n_edge=edgeitems, + linewidth=using_array_width, + precision=precision, + ) + bounds_array_str = reindent_data_string( + bounds_array_str, 2 * n_indent + ) - return result + bounds_text = "bounds: " + if "\n" in bounds_array_str: + # Put initial '[' here, and the rest on subsequent lines + bounds_text += ( + "[" + newline_indent + indent + bounds_array_str[1:] + ) + else: + # All on one line + bounds_text += bounds_array_str + + # N.B. indent section and record section start after that + add_output(newline_indent) + add_output(bounds_text, section="bounds") + + if self.has_bounds(): + shape_str += f" bounds{self._bounds_dm.shape}" + + # Add shape section (always) + add_output(newline_indent) + add_output(f"shape: {shape_str}", section="shape") + + # Add dtype section (always) + add_output(newline_indent) + add_output(f"dtype: {self.dtype}", section="dtype") + + for name in self._metadata_manager._fields: + if name == "units": + # This was already included in the header line + continue + val = getattr(self, name, None) + if isinstance(val, Container): + # Don't print empty containers, like attributes={} + show = bool(val) + else: + # Don't print properties when not present, or set to None, + # or False. + # This works OK as long as we are happy to treat all + # boolean properties as 'off' when False : Which happens to + # work for all those defined so far. + show = val is not None and val is not False + if show: + if name == "attributes": + # Use a multi-line form for this. + add_output(newline_indent) + add_output("attributes:", section="attributes") + max_attname_len = max(len(attr) for attr in val.keys()) + for attrname, attrval in val.items(): + attrname = attrname.ljust(max_attname_len) + if isinstance(attrval, str): + # quote strings + attrval = repr(attrval) + # and abbreviate really long ones + attrval = iris.util.clip_string(attrval) + attr_string = f"{attrname} {attrval}" + add_output(newline_indent + indent + attr_string) + else: + # add a one-line section for this property + # (aka metadata field) + add_output(newline_indent) + add_output(f"{name}: {val!r}", section=name) + + return "\n".join(output_lines) + + def __str__(self): + return self.summary() def __repr__(self): - # Note: this method includes bounds handling code, but it only runs - # within Coord type instances, as only these allow bounds to be set. - fmt = ( - "{cls}({self._values!r}{bounds}" - ", standard_name={self.standard_name!r}, units={self.units!r}" - "{other_metadata})" - ) - bounds = "" - # if coordinate, handle the bounds - if self.has_bounds(): - bounds = ", bounds=" + repr(self.bounds) - result = fmt.format( - self=self, - cls=type(self).__name__, - bounds=bounds, - other_metadata=self._repr_other_metadata(), - ) - return result + return self.summary(shorten=True) def __eq__(self, other): # Note: this method includes bounds handling code, but it only runs @@ -861,23 +1119,6 @@ def measure(self, measure): raise ValueError(emsg) self._metadata_manager.measure = measure - def __str__(self): - result = repr(self) - return result - - def __repr__(self): - fmt = ( - "{cls}({self.data!r}, " - "measure={self.measure!r}, standard_name={self.standard_name!r}, " - "units={self.units!r}{other_metadata})" - ) - result = fmt.format( - self=self, - cls=type(self).__name__, - other_metadata=self._repr_other_metadata(), - ) - return result - def cube_dims(self, cube): """ Return the cube dimensions of this CellMeasure. @@ -1303,6 +1544,8 @@ class Coord(_DimensionalMetadata): """ + _values_array_name = "points" + @abstractmethod def __init__( self, @@ -1603,14 +1846,6 @@ def has_lazy_bounds(self): result = self._bounds_dm.has_lazy_data() return result - def _repr_other_metadata(self): - result = super()._repr_other_metadata() - if self.coord_system: - result += ", coord_system={}".format(self.coord_system) - if self.climatological: - result += ", climatological={}".format(self.climatological) - return result - # Must supply __hash__ as Python 3 does not enable it if __eq__ is defined. # NOTE: Violates "objects which compare equal must have the same hash". # We ought to remove this, as equality of two coords can *change*, so they @@ -2512,12 +2747,6 @@ def collapsed(self, dims_to_collapse=None): coord.circular = False return coord - def _repr_other_metadata(self): - result = Coord._repr_other_metadata(self) - if self.circular: - result += ", circular=%r" % self.circular - return result - def _new_points_requirements(self, points): """ Confirm that a new set of coord points adheres to the requirements for diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 90acc021bc..b456bd9663 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -745,7 +745,7 @@ class Cube(CFVariableMixin): mean over years time Attributes: STASH m01s16i203 - source Data from Met Office Unified Model + source 'Data from Met Office Unified Model' See the :doc:`user guide` for more information. @@ -1051,9 +1051,7 @@ def convert_units(self, unit): old_unit = self.units new_unit = unit - # Define a delayed conversion operation (i.e. a callback). - def pointwise_convert(values): - return old_unit.convert(values, new_unit) + pointwise_convert = partial(old_unit.convert, other=new_unit) new_data = _lazy.lazy_elementwise( self.lazy_data(), pointwise_convert @@ -1453,39 +1451,53 @@ def coord_dims(self, coord): The (name of the) coord to look for. """ - - coord = self.coord(coord) - - # Search for existing coordinate (object) on the cube, faster lookup - # than equality - makes no functional difference. - matches = [ - (dim,) - for coord_, dim in self._dim_coords_and_dims - if coord_ is coord - ] - if not matches: - matches = [ - dims - for coord_, dims in self._aux_coords_and_dims - if coord_ is coord - ] - - # Search derived aux coords - if not matches: + name_provided = False + if isinstance(coord, str): + # Forced to look-up the coordinate if we only have the name. + coord = self.coord(coord) + name_provided = True + + coord_id = id(coord) + + # Dimension of dimension coordinate by object id + dims_by_id = {id(c): (d,) for c, d in self._dim_coords_and_dims} + # Check for id match - faster than equality check + match = dims_by_id.get(coord_id) + + if match is None: + # Dimension/s of auxiliary coordinate by object id + aux_dims_by_id = {id(c): d for c, d in self._aux_coords_and_dims} + # Check for id match - faster than equality + match = aux_dims_by_id.get(coord_id) + if match is None: + dims_by_id.update(aux_dims_by_id) + + if match is None and not name_provided: + # We may have an equivalent coordinate but not the actual + # cube coordinate instance - so forced to perform coordinate + # lookup to attempt to retrieve it + coord = self.coord(coord) + # Check for id match - faster than equality + match = dims_by_id.get(id(coord)) + + # Search derived aux coordinates + if match is None: target_metadata = coord.metadata - def match(factory): + def matcher(factory): return factory.metadata == target_metadata - factories = filter(match, self._aux_factories) + factories = filter(matcher, self._aux_factories) matches = [ factory.derived_dims(self.coord_dims) for factory in factories ] + if matches: + match = matches[0] - if not matches: + if match is None: raise iris.exceptions.CoordinateNotFoundError(coord.name()) - return matches[0] + return match def cell_measure_dims(self, cell_measure): """ @@ -3638,7 +3650,7 @@ def collapsed(self, coords, aggregator, **kwargs): mean month, year mean longitude Attributes: - Conventions CF-1.5 + Conventions 'CF-1.5' STASH m01s00i024 @@ -3873,7 +3885,7 @@ def aggregated_by(self, coords, aggregator, **kwargs): mean month, year mean year Attributes: - Conventions CF-1.5 + Conventions 'CF-1.5' STASH m01s00i024 """ @@ -4078,8 +4090,8 @@ def rolling_window(self, coord, aggregator, window, **kwargs): Attributes: STASH m01s00i024 source \ -Data from Met Office Unified Model - um_version 7.6 +'Data from Met Office Unified Model' + um_version '7.6' >>> print(air_press.rolling_window('time', iris.analysis.MEAN, 3)) @@ -4104,8 +4116,8 @@ def rolling_window(self, coord, aggregator, window, **kwargs): Attributes: STASH m01s00i024 source \ -Data from Met Office Unified Model - um_version 7.6 +'Data from Met Office Unified Model' + um_version '7.6' Notice that the forecast_period dimension now represents the 4 possible windows of size 3 from the original cube. @@ -4236,7 +4248,7 @@ def interpolate(self, sample_points, scheme, collapse_scalar=True): dates or times may optionally be supplied as datetime.datetime or cftime.datetime instances. * scheme: - The type of interpolation to use to interpolate from this + An instance of the type of interpolation to use to interpolate from this :class:`~iris.cube.Cube` to the given sample points. The interpolation schemes currently available in Iris are: @@ -4265,8 +4277,11 @@ def interpolate(self, sample_points, scheme, collapse_scalar=True): air_potential_temperature / (K) \ (time: 3; model_level_number: 7; grid_latitude: 204; grid_longitude: 187) >>> print(cube.coord('time')) - DimCoord([2009-11-19 10:00:00, 2009-11-19 11:00:00, \ -2009-11-19 12:00:00], standard_name='time', calendar='gregorian') + DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) + points: [2009-11-19 10:00:00, 2009-11-19 11:00:00, 2009-11-19 12:00:00] + shape: (3,) + dtype: float64 + standard_name: 'time' >>> print(cube.coord('time').points) [349618. 349619. 349620.] >>> samples = [('time', 349618.5)] @@ -4275,8 +4290,11 @@ def interpolate(self, sample_points, scheme, collapse_scalar=True): air_potential_temperature / (K) \ (model_level_number: 7; grid_latitude: 204; grid_longitude: 187) >>> print(result.coord('time')) - DimCoord([2009-11-19 10:30:00], standard_name='time', \ -calendar='gregorian') + DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) + points: [2009-11-19 10:30:00] + shape: (1,) + dtype: float64 + standard_name: 'time' >>> print(result.coord('time').points) [349618.5] >>> # For datetime-like coordinates, we can also use @@ -4287,8 +4305,11 @@ def interpolate(self, sample_points, scheme, collapse_scalar=True): air_potential_temperature / (K) \ (model_level_number: 7; grid_latitude: 204; grid_longitude: 187) >>> print(result2.coord('time')) - DimCoord([2009-11-19 10:30:00], standard_name='time', \ -calendar='gregorian') + DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) + points: [2009-11-19 10:30:00] + shape: (1,) + dtype: float64 + standard_name: 'time' >>> print(result2.coord('time').points) [349618.5] >>> print(result == result2) @@ -4309,7 +4330,7 @@ def regrid(self, grid, scheme): * grid: A :class:`~iris.cube.Cube` that defines the target grid. * scheme: - The type of regridding to use to regrid this cube onto the + An instance of the type of regridding to use to regrid this cube onto the target grid. The regridding schemes in Iris currently include: * :class:`iris.analysis.Linear`\*, diff --git a/lib/iris/experimental/equalise_cubes.py b/lib/iris/experimental/equalise_cubes.py deleted file mode 100644 index 8be7175067..0000000000 --- a/lib/iris/experimental/equalise_cubes.py +++ /dev/null @@ -1,30 +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. -""" -Experimental cube-adjusting functions to assist merge operations. - -""" - - -def equalise_attributes(cubes): - """ - Delete cube attributes that are not identical over all cubes in a group. - - .. warning:: - - This function is now **disabled**. - - The functionality has been moved to - :func:`iris.util.equalise_attributes`. - - """ - old = "iris.experimental.equalise_cubes.equalise_attributes" - new = "iris.util.equalise_attributes" - emsg = ( - f'The function "{old}" has been moved.\n' - f'Please replace "{old}()" with "{new}()".' - ) - raise Exception(emsg) diff --git a/lib/iris/experimental/raster.py b/lib/iris/experimental/raster.py index 83fd761973..7c95293469 100644 --- a/lib/iris/experimental/raster.py +++ b/lib/iris/experimental/raster.py @@ -20,8 +20,17 @@ from osgeo import gdal, osr import iris +from iris._deprecation import warn_deprecated import iris.coord_systems +wmsg = ( + "iris.experimental.raster is deprecated since version 3.2, and will be " + "removed in a future release. If you make use of this functionality, " + "please contact the Iris Developers to discuss how to retain it (which may " + "involve reversing the deprecation)." +) +warn_deprecated(wmsg) + _GDAL_DATATYPES = { "i2": gdal.GDT_Int16, "i4": gdal.GDT_Int32, @@ -96,6 +105,14 @@ def export_geotiff(cube, fname): """ Writes cube data to raster file format as a PixelIsArea GeoTiff image. + .. deprecated:: 3.2.0 + + This method is scheduled to be removed in a future release, and no + replacement is currently planned. + If you make use of this functionality, please contact the Iris + Developers to discuss how to retain it (which could include reversing + the deprecation). + Args: * cube (Cube): The 2D regularly gridded cube slice to be exported. The cube must have regular, contiguous bounds. @@ -107,6 +124,13 @@ def export_geotiff(cube, fname): http://www.remotesensing.org/geotiff/spec/geotiff2.5.html#2.5.2.2 """ + wmsg = ( + "iris.experimental.raster.export_geotiff has been deprecated, and will " + "be removed in a future release. Please consult the docstring for " + "details." + ) + warn_deprecated(wmsg) + if cube.ndim != 2: raise ValueError("The cube must be two dimensional.") diff --git a/lib/iris/experimental/regrid.py b/lib/iris/experimental/regrid.py index 9a02fbd3b1..7c5d8e99cc 100644 --- a/lib/iris/experimental/regrid.py +++ b/lib/iris/experimental/regrid.py @@ -6,26 +6,37 @@ """ Regridding functions. -""" +.. note:: + + .. deprecated:: 3.2.0 + + This package will be removed in a future release. + The PointInCell class has now moved to :class:`iris.analysis.PointInCell`. + All the other content will be withdrawn. + + If you still use any of this, please contact the Iris Developers to + discuss how to replace it or to retain it. +""" import copy import functools import warnings import cartopy.crs as ccrs -import cf_units import numpy as np -import numpy.ma as ma import scipy.interpolate -from iris._lazy_data import map_complete_blocks +from iris._deprecation import warn_deprecated +from iris.analysis._area_weighted import ( + _regrid_area_weighted_rectilinear_src_and_grid__perform, + _regrid_area_weighted_rectilinear_src_and_grid__prepare, +) from iris.analysis._interpolation import ( get_xy_coords, get_xy_dim_coords, snapshot_grid, ) from iris.analysis._regrid import ( - RectilinearRegridder, _regrid_weighted_curvilinear_to_rectilinear__perform, _regrid_weighted_curvilinear_to_rectilinear__prepare, ) @@ -34,564 +45,38 @@ import iris.cube from iris.util import _meshgrid +wmsg = ( + "The 'iris.experimental.regrid' package is deprecated since version 3.2, " + "and will be removed in a future release. The PointInCell class has now " + "moved into iris.analysis. All its other content will be withdrawn. " + "If you still use any of this, please contact the Iris Developers to " + "discuss how to replace it or to retain it (reverse the deprecation)." +) +warn_deprecated(wmsg) -def _get_xy_coords(cube): - """ - Return the x and y coordinates from a cube. - - This function will preferentially return a pair of dimension - coordinates (if there are more than one potential x or y dimension - coordinates a ValueError will be raised). If the cube does not have - a pair of x and y dimension coordinates it will return 1D auxiliary - coordinates (including scalars). If there is not one and only one set - of x and y auxiliary coordinates a ValueError will be raised. - - Having identified the x and y coordinates, the function checks that they - have equal coordinate systems and that they do not occupy the same - dimension on the cube. - - Args: - - * cube: - An instance of :class:`iris.cube.Cube`. - - Returns: - A tuple containing the cube's x and y coordinates. - - """ - # Look for a suitable dimension coords first. - x_coords = cube.coords(axis="x", dim_coords=True) - if not x_coords: - # If there is no x coord in dim_coords look for scalars or - # monotonic coords in aux_coords. - x_coords = [ - coord - for coord in cube.coords(axis="x", dim_coords=False) - if coord.ndim == 1 and coord.is_monotonic() - ] - if len(x_coords) != 1: - raise ValueError( - "Cube {!r} must contain a single 1D x " - "coordinate.".format(cube.name()) - ) - x_coord = x_coords[0] - - # Look for a suitable dimension coords first. - y_coords = cube.coords(axis="y", dim_coords=True) - if not y_coords: - # If there is no y coord in dim_coords look for scalars or - # monotonic coords in aux_coords. - y_coords = [ - coord - for coord in cube.coords(axis="y", dim_coords=False) - if coord.ndim == 1 and coord.is_monotonic() - ] - if len(y_coords) != 1: - raise ValueError( - "Cube {!r} must contain a single 1D y " - "coordinate.".format(cube.name()) - ) - y_coord = y_coords[0] - - if x_coord.coord_system != y_coord.coord_system: - raise ValueError( - "The cube's x ({!r}) and y ({!r}) " - "coordinates must have the same coordinate " - "system.".format(x_coord.name(), y_coord.name()) - ) - - # The x and y coordinates must describe different dimensions - # or be scalar coords. - x_dims = cube.coord_dims(x_coord) - x_dim = None - if x_dims: - x_dim = x_dims[0] - - y_dims = cube.coord_dims(y_coord) - y_dim = None - if y_dims: - y_dim = y_dims[0] - - if x_dim is not None and y_dim == x_dim: - raise ValueError( - "The cube's x and y coords must not describe the " - "same data dimension." - ) - - return x_coord, y_coord - - -def _within_bounds(src_bounds, tgt_bounds, orderswap=False): - """ - Determine which target bounds lie within the extremes of the source bounds. - - Args: - - * src_bounds (ndarray): - An (n, 2) shaped array of monotonic contiguous source bounds. - * tgt_bounds (ndarray): - An (n, 2) shaped array corresponding to the target bounds. - - Kwargs: - - * orderswap (bool): - A Boolean indicating whether the target bounds are in descending order - (True). Defaults to False. - - Returns: - Boolean ndarray, indicating whether each target bound is within the - extremes of the source bounds. - - """ - min_bound = np.min(src_bounds) - 1e-14 - max_bound = np.max(src_bounds) + 1e-14 - - # Swap upper-lower is necessary. - if orderswap is True: - upper, lower = tgt_bounds.T - else: - lower, upper = tgt_bounds.T - - return ((lower <= max_bound) * (lower >= min_bound)) * ( - (upper <= max_bound) * (upper >= min_bound) - ) - - -def _cropped_bounds(bounds, lower, upper): - """ - Return a new bounds array and corresponding slice object (or indices) of - the original data array, resulting from cropping the provided bounds - between the specified lower and upper values. The bounds at the - extremities will be truncated so that they start and end with lower and - upper. - - This function will return an empty NumPy array and slice if there is no - overlap between the region covered by bounds and the region from lower to - upper. - - If lower > upper the resulting bounds may not be contiguous and the - indices object will be a tuple of indices rather than a slice object. - - Args: - - * bounds: - An (n, 2) shaped array of monotonic contiguous bounds. - * lower: - Lower bound at which to crop the bounds array. - * upper: - Upper bound at which to crop the bounds array. - - Returns: - A tuple of the new bounds array and the corresponding slice object or - indices from the zeroth axis of the original array. - - """ - reversed_flag = False - # Ensure order is increasing. - if bounds[0, 0] > bounds[-1, 0]: - # Reverse bounds - bounds = bounds[::-1, ::-1] - reversed_flag = True - - # Number of bounds. - n = bounds.shape[0] - - if lower <= upper: - if lower > bounds[-1, 1] or upper < bounds[0, 0]: - new_bounds = bounds[0:0] - indices = slice(0, 0) - else: - # A single region lower->upper. - if lower < bounds[0, 0]: - # Region extends below bounds so use first lower bound. - lindex = 0 - lower = bounds[0, 0] - else: - # Index of last lower bound less than or equal to lower. - lindex = np.nonzero(bounds[:, 0] <= lower)[0][-1] - if upper > bounds[-1, 1]: - # Region extends above bounds so use last upper bound. - uindex = n - 1 - upper = bounds[-1, 1] - else: - # Index of first upper bound greater than or equal to - # upper. - uindex = np.nonzero(bounds[:, 1] >= upper)[0][0] - # Extract the bounds in our region defined by lower->upper. - new_bounds = np.copy(bounds[lindex : (uindex + 1), :]) - # Replace first and last values with specified bounds. - new_bounds[0, 0] = lower - new_bounds[-1, 1] = upper - if reversed_flag: - indices = slice(n - (uindex + 1), n - lindex) - else: - indices = slice(lindex, uindex + 1) - else: - # Two regions [0]->upper, lower->[-1] - # [0]->upper - if upper < bounds[0, 0]: - # Region outside src bounds. - new_bounds_left = bounds[0:0] - indices_left = tuple() - slice_left = slice(0, 0) - else: - if upper > bounds[-1, 1]: - # Whole of bounds. - uindex = n - 1 - upper = bounds[-1, 1] - else: - # Index of first upper bound greater than or equal to upper. - uindex = np.nonzero(bounds[:, 1] >= upper)[0][0] - # Extract the bounds in our region defined by [0]->upper. - new_bounds_left = np.copy(bounds[0 : (uindex + 1), :]) - # Replace last value with specified bound. - new_bounds_left[-1, 1] = upper - if reversed_flag: - indices_left = tuple(range(n - (uindex + 1), n)) - slice_left = slice(n - (uindex + 1), n) - else: - indices_left = tuple(range(0, uindex + 1)) - slice_left = slice(0, uindex + 1) - # lower->[-1] - if lower > bounds[-1, 1]: - # Region is outside src bounds. - new_bounds_right = bounds[0:0] - indices_right = tuple() - slice_right = slice(0, 0) - else: - if lower < bounds[0, 0]: - # Whole of bounds. - lindex = 0 - lower = bounds[0, 0] - else: - # Index of last lower bound less than or equal to lower. - lindex = np.nonzero(bounds[:, 0] <= lower)[0][-1] - # Extract the bounds in our region defined by lower->[-1]. - new_bounds_right = np.copy(bounds[lindex:, :]) - # Replace first value with specified bound. - new_bounds_right[0, 0] = lower - if reversed_flag: - indices_right = tuple(range(0, n - lindex)) - slice_right = slice(0, n - lindex) - else: - indices_right = tuple(range(lindex, n)) - slice_right = slice(lindex, None) - - if reversed_flag: - # Flip everything around. - indices_left, indices_right = indices_right, indices_left - slice_left, slice_right = slice_right, slice_left - - # Combine regions. - new_bounds = np.concatenate((new_bounds_left, new_bounds_right)) - # Use slices if possible, but if we have two regions use indices. - if indices_left and indices_right: - indices = indices_left + indices_right - elif indices_left: - indices = slice_left - elif indices_right: - indices = slice_right - else: - indices = slice(0, 0) - - if reversed_flag: - new_bounds = new_bounds[::-1, ::-1] - - return new_bounds, indices - - -def _cartesian_area(y_bounds, x_bounds): - """ - Return an array of the areas of each cell given two arrays - of cartesian bounds. - - Args: - - * y_bounds: - An (n, 2) shaped NumPy array. - * x_bounds: - An (m, 2) shaped NumPy array. - - Returns: - An (n, m) shaped Numpy array of areas. - - """ - heights = y_bounds[:, 1] - y_bounds[:, 0] - widths = x_bounds[:, 1] - x_bounds[:, 0] - return np.abs(np.outer(heights, widths)) - - -def _spherical_area(y_bounds, x_bounds, radius=1.0): - """ - Return an array of the areas of each cell on a sphere - given two arrays of latitude and longitude bounds in radians. - - Args: - - * y_bounds: - An (n, 2) shaped NumPy array of latitide bounds in radians. - * x_bounds: - An (m, 2) shaped NumPy array of longitude bounds in radians. - * radius: - Radius of the sphere. Default is 1.0. - - Returns: - An (n, m) shaped Numpy array of areas. - - """ - return iris.analysis.cartography._quadrant_area(y_bounds, x_bounds, radius) - - -def _get_bounds_in_units(coord, units, dtype): - """Return a copy of coord's bounds in the specified units and dtype.""" - # The bounds are cast to dtype before conversion to prevent issues when - # mixing float32 and float64 types. - return coord.units.convert(coord.bounds.astype(dtype), units).astype(dtype) - - -def _weighted_mean_with_mdtol(data, weights, axis=None, mdtol=0): - """ - Return the weighted mean of an array over the specified axis - using the provided weights (if any) and a permitted fraction of - masked data. - - Args: - - * data (array-like): - Data to be averaged. - - * weights (array-like): - An array of the same shape as the data that specifies the contribution - of each corresponding data element to the calculated mean. - - Kwargs: - - * axis (int or tuple of ints): - Axis along which the mean is computed. The default is to compute - the mean of the flattened array. - - * mdtol (float): - Tolerance of missing data. The value returned in each element of the - returned array will be masked if the fraction of masked data exceeds - mdtol. This fraction is weighted by the `weights` array if one is - provided. mdtol=0 means no missing data is tolerated - while mdtol=1 will mean the resulting element will be masked if and - only if all the contributing elements of data are masked. - Defaults to 0. - - Returns: - Numpy array (possibly masked) or scalar. - """ - if ma.is_masked(data): - res, unmasked_weights_sum = ma.average( - data, weights=weights, axis=axis, returned=True - ) - if mdtol < 1: - weights_sum = weights.sum(axis=axis) - frac_masked = 1 - np.true_divide(unmasked_weights_sum, weights_sum) - mask_pt = frac_masked > mdtol - if np.any(mask_pt) and not isinstance(res, ma.core.MaskedConstant): - if np.isscalar(res): - res = ma.masked - elif ma.isMaskedArray(res): - res.mask |= mask_pt - else: - res = ma.masked_array(res, mask=mask_pt) - else: - res = np.average(data, weights=weights, axis=axis) - return res - - -def _regrid_area_weighted_array( - src_data, x_dim, y_dim, weights_info, index_info, mdtol=0 +def regrid_area_weighted_rectilinear_src_and_grid( + src_cube, grid_cube, mdtol=0 ): """ - Regrid the given data from its source grid to a new grid using - an area weighted mean to determine the resulting data values. + Return a new cube with data values calculated using the area weighted + mean of data values from src_grid regridded onto the horizontal grid of + grid_cube. .. note:: - Elements in the returned array that lie either partially - or entirely outside of the extent of the source grid will - be masked irrespective of the value of mdtol. + .. deprecated:: 3.2.0 - Args: + This function is scheduled to be removed in a future release. + Please use :meth:`iris.cube.Cube.regrid` with the + :class:`iris.analysis.AreaWeighted` scheme instead : this is an exact + replacement. - * src_data: - An N-dimensional NumPy array. - * x_dim: - The X dimension within `src_data`. - * y_dim: - The Y dimension within `src_data`. - * weights_info: - The area weights information to be used for area-weighted - regridding. + For example : - Kwargs: + .. code:: - * mdtol: - Tolerance of missing data. The value returned in each element of the - returned array will be masked if the fraction of missing data exceeds - mdtol. This fraction is calculated based on the area of masked cells - within each target cell. mdtol=0 means no missing data is tolerated - while mdtol=1 will mean the resulting element will be masked if and - only if all the overlapping elements of the source grid are masked. - Defaults to 0. - - Returns: - The regridded data as an N-dimensional NumPy array. The lengths - of the X and Y dimensions will now match those of the target - grid. - - """ - ( - blank_weights, - src_area_weights, - new_data_mask_basis, - ) = weights_info - - ( - result_x_extent, - result_y_extent, - square_data_indices_y, - square_data_indices_x, - src_area_datas_required, - ) = index_info - - # Ensure we have x_dim and y_dim. - x_dim_orig = x_dim - y_dim_orig = y_dim - if y_dim is None: - src_data = np.expand_dims(src_data, axis=src_data.ndim) - y_dim = src_data.ndim - 1 - if x_dim is None: - src_data = np.expand_dims(src_data, axis=src_data.ndim) - x_dim = src_data.ndim - 1 - # Move y_dim and x_dim to last dimensions - if not x_dim == src_data.ndim - 1: - src_data = np.moveaxis(src_data, x_dim, -1) - if not y_dim == src_data.ndim - 2: - if x_dim < y_dim: - # note: y_dim was shifted along by one position when - # x_dim was moved to the last dimension - src_data = np.moveaxis(src_data, y_dim - 1, -2) - elif x_dim > y_dim: - src_data = np.moveaxis(src_data, y_dim, -2) - x_dim = src_data.ndim - 1 - y_dim = src_data.ndim - 2 - - # Create empty "pre-averaging" data array that will enable the - # src_data data coresponding to a given target grid point, - # to be stacked per point. - # Note that dtype is not preserved and that the array mask - # allows for regions that do not overlap. - new_shape = list(src_data.shape) - new_shape[x_dim] = result_x_extent - new_shape[y_dim] = result_y_extent - - # Use input cube dtype or convert values to the smallest possible float - # dtype when necessary. - dtype = np.promote_types(src_data.dtype, np.float16) - - # Axes of data over which the weighted mean is calculated. - axis = (y_dim, x_dim) - - # Use previously established indices - - src_area_datas_square = src_data[ - ..., square_data_indices_y, square_data_indices_x - ] - - _, src_area_datas_required = np.broadcast_arrays( - src_area_datas_square, src_area_datas_required - ) - - src_area_datas = np.where( - src_area_datas_required, src_area_datas_square, 0 - ) - - # Flag to indicate whether the original data was a masked array. - src_masked = src_data.mask.any() if ma.isMaskedArray(src_data) else False - if src_masked: - src_area_masks_square = src_data.mask[ - ..., square_data_indices_y, square_data_indices_x - ] - src_area_masks = np.where( - src_area_datas_required, src_area_masks_square, True - ) - - else: - # If the weights were originally blank, set the weights to all 1 to - # avoid divide by 0 error and set the new data mask for making the - # values 0 - src_area_weights = np.where(blank_weights, 1, src_area_weights) - - new_data_mask = np.broadcast_to(new_data_mask_basis, new_shape) - - # Broadcast the weights array to allow numpy's ma.average - # to be called. - # Assign new shape to raise error on copy. - src_area_weights.shape = src_area_datas.shape[-3:] - # Broadcast weights to match shape of data. - _, src_area_weights = np.broadcast_arrays(src_area_datas, src_area_weights) - - # Mask the data points - if src_masked: - src_area_datas = np.ma.array(src_area_datas, mask=src_area_masks) - - # Calculate weighted mean taking into account missing data. - new_data = _weighted_mean_with_mdtol( - src_area_datas, weights=src_area_weights, axis=axis, mdtol=mdtol - ) - new_data = new_data.reshape(new_shape) - if src_masked: - new_data_mask = new_data.mask - - # Mask the data if originally masked or if the result has masked points - if ma.isMaskedArray(src_data): - new_data = ma.array( - new_data, - mask=new_data_mask, - fill_value=src_data.fill_value, - dtype=dtype, - ) - elif new_data_mask.any(): - new_data = ma.array(new_data, mask=new_data_mask, dtype=dtype) - else: - new_data = new_data.astype(dtype) - - # Restore data to original form - if x_dim_orig is None and y_dim_orig is None: - new_data = np.squeeze(new_data, axis=x_dim) - new_data = np.squeeze(new_data, axis=y_dim) - elif y_dim_orig is None: - new_data = np.squeeze(new_data, axis=y_dim) - new_data = np.moveaxis(new_data, -1, x_dim_orig) - elif x_dim_orig is None: - new_data = np.squeeze(new_data, axis=x_dim) - new_data = np.moveaxis(new_data, -1, y_dim_orig) - elif x_dim_orig < y_dim_orig: - # move the x_dim back first, so that the y_dim will - # then be moved to its original position - new_data = np.moveaxis(new_data, -1, x_dim_orig) - new_data = np.moveaxis(new_data, -1, y_dim_orig) - else: - # move the y_dim back first, so that the x_dim will - # then be moved to its original position - new_data = np.moveaxis(new_data, -2, y_dim_orig) - new_data = np.moveaxis(new_data, -1, x_dim_orig) - - return new_data - - -def regrid_area_weighted_rectilinear_src_and_grid( - src_cube, grid_cube, mdtol=0 -): - """ - Return a new cube with data values calculated using the area weighted - mean of data values from src_grid regridded onto the horizontal grid of - grid_cube. + result = src_cube.regrid(grid_cube, AreaWeighted()) This function requires that the horizontal grids of both cubes are rectilinear (i.e. expressed in terms of two orthogonal 1D coordinates) @@ -629,6 +114,15 @@ def regrid_area_weighted_rectilinear_src_and_grid( A new :class:`iris.cube.Cube` instance. """ + wmsg = ( + "The function " + "'iris.experimental.regrid." + "regrid_area_weighted_rectilinear_src_and_grid' " + "has been deprecated, and will be removed in a future release. " + "Please consult the docstring for details." + ) + warn_deprecated(wmsg) + regrid_info = _regrid_area_weighted_rectilinear_src_and_grid__prepare( src_cube, grid_cube ) @@ -638,460 +132,26 @@ def regrid_area_weighted_rectilinear_src_and_grid( return result -def _regrid_area_weighted_rectilinear_src_and_grid__prepare( - src_cube, grid_cube -): - """ - First (setup) part of 'regrid_area_weighted_rectilinear_src_and_grid'. - - Check inputs and calculate related info. The 'regrid info' returned - can be re-used over many 2d slices. - - """ - # Get the 1d monotonic (or scalar) src and grid coordinates. - src_x, src_y = _get_xy_coords(src_cube) - grid_x, grid_y = _get_xy_coords(grid_cube) - - # Condition 1: All x and y coordinates must have contiguous bounds to - # define areas. - if ( - not src_x.is_contiguous() - or not src_y.is_contiguous() - or not grid_x.is_contiguous() - or not grid_y.is_contiguous() - ): - raise ValueError( - "The horizontal grid coordinates of both the source " - "and grid cubes must have contiguous bounds." - ) - - # Condition 2: Everything must have the same coordinate system. - src_cs = src_x.coord_system - grid_cs = grid_x.coord_system - if src_cs != grid_cs: - raise ValueError( - "The horizontal grid coordinates of both the source " - "and grid cubes must have the same coordinate " - "system." - ) - - # Condition 3: cannot create vector coords from scalars. - src_x_dims = src_cube.coord_dims(src_x) - src_x_dim = None - if src_x_dims: - src_x_dim = src_x_dims[0] - src_y_dims = src_cube.coord_dims(src_y) - src_y_dim = None - if src_y_dims: - src_y_dim = src_y_dims[0] - if ( - src_x_dim is None - and grid_x.shape[0] != 1 - or src_y_dim is None - and grid_y.shape[0] != 1 - ): - raise ValueError( - "The horizontal grid coordinates of source cube " - "includes scalar coordinates, but the new grid does " - "not. The new grid must not require additional data " - "dimensions to be created." - ) - - # Determine whether to calculate flat or spherical areas. - # Don't only rely on coord system as it may be None. - spherical = ( - isinstance( - src_cs, - (iris.coord_systems.GeogCS, iris.coord_systems.RotatedGeogCS), - ) - or src_x.units == "degrees" - or src_x.units == "radians" - ) - - # Get src and grid bounds in the same units. - x_units = cf_units.Unit("radians") if spherical else src_x.units - y_units = cf_units.Unit("radians") if spherical else src_y.units - - # Operate in highest precision. - src_dtype = np.promote_types(src_x.bounds.dtype, src_y.bounds.dtype) - grid_dtype = np.promote_types(grid_x.bounds.dtype, grid_y.bounds.dtype) - dtype = np.promote_types(src_dtype, grid_dtype) - - src_x_bounds = _get_bounds_in_units(src_x, x_units, dtype) - src_y_bounds = _get_bounds_in_units(src_y, y_units, dtype) - grid_x_bounds = _get_bounds_in_units(grid_x, x_units, dtype) - grid_y_bounds = _get_bounds_in_units(grid_y, y_units, dtype) - - # Create 2d meshgrids as required by _create_cube func. - meshgrid_x, meshgrid_y = _meshgrid(grid_x.points, grid_y.points) - - # Determine whether target grid bounds are decreasing. This must - # be determined prior to wrap_lons being called. - grid_x_decreasing = grid_x_bounds[-1, 0] < grid_x_bounds[0, 0] - grid_y_decreasing = grid_y_bounds[-1, 0] < grid_y_bounds[0, 0] - - # Wrapping of longitudes. - if spherical: - base = np.min(src_x_bounds) - modulus = x_units.modulus - # Only wrap if necessary to avoid introducing floating - # point errors. - if np.min(grid_x_bounds) < base or np.max(grid_x_bounds) > ( - base + modulus - ): - grid_x_bounds = iris.analysis.cartography.wrap_lons( - grid_x_bounds, base, modulus - ) - - # Determine whether the src_x coord has periodic boundary conditions. - circular = getattr(src_x, "circular", False) - - # Use simple cartesian area function or one that takes into - # account the curved surface if coord system is spherical. - if spherical: - area_func = _spherical_area - else: - area_func = _cartesian_area - - def _calculate_regrid_area_weighted_weights( - src_x_bounds, - src_y_bounds, - grid_x_bounds, - grid_y_bounds, - grid_x_decreasing, - grid_y_decreasing, - area_func, - circular=False, - ): - """ - Compute the area weights used for area-weighted regridding. - Args: - * src_x_bounds: - A NumPy array of bounds along the X axis defining the source grid. - * src_y_bounds: - A NumPy array of bounds along the Y axis defining the source grid. - * grid_x_bounds: - A NumPy array of bounds along the X axis defining the new grid. - * grid_y_bounds: - A NumPy array of bounds along the Y axis defining the new grid. - * grid_x_decreasing: - Boolean indicating whether the X coordinate of the new grid is - in descending order. - * grid_y_decreasing: - Boolean indicating whether the Y coordinate of the new grid is - in descending order. - * area_func: - A function that returns an (p, q) array of weights given an (p, 2) - shaped array of Y bounds and an (q, 2) shaped array of X bounds. - Kwargs: - * circular: - A boolean indicating whether the `src_x_bounds` are periodic. - Default is False. - Returns: - The area weights to be used for area-weighted regridding. - """ - # Determine which grid bounds are within src extent. - y_within_bounds = _within_bounds( - src_y_bounds, grid_y_bounds, grid_y_decreasing - ) - x_within_bounds = _within_bounds( - src_x_bounds, grid_x_bounds, grid_x_decreasing - ) - - # Cache which src_bounds are within grid bounds - cached_x_bounds = [] - cached_x_indices = [] - max_x_indices = 0 - for (x_0, x_1) in grid_x_bounds: - if grid_x_decreasing: - x_0, x_1 = x_1, x_0 - x_bounds, x_indices = _cropped_bounds(src_x_bounds, x_0, x_1) - cached_x_bounds.append(x_bounds) - cached_x_indices.append(x_indices) - # Keep record of the largest slice - if isinstance(x_indices, slice): - x_indices_size = np.sum(x_indices.stop - x_indices.start) - else: # is tuple of indices - x_indices_size = len(x_indices) - if x_indices_size > max_x_indices: - max_x_indices = x_indices_size - - # Cache which y src_bounds areas and weights are within grid bounds - cached_y_indices = [] - cached_weights = [] - max_y_indices = 0 - for j, (y_0, y_1) in enumerate(grid_y_bounds): - # Reverse lower and upper if dest grid is decreasing. - if grid_y_decreasing: - y_0, y_1 = y_1, y_0 - y_bounds, y_indices = _cropped_bounds(src_y_bounds, y_0, y_1) - cached_y_indices.append(y_indices) - # Keep record of the largest slice - if isinstance(y_indices, slice): - y_indices_size = np.sum(y_indices.stop - y_indices.start) - else: # is tuple of indices - y_indices_size = len(y_indices) - if y_indices_size > max_y_indices: - max_y_indices = y_indices_size - - weights_i = [] - for i, (x_0, x_1) in enumerate(grid_x_bounds): - # Reverse lower and upper if dest grid is decreasing. - if grid_x_decreasing: - x_0, x_1 = x_1, x_0 - x_bounds = cached_x_bounds[i] - x_indices = cached_x_indices[i] - - # Determine whether element i, j overlaps with src and hence - # an area weight should be computed. - # If x_0 > x_1 then we want [0]->x_1 and x_0->[0] + mod in the case - # of wrapped longitudes. However if the src grid is not global - # (i.e. circular) this new cell would include a region outside of - # the extent of the src grid and thus the weight is therefore - # invalid. - outside_extent = x_0 > x_1 and not circular - if ( - outside_extent - or not y_within_bounds[j] - or not x_within_bounds[i] - ): - weights = False - else: - # Calculate weights based on areas of cropped bounds. - if isinstance(x_indices, tuple) and isinstance( - y_indices, tuple - ): - raise RuntimeError( - "Cannot handle split bounds " "in both x and y." - ) - weights = area_func(y_bounds, x_bounds) - weights_i.append(weights) - cached_weights.append(weights_i) - return ( - tuple(cached_x_indices), - tuple(cached_y_indices), - max_x_indices, - max_y_indices, - tuple(cached_weights), - ) - - ( - cached_x_indices, - cached_y_indices, - max_x_indices, - max_y_indices, - cached_weights, - ) = _calculate_regrid_area_weighted_weights( - src_x_bounds, - src_y_bounds, - grid_x_bounds, - grid_y_bounds, - grid_x_decreasing, - grid_y_decreasing, - area_func, - circular, - ) - - # Go further, calculating the full weights array that we'll need in the - # perform step and the indices we'll need to extract from the cube we're - # regridding (src_data) - - result_y_extent = len(grid_y_bounds) - result_x_extent = len(grid_x_bounds) - - # Total number of points - num_target_pts = result_y_extent * result_x_extent - - # Create empty array to hold weights - src_area_weights = np.zeros( - list((max_y_indices, max_x_indices, num_target_pts)) - ) - - # Built for the case where the source cube isn't masked - blank_weights = np.zeros((num_target_pts,)) - new_data_mask_basis = np.full( - (len(cached_y_indices), len(cached_x_indices)), False, dtype=np.bool_ - ) - - # To permit fancy indexing, we need to store our data in an array whose - # first two dimensions represent the indices needed for the target cell. - # Since target cells can require a different number of indices, the size of - # these dimensions should be the maximum of this number. - # This means we need to track whether the data in - # that array is actually required and build those squared-off arrays - # TODO: Consider if a proper mask would be better - src_area_datas_required = np.full( - (max_y_indices, max_x_indices, num_target_pts), False - ) - square_data_indices_y = np.zeros( - (max_y_indices, max_x_indices, num_target_pts), dtype=int - ) - square_data_indices_x = np.zeros( - (max_y_indices, max_x_indices, num_target_pts), dtype=int - ) - - # Stack the weights for each target point and build the indices we'll need - # to extract the src_area_data - target_pt_ji = -1 - for j, y_indices in enumerate(cached_y_indices): - for i, x_indices in enumerate(cached_x_indices): - target_pt_ji += 1 - # Determine whether to mask element i, j based on whether - # there are valid weights. - weights = cached_weights[j][i] - if weights is False: - # Prepare for the src_data not being masked by storing the - # information that will let us fill the data with zeros and - # weights as one. The weighted average result will be the same, - # but we avoid dividing by zero. - blank_weights[target_pt_ji] = True - new_data_mask_basis[j, i] = True - else: - # Establish which indices are actually in y_indices and x_indices - if isinstance(y_indices, slice): - y_indices = list( - range( - y_indices.start, - y_indices.stop, - y_indices.step or 1, - ) - ) - else: - y_indices = list(y_indices) - - if isinstance(x_indices, slice): - x_indices = list( - range( - x_indices.start, - x_indices.stop, - x_indices.step or 1, - ) - ) - else: - x_indices = list(x_indices) - - # For the weights, we just need the lengths of these as we're - # dropping them into a pre-made array - - len_y = len(y_indices) - len_x = len(x_indices) - - src_area_weights[0:len_y, 0:len_x, target_pt_ji] = weights - - # To build the indices for the source cube, we need equal - # shaped array so we pad with 0s and record the need to mask - # them in src_area_datas_required - padded_y_indices = y_indices + [0] * (max_y_indices - len_y) - padded_x_indices = x_indices + [0] * (max_x_indices - len_x) - - square_data_indices_y[..., target_pt_ji] = np.array( - padded_y_indices - )[:, np.newaxis] - square_data_indices_x[..., target_pt_ji] = padded_x_indices - - src_area_datas_required[0:len_y, 0:len_x, target_pt_ji] = True - - # Package up the return data - - weights_info = ( - blank_weights, - src_area_weights, - new_data_mask_basis, - ) - - index_info = ( - result_x_extent, - result_y_extent, - square_data_indices_y, - square_data_indices_x, - src_area_datas_required, - ) - - # Now return it - - return ( - src_x, - src_y, - src_x_dim, - src_y_dim, - grid_x, - grid_y, - meshgrid_x, - meshgrid_y, - weights_info, - index_info, - ) - - -def _regrid_area_weighted_rectilinear_src_and_grid__perform( - src_cube, regrid_info, mdtol -): - """ - Second (regrid) part of 'regrid_area_weighted_rectilinear_src_and_grid'. - - Perform the prepared regrid calculation on a single 2d cube. +def regrid_weighted_curvilinear_to_rectilinear(src_cube, weights, grid_cube): + r""" + Return a new cube with the data values calculated using the weighted + mean of data values from :data:`src_cube` and the weights from + :data:`weights` regridded onto the horizontal grid of :data:`grid_cube`. - """ - ( - src_x, - src_y, - src_x_dim, - src_y_dim, - grid_x, - grid_y, - meshgrid_x, - meshgrid_y, - weights_info, - index_info, - ) = regrid_info - - # Calculate new data array for regridded cube. - regrid = functools.partial( - _regrid_area_weighted_array, - x_dim=src_x_dim, - y_dim=src_y_dim, - weights_info=weights_info, - index_info=index_info, - mdtol=mdtol, - ) + .. note :: - new_data = map_complete_blocks( - src_cube, regrid, (src_y_dim, src_x_dim), meshgrid_x.shape - ) + .. deprecated:: 3.2.0 - # Wrap up the data as a Cube. - regrid_callback = RectilinearRegridder._regrid - new_cube = RectilinearRegridder._create_cube( - new_data, - src_cube, - src_x_dim, - src_y_dim, - src_x, - src_y, - grid_x, - grid_y, - meshgrid_x, - meshgrid_y, - regrid_callback, - ) + This function is scheduled to be removed in a future release. + Please use :meth:`iris.cube.Cube.regrid` with the + :class:`iris.analysis.PointInCell` scheme instead : this is an exact + replacement. - # Slice out any length 1 dimensions. - indices = [slice(None, None)] * new_data.ndim - if src_x_dim is not None and new_cube.shape[src_x_dim] == 1: - indices[src_x_dim] = 0 - if src_y_dim is not None and new_cube.shape[src_y_dim] == 1: - indices[src_y_dim] = 0 - if 0 in indices: - new_cube = new_cube[tuple(indices)] + For example : - return new_cube + .. code:: - -def regrid_weighted_curvilinear_to_rectilinear(src_cube, weights, grid_cube): - r""" - Return a new cube with the data values calculated using the weighted - mean of data values from :data:`src_cube` and the weights from - :data:`weights` regridded onto the horizontal grid of :data:`grid_cube`. + result = src_cube.regrid(grid_cube, PointInCell()) This function requires that the :data:`src_cube` has a horizontal grid defined by a pair of X- and Y-axis coordinates which are mapped over the @@ -1134,6 +194,14 @@ def regrid_weighted_curvilinear_to_rectilinear(src_cube, weights, grid_cube): A :class:`iris.cube.Cube` instance. """ + wmsg = ( + "The function " + "'iris.experimental.regrid." + "regrid_weighted_curvilinear_to_rectilinear' " + "has been deprecated, and will be removed in a future release. " + "Please consult the docstring for details." + ) + warn_deprecated(wmsg) regrid_info = _regrid_weighted_curvilinear_to_rectilinear__prepare( src_cube, weights, grid_cube ) @@ -1572,6 +640,16 @@ def __init__(self, projection=None): Linear regridding scheme that uses scipy.interpolate.griddata on projected unstructured data. + .. note:: + + .. deprecated:: 3.2.0 + + This class is scheduled to be removed in a future release, and no + replacement is currently planned. + If you make use of this functionality, please contact the Iris + Developers to discuss how to retain it (which could include + reversing the deprecation). + Optional Args: * projection: `cartopy.crs instance` @@ -1581,6 +659,12 @@ def __init__(self, projection=None): """ self.projection = projection + wmsg = ( + "The class iris.experimental.regrid.ProjectedUnstructuredLinear " + "has been deprecated, and will be removed in a future release. " + "Please consult the docstring for details." + ) + warn_deprecated(wmsg) def regridder(self, src_cube, target_grid): """ @@ -1639,6 +723,17 @@ def __init__(self, projection=None): Nearest regridding scheme that uses scipy.interpolate.griddata on projected unstructured data. + .. note:: + + .. deprecated:: 3.2.0 + + This class is scheduled to be removed in a future release, and no + exact replacement is currently planned. + Please use :class:`iris.analysis.UnstructuredNearest` instead, if + possible. If you have a need for this exact functionality, please + contact the Iris Developers to discuss how to retain it (which + could include reversing the deprecation). + Optional Args: * projection: `cartopy.crs instance` @@ -1648,6 +743,13 @@ def __init__(self, projection=None): """ self.projection = projection + wmsg = ( + "iris.experimental.regrid.ProjectedUnstructuredNearest has been " + "deprecated, and will be removed in a future release. " + "Please use 'iris.analysis.UnstructuredNearest' instead, where " + "possible. Consult the docstring for details." + ) + warn_deprecated(wmsg) def regridder(self, src_cube, target_grid): """ diff --git a/lib/iris/experimental/regrid_conservative.py b/lib/iris/experimental/regrid_conservative.py index 421bd86c93..bfa048ddf0 100644 --- a/lib/iris/experimental/regrid_conservative.py +++ b/lib/iris/experimental/regrid_conservative.py @@ -6,16 +6,35 @@ """ Support for conservative regridding via ESMPy. +.. note:: + + .. deprecated:: 3.2.0 + + This package will be removed in a future release. + Please use + `iris-esmf-regrid `_ + instead. + """ import cartopy.crs as ccrs import numpy as np import iris +from iris._deprecation import warn_deprecated from iris.analysis._interpolation import get_xy_dim_coords from iris.analysis._regrid import RectilinearRegridder from iris.util import _meshgrid +wmsg = ( + "The 'iris.experimental.regrid_conservative' package is deprecated since " + "version 3.2, and will be removed in a future release. Please use " + "iris-emsf-regrid instead. " + "See https://github.com/SciTools-incubator/iris-esmf-regrid." +) +warn_deprecated(wmsg) + + #: A static Cartopy Geodetic() instance for transforming to true-lat-lons. _CRS_TRUELATLON = ccrs.Geodetic() @@ -131,6 +150,22 @@ def regrid_conservative_via_esmpy(source_cube, grid_cube): """ Perform a conservative regridding with ESMPy. + .. note :: + + .. deprecated:: 3.2.0 + + This function is scheduled to be removed in a future release. + Please use + `iris-esmf-regrid `_ + instead. + + For example : + + .. code:: + + from emsf_regrid.schemes import ESMFAreaWeighted + result = src_cube.regrid(grid_cube, ESMFAreaWeighted()) + Regrids the data of a source cube onto a new grid defined by a destination cube. @@ -169,6 +204,15 @@ def regrid_conservative_via_esmpy(source_cube, grid_cube): To alter this, make a prior call to ESMF.Manager(). """ + wmsg = ( + "The function " + "'iris.experimental.regrid_conservative." + "regrid_weighted_curvilinear_to_rectilinear' " + "has been deprecated, and will be removed in a future release. " + "Please consult the docstring for details." + ) + warn_deprecated(wmsg) + # Lazy import so we can build the docs with no ESMF. import ESMF diff --git a/lib/iris/experimental/ugrid/load.py b/lib/iris/experimental/ugrid/load.py index 5e42f2e606..6c802e00d4 100644 --- a/lib/iris/experimental/ugrid/load.py +++ b/lib/iris/experimental/ugrid/load.py @@ -280,7 +280,7 @@ def _build_aux_coord(coord_var, file_path): return coord, axis -def _build_connectivity(connectivity_var, file_path, location_dims): +def _build_connectivity(connectivity_var, file_path, element_dims): """ Construct a :class:`~iris.experimental.ugrid.mesh.Connectivity` from a given :class:`~iris.experimental.ugrid.cf.CFUGridConnectivityVariable`, @@ -301,10 +301,10 @@ def _build_connectivity(connectivity_var, file_path, location_dims): dim_names = connectivity_var.dimensions # Connectivity arrays must have two dimensions. assert len(dim_names) == 2 - if dim_names[1] in location_dims: - src_dim = 1 + if dim_names[1] in element_dims: + location_axis = 1 else: - src_dim = 0 + location_axis = 0 standard_name, long_name, var_name = get_names( connectivity_var, None, attributes @@ -319,7 +319,7 @@ def _build_connectivity(connectivity_var, file_path, location_dims): units=attr_units, attributes=attributes, start_index=start_index, - src_dim=src_dim, + location_axis=location_axis, ) return connectivity, dim_names[0] @@ -423,20 +423,20 @@ def _build_mesh(cf, mesh_var, file_path): raise ValueError(message) # Used for detecting transposed connectivities. - location_dims = (edge_dimension, face_dimension) + element_dims = (edge_dimension, face_dimension) connectivity_args = [] for connectivity_var in mesh_var.cf_group.connectivities.values(): connectivity, first_dim_name = _build_connectivity( - connectivity_var, file_path, location_dims + connectivity_var, file_path, element_dims ) assert connectivity.var_name == getattr(mesh_var, connectivity.cf_role) connectivity_args.append(connectivity) # If the mesh_var has not supplied the dimension name, it is safe to # fall back on the connectivity's first dimension's name. - if edge_dimension is None and connectivity.src_location == "edge": + if edge_dimension is None and connectivity.location == "edge": edge_dimension = first_dim_name - if face_dimension is None and connectivity.src_location == "face": + if face_dimension is None and connectivity.location == "face": face_dimension = first_dim_name standard_name, long_name, var_name = get_names(mesh_var, None, attributes) @@ -480,12 +480,12 @@ def _build_mesh_coords(mesh, cf_var): """ # TODO: integrate with standard saving API when no longer 'experimental'. # Identify the cube's mesh dimension, for attaching MeshCoords. - locations_dimensions = { + element_dimensions = { "node": mesh.node_dimension, "edge": mesh.edge_dimension, "face": mesh.face_dimension, } - mesh_dim_name = locations_dimensions[cf_var.location] + mesh_dim_name = element_dimensions[cf_var.location] # (Only expecting 1 mesh dimension per cf_var). mesh_dim = cf_var.dimensions.index(mesh_dim_name) diff --git a/lib/iris/experimental/ugrid/mesh.py b/lib/iris/experimental/ugrid/mesh.py index 0f2bfd844c..974a563046 100644 --- a/lib/iris/experimental/ugrid/mesh.py +++ b/lib/iris/experimental/ugrid/mesh.py @@ -12,9 +12,10 @@ """ from abc import ABC, abstractmethod from collections import namedtuple -import re +from collections.abc import Container from typing import Iterable +from cf_units import Unit from dask import array as da import numpy as np @@ -24,10 +25,11 @@ metadata_filter, metadata_manager_factory, ) +from ...common.metadata import BaseMetadata from ...config import get_logger from ...coords import AuxCoord, _DimensionalMetadata from ...exceptions import ConnectivityNotFoundError, CoordinateNotFoundError -from ...util import array_equal, guess_coord_axis +from ...util import array_equal, clip_string, guess_coord_axis from .metadata import ConnectivityMetadata, MeshCoordMetadata, MeshMetadata # Configure the logger. @@ -92,8 +94,8 @@ class Connectivity(_DimensionalMetadata): """ A CF-UGRID topology connectivity, describing the topological relationship - between two lists of dimensional locations. One or more connectivities - make up a CF-UGRID topology - a constituent of a CF-UGRID mesh. + between two types of mesh element. One or more connectivities make up a + CF-UGRID topology - a constituent of a CF-UGRID mesh. See: https://ugrid-conventions.github.io/ugrid-conventions @@ -122,7 +124,7 @@ def __init__( units=None, attributes=None, start_index=0, - src_dim=0, + location_axis=0, ): """ Constructs a single connectivity. @@ -130,17 +132,23 @@ def __init__( Args: * indices (numpy.ndarray or numpy.ma.core.MaskedArray or dask.array.Array): - The index values describing a topological relationship. Constructed - of 2 dimensions - the list of locations, and within each location: - the indices of the 'target locations' it relates to. - Use a :class:`numpy.ma.core.MaskedArray` if :attr:`src_location` - lengths vary - mask unused index 'slots' within each - :attr:`src_location`. Use a :class:`dask.array.Array` to keep - indices 'lazy'. + 2D array giving the topological connection relationship between + :attr:`location` elements and :attr:`connected` elements. + The :attr:`location_axis` dimension indexes over the + :attr:`location` dimension of the mesh - i.e. its length matches + the total number of :attr:`location` elements in the mesh. The + :attr:`connected_axis` dimension can be any length, corresponding + to the highest number of :attr:`connected` elements connected to a + :attr:`location` element. The array values are indices into the + :attr:`connected` dimension of the mesh. If the number of + :attr:`connected` elements varies between :attr:`location` + elements: use a :class:`numpy.ma.core.MaskedArray` and mask the + :attr:`location` elements' unused index 'slots'. Use a + :class:`dask.array.Array` to keep indices 'lazy'. * cf_role (str): Denotes the topological relationship that this connectivity - describes. Made up of this array's locations, and the indexed - 'target location' within each location. + describes. Made up of this array's :attr:`location`, and the + :attr:`connected` element type that is indexed by the array. See :attr:`UGRID_CF_ROLES` for valid arguments. Kwargs: @@ -164,14 +172,14 @@ def __init__( Either ``0`` or ``1``. Default is ``0``. Denotes whether :attr:`indices` uses 0-based or 1-based indexing (allows support for Fortran and legacy NetCDF files). - * src_dim (int): - Either ``0`` or ``1``. Default is ``0``. Denotes which dimension - of :attr:`indices` varies over the :attr:`src_location`\\ s (the - alternate dimension therefore varying within individual - :attr:`src_location`\\ s). (This parameter allows support for fastest varying index being + * location_axis (int): + Either ``0`` or ``1``. Default is ``0``. Denotes which axis + of :attr:`indices` varies over the :attr:`location` elements (the + alternate axis therefore varying over :attr:`connected` elements). + (This parameter allows support for fastest varying index being either first or last). E.g. for ``face_node_connectivity``, for 10 faces: - ``indices.shape[src_dim] = 10``. + ``indices.shape[location_axis] == 10``. """ @@ -188,15 +196,15 @@ def validate_arg_vs_list(arg_name, arg, valid_list): validate_arg_vs_list("start_index", start_index, [0, 1]) # indices array will be 2-dimensional, so must be either 0 or 1. - validate_arg_vs_list("src_dim", src_dim, [0, 1]) + validate_arg_vs_list("location_axis", location_axis, [0, 1]) validate_arg_vs_list("cf_role", cf_role, Connectivity.UGRID_CF_ROLES) self._metadata_manager.start_index = start_index - self._metadata_manager.src_dim = src_dim + self._metadata_manager.location_axis = location_axis self._metadata_manager.cf_role = cf_role - self._tgt_dim = 1 - src_dim - self._src_location, self._tgt_location = cf_role.split("_")[:2] + self._connected_axis = 1 - location_axis + self._location, self._connected = cf_role.split("_")[:2] super().__init__( values=indices, @@ -207,58 +215,6 @@ def validate_arg_vs_list(arg_name, arg, valid_list): attributes=attributes, ) - def __repr__(self): - def kwargs_filter(k, v): - result = False - if k != "cf_role": - if v is not None: - result = True - if ( - not isinstance(v, str) - and isinstance(v, Iterable) - and not v - ): - result = False - elif k == "units" and v == "unknown": - result = False - return result - - def array2repr(array): - if self.has_lazy_indices(): - result = repr(array) - else: - with np.printoptions( - threshold=NP_PRINTOPTIONS_THRESHOLD, - edgeitems=NP_PRINTOPTIONS_EDGEITEMS, - ): - result = re.sub("\n *", " ", repr(array)) - return result - - # positional arguments - args = ", ".join( - [ - f"{array2repr(self.core_indices())}", - f"cf_role={self.cf_role!r}", - ] - ) - - # optional arguments (metadata) - kwargs = ", ".join( - [ - f"{k}={v!r}" - for k, v in self.metadata._asdict().items() - if kwargs_filter(k, v) - ] - ) - - return f"{self.__class__.__name__}({', '.join([args, kwargs])})" - - def __str__(self): - args = ", ".join( - [f"cf_role={self.cf_role!r}", f"start_index={self.start_index!r}"] - ) - return f"{self.__class__.__name__}({args})" - @property def _values(self): # Overridden just to allow .setter override. @@ -283,25 +239,25 @@ def cf_role(self): return self._metadata_manager.cf_role @property - def src_location(self): + def location(self): """ Derived from the connectivity's :attr:`cf_role` - the first part, e.g. - ``face`` in ``face_node_connectivity``. Refers to the locations - listed by the :attr:`src_dim` of the connectivity's :attr:`indices` - array. + ``face`` in ``face_node_connectivity``. Refers to the elements that + vary along the :attr:`location_axis` of the connectivity's + :attr:`indices` array. """ - return self._src_location + return self._location @property - def tgt_location(self): + def connected(self): """ Derived from the connectivity's :attr:`cf_role` - the second part, e.g. - ``node`` in ``face_node_connectivity``. Refers to the locations indexed + ``node`` in ``face_node_connectivity``. Refers to the elements indexed by the values in the connectivity's :attr:`indices` array. """ - return self._tgt_location + return self._connected @property def start_index(self): @@ -316,47 +272,48 @@ def start_index(self): return self._metadata_manager.start_index @property - def src_dim(self): + def location_axis(self): """ - The dimension of the connectivity's :attr:`indices` array that varies - over the connectivity's :attr:`src_location`\\ s. Either ``0`` or ``1``. + The axis of the connectivity's :attr:`indices` array that varies + over the connectivity's :attr:`location` elements. Either ``0`` or ``1``. **Read-only** - validity of :attr:`indices` is dependent on - :attr:`src_dim`. Use :meth:`transpose` to create a new, transposed - :class:`Connectivity` if a different :attr:`src_dim` is needed. + :attr:`location_axis`. Use :meth:`transpose` to create a new, transposed + :class:`Connectivity` if a different :attr:`location_axis` is needed. """ - return self._metadata_manager.src_dim + return self._metadata_manager.location_axis @property - def tgt_dim(self): + def connected_axis(self): """ - Derived as the alternate value of :attr:`src_dim` - each must equal - either ``0`` or ``1``. - The dimension of the connectivity's :attr:`indices` array that varies - within the connectivity's individual :attr:`src_location`\\ s. + Derived as the alternate value of :attr:`location_axis` - each must + equal either ``0`` or ``1``. The axis of the connectivity's + :attr:`indices` array that varies over the :attr:`connected` elements + associated with each :attr:`location` element. """ - return self._tgt_dim + return self._connected_axis @property def indices(self): """ The index values describing the topological relationship of the connectivity, as a NumPy array. Masked points indicate a - :attr:`src_location` shorter than the longest :attr:`src_location` - described in this array - unused index 'slots' are masked. + :attr:`location` element with fewer :attr:`connected` elements than + other :attr:`location` elements described in this array - unused index + 'slots' are masked. **Read-only** - index values are only meaningful when combined with an appropriate :attr:`cf_role`, :attr:`start_index` and - :attr:`src_dim`. A new :class:`Connectivity` must therefore be + :attr:`location_axis`. A new :class:`Connectivity` must therefore be defined if different indices are needed. """ return self._values - def indices_by_src(self, indices=None): + def indices_by_location(self, indices=None): """ - Return a view of the indices array with :attr:`src_dim` **always** as - the first index - transposed if necessary. Can optionally pass in an + Return a view of the indices array with :attr:`location_axis` **always** as + the first axis - transposed if necessary. Can optionally pass in an identically shaped array on which to perform this operation (e.g. the output from :meth:`core_indices` or :meth:`lazy_indices`). @@ -368,7 +325,7 @@ def indices_by_src(self, indices=None): Returns: A view of the indices array, transposed - if necessary - to put - :attr:`src_dim` first. + :attr:`location_axis` first. """ if indices is None: @@ -380,20 +337,20 @@ def indices_by_src(self, indices=None): f"got shape={indices.shape} ." ) - if self.src_dim == 0: + if self.location_axis == 0: result = indices - elif self.src_dim == 1: + elif self.location_axis == 1: result = indices.transpose() else: - raise ValueError("Invalid src_dim.") + raise ValueError("Invalid location_axis.") return result def _validate_indices(self, indices, shapes_only=False): # Use shapes_only=True for a lower resource, less thorough validation # of indices by just inspecting the array shape instead of inspecting - # individual masks. So will not catch individual src_locations being - # unacceptably small. + # individual masks. So will not catch individual location elements + # having unacceptably low numbers of associated connected elements. def indices_error(message): raise ValueError("Invalid indices provided. " + message) @@ -422,43 +379,43 @@ def indices_error(message): len_req_fail = False if shapes_only: - src_shape = indices_shape[self.tgt_dim] + location_shape = indices_shape[self.connected_axis] # Wrap as lazy to allow use of the same operations below # regardless of shapes_only. - src_lengths = _lazy.as_lazy_data(np.asarray(src_shape)) + location_lengths = _lazy.as_lazy_data(np.asarray(location_shape)) else: # Wouldn't be safe to use during __init__ validation, since - # lazy_src_lengths requires self.indices to exist. Safe here since + # lazy_location_lengths requires self.indices to exist. Safe here since # shapes_only==False is only called manually, i.e. after # initialisation. - src_lengths = self.lazy_src_lengths() - if self.src_location in ("edge", "boundary"): - if (src_lengths != 2).any().compute(): + location_lengths = self.lazy_location_lengths() + if self.location in ("edge", "boundary"): + if (location_lengths != 2).any().compute(): len_req_fail = "len=2" else: - if self.src_location == "face": + if self.location == "face": min_size = 3 - elif self.src_location == "volume": - if self.tgt_location == "edge": + elif self.location == "volume": + if self.connected == "edge": min_size = 6 else: min_size = 4 else: raise NotImplementedError - if (src_lengths < min_size).any().compute(): + if (location_lengths < min_size).any().compute(): len_req_fail = f"len>={min_size}" if len_req_fail: indices_error( - f"Not all src_locations meet requirement: {len_req_fail} - " + f"Not all {self.location}s meet requirement: {len_req_fail} - " f"needed to describe '{self.cf_role}' ." ) def validate_indices(self): """ Perform a thorough validity check of this connectivity's - :attr:`indices`. Includes checking the sizes of individual - :attr:`src_location`\\ s (specified using masks on the - :attr:`indices` array) against the :attr:`cf_role`. + :attr:`indices`. Includes checking the number of :attr:`connected` + elements associated with each :attr:`location` element (specified using + masks on the :attr:`indices` array) against the :attr:`cf_role`. Raises a ``ValueError`` if any problems are encountered, otherwise passes silently. @@ -476,8 +433,8 @@ def __eq__(self, other): if isinstance(other, Connectivity): # Account for the fact that other could be the transposed equivalent # of self, which we consider 'safe' since the recommended - # interaction with the indices array is via indices_by_src, which - # corrects for this difference. (To enable this, src_dim does + # interaction with the indices array is via indices_by_location, which + # corrects for this difference. (To enable this, location_axis does # not participate in ConnectivityMetadata to ConnectivityMetadata # equivalence). if hasattr(other, "metadata"): @@ -486,22 +443,22 @@ def __eq__(self, other): if eq: eq = ( self.shape == other.shape - and self.src_dim == other.src_dim + and self.location_axis == other.location_axis ) or ( self.shape == other.shape[::-1] - and self.src_dim == other.tgt_dim + and self.location_axis == other.connected_axis ) if eq: eq = array_equal( - self.indices_by_src(self.core_indices()), - other.indices_by_src(other.core_indices()), + self.indices_by_location(self.core_indices()), + other.indices_by_location(other.core_indices()), ) return eq def transpose(self): """ Create a new :class:`Connectivity`, identical to this one but with the - :attr:`indices` array transposed and the :attr:`src_dim` value flipped. + :attr:`indices` array transposed and the :attr:`location_axis` value flipped. Returns: A new :class:`Connectivity` that is the transposed equivalent of @@ -517,7 +474,7 @@ def transpose(self): units=self.units, attributes=self.attributes, start_index=self.start_index, - src_dim=self.tgt_dim, + location_axis=self.connected_axis, ) return new_connectivity @@ -560,11 +517,11 @@ def has_lazy_indices(self): """ return super()._has_lazy_values() - def lazy_src_lengths(self): + def lazy_location_lengths(self): """ - Return a lazy array representing the lengths of each - :attr:`src_location` in the :attr:`src_dim` of the connectivity's - :attr:`indices` array, accounting for masks if present. + Return a lazy array representing the number of :attr:`connected` + elements associated with each of the connectivity's :attr:`location` + elements, accounting for masks if present. Accessing this method will never cause the :attr:`indices` values to be loaded. Similarly, calling methods on, or indexing, the returned Array @@ -574,26 +531,28 @@ def lazy_src_lengths(self): :attr:`indices` have already been loaded. Returns: - A lazy array, representing the lengths of each :attr:`src_location`. + A lazy array, representing the number of :attr:`connected` + elements associated with each :attr:`location` element. """ - src_mask_counts = da.sum( - da.ma.getmaskarray(self.indices), axis=self.tgt_dim + location_mask_counts = da.sum( + da.ma.getmaskarray(self.indices), axis=self.connected_axis ) - max_src_size = self.indices.shape[self.tgt_dim] - return max_src_size - src_mask_counts + max_location_size = self.indices.shape[self.connected_axis] + return max_location_size - location_mask_counts - def src_lengths(self): + def location_lengths(self): """ - Return a NumPy array representing the lengths of each - :attr:`src_location` in the :attr:`src_dim` of the connectivity's - :attr:`indices` array, accounting for masks if present. + Return a NumPy array representing the number of :attr:`connected` + elements associated with each of the connectivity's :attr:`location` + elements, accounting for masks if present. Returns: - A NumPy array, representing the lengths of each :attr:`src_location`. + A NumPy array, representing the number of :attr:`connected` + elements associated with each :attr:`location` element. """ - return self.lazy_src_lengths().compute() + return self.lazy_location_lengths().compute() def cube_dims(self, cube): """Not available on :class:`Connectivity`.""" @@ -606,7 +565,7 @@ def xml_element(self, doc): element.setAttribute("cf_role", self.cf_role) element.setAttribute("start_index", self.start_index) - element.setAttribute("src_dim", self.src_dim) + element.setAttribute("location_axis", self.location_axis) return element @@ -632,8 +591,8 @@ class Mesh(CFVariableMixin): AXES = ("x", "y") #: Valid range of values for ``topology_dimension``. TOPOLOGY_DIMENSIONS = (1, 2) - #: Valid mesh locations. - LOCATIONS = ("edge", "node", "face") + #: Valid mesh elements. + ELEMENTS = ("edge", "node", "face") def __init__( self, @@ -684,12 +643,12 @@ def __init__( self.attributes = attributes # based on the topology_dimension, create the appropriate coordinate manager - def normalise(location, axis): + def normalise(element, axis): result = str(axis).lower() if result not in self.AXES: - emsg = f"Invalid axis specified for {location} coordinate {coord.name()!r}, got {axis!r}." + emsg = f"Invalid axis specified for {element} coordinate {coord.name()!r}, got {axis!r}." raise ValueError(emsg) - return f"{location}_{result}" + return f"{element}_{result}" if not isinstance(node_coords_and_axes, Iterable): node_coords_and_axes = [node_coords_and_axes] @@ -975,77 +934,143 @@ def __ne__(self, other): result = not result return result - def __repr__(self): - def to_coord_and_axis(members): - def axis(member): - return member.split("_")[1] - - result = [ - f"({coord!s}, {axis(member)!r})" - for member, coord in members._asdict().items() - if coord is not None - ] - result = f"[{', '.join(result)}]" if result else None - return result + def summary(self, shorten=False): + """ + Return a string representation of the Mesh. - node_coords_and_axes = to_coord_and_axis(self.node_coords) - connectivities = [ - str(connectivity) - for connectivity in self.all_connectivities - if connectivity is not None - ] + Parameters + ---------- + shorten : bool, default = False + If True, produce a oneline string form of the form . + If False, produce a multi-line detailed print output. + + Returns + ------- + result : str - if len(connectivities) == 1: - connectivities = connectivities[0] + """ + if shorten: + result = self._summary_oneline() else: - connectivities = f"[{', '.join(connectivities)}]" + result = self._summary_multiline() + return result - # positional arguments - args = [ - f"topology_dimension={self.topology_dimension!r}", - f"node_coords_and_axes={node_coords_and_axes}", - f"connectivities={connectivities}", - ] + def __repr__(self): + return self.summary(shorten=True) - # optional argument - edge_coords_and_axes = to_coord_and_axis(self.edge_coords) - if edge_coords_and_axes: - args.append(f"edge_coords_and_axes={edge_coords_and_axes}") - - # optional argument - if self.topology_dimension > 1: - face_coords_and_axes = to_coord_and_axis(self.face_coords) - if face_coords_and_axes: - args.append(f"face_coords_and_axes={face_coords_and_axes}") - - def kwargs_filter(k, v): - result = False - if k != "topology_dimension": - if not ( - self.topology_dimension == 1 and k == "face_dimension" - ): - if v is not None: - result = True - if ( - not isinstance(v, str) - and isinstance(v, Iterable) - and not v - ): - result = False - elif k == "units" and v == "unknown": - result = False - return result - - # optional arguments (metadata) - args.extend( - [ - f"{k}={v!r}" - for k, v in self.metadata._asdict().items() - if kwargs_filter(k, v) - ] - ) + def __str__(self): + return self.summary(shorten=False) + + def _summary_oneline(self): + # We use the repr output to produce short one-line identity summary, + # similar to the object.__str__ output "". + # This form also used in other str() constructions, like MeshCoord. + # By contrast, __str__ (below) produces a readable multi-line printout. + mesh_name = self.name() + if mesh_name in (None, "", "unknown"): + mesh_name = None + if mesh_name: + # Use a more human-readable form + mesh_string = f"" + else: + # Mimic the generic object.__str__ style. + mesh_id = id(self) + mesh_string = f"" - return f"{self.__class__.__name__}({', '.join(args)})" + return mesh_string + + def _summary_multiline(self): + # Produce a readable multi-line summary of the Mesh content. + lines = [] + n_indent = 4 + indent_str = " " * n_indent + + def line(text, i_indent=0): + indent = indent_str * i_indent + lines.append(f"{indent}{text}") + + line(f"Mesh : '{self.name()}'") + line(f"topology_dimension: {self.topology_dimension}", 1) + for element in ("node", "edge", "face"): + if element == "node": + element_exists = True + else: + main_conn_name = f"{element}_node_connectivity" + main_conn = getattr(self, main_conn_name, None) + element_exists = main_conn is not None + if element_exists: + # Include a section for this element + line(element, 1) + # Print element dimension + dim_name = f"{element}_dimension" + dim = getattr(self, dim_name) + line(f"{dim_name}: '{dim}'", 2) + # Print defining connectivity (except node) + if element != "node": + main_conn_string = main_conn.summary( + shorten=True, linewidth=0 + ) + line(f"{main_conn_name}: {main_conn_string}", 2) + # Print coords + include_key = f"include_{element}s" + coords = self.coords(**{include_key: True}) + if coords: + line(f"{element} coordinates", 2) + for coord in coords: + coord_string = coord.summary(shorten=True, linewidth=0) + line(coord_string, 3) + + # Having dealt with essential info, now add any optional connectivities + # N.B. includes boundaries: as optional connectivity, not an "element" + optional_conn_names = ( + "boundary_connectivity", + "face_face_connectivity", + "face_edge_connectivity", + "edge_face_connectivity", + ) + optional_conns = [ + getattr(self, name, None) for name in optional_conn_names + ] + optional_conns = { + name: conn + for conn, name in zip(optional_conns, optional_conn_names) + if conn is not None + } + if optional_conns: + line("optional connectivities", 1) + for name, conn in optional_conns.items(): + conn_string = conn.summary(shorten=True, linewidth=0) + line(f"{name}: {conn_string}", 2) + + # Output the detail properties, basically those from CFVariableMixin + for name in BaseMetadata._members: + val = getattr(self, name, None) + if val is not None: + if name == "units": + show = val.origin != Unit(None) + elif isinstance(val, Container): + show = bool(val) + else: + show = val is not None + if show: + if name == "attributes": + # Use a multi-line form for this. + line("attributes:", 1) + max_attname_len = max(len(attr) for attr in val.keys()) + for attrname, attrval in val.items(): + attrname = attrname.ljust(max_attname_len) + if isinstance(attrval, str): + # quote strings + attrval = repr(attrval) + # and abbreviate really long ones + attrval = clip_string(attrval) + attr_string = f"{attrname} {attrval}" + line(attr_string, 2) + else: + line(f"{name}: {val!r}", 1) + + result = "\n".join(lines) + return result def __setstate__(self, state): metadata_manager, coord_manager, connectivity_manager = state @@ -1375,17 +1400,17 @@ def connectivities( :class:`~iris.experimental.ugrid.mesh.Connectivity`. * contains_node (bool): - Contains the ``node`` location as part of the + Contains the ``node`` element as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. * contains_edge (bool): - Contains the ``edge`` location as part of the + Contains the ``edge`` element as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. * contains_face (bool): - Contains the ``face`` location as part of the + Contains the ``face`` element as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. @@ -1476,17 +1501,17 @@ def connectivity( :class:`~iris.experimental.ugrid.mesh.Connectivity`. * contains_node (bool): - Contains the ``node`` location as part of the + Contains the ``node`` element as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. * contains_edge (bool): - Contains the ``edge`` location as part of the + Contains the ``edge`` element as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. * contains_face (bool): - Contains the ``face`` location as part of the + Contains the ``face`` element as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched. @@ -1743,17 +1768,17 @@ def remove_connectivities( :class:`~iris.experimental.ugrid.mesh.Connectivity`. * contains_node (bool): - Contains the ``node`` location as part of the + Contains the ``node`` element as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched for potential removal. * contains_edge (bool): - Contains the ``edge`` location as part of the + Contains the ``edge`` element as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched for potential removal. * contains_face (bool): - Contains the ``face`` location as part of the + Contains the ``face`` element as part of the :attr:`~iris.experimental.ugrid.metadata.ConnectivityMetadata.cf_role` in the list of objects to be matched for potential removal. @@ -2089,12 +2114,12 @@ def _remove(self, **kwargs): return result - def _setter(self, location, axis, coord, shape): + def _setter(self, element, axis, coord, shape): axis = axis.lower() - member = f"{location}_{axis}" + member = f"{element}_{axis}" # enforce the UGRID minimum coordinate requirement - if location == "node" and coord is None: + if element == "node" and coord is None: emsg = ( f"{member!r} is a required coordinate, cannot set to 'None'." ) @@ -2121,22 +2146,22 @@ def _setter(self, location, axis, coord, shape): self._members[member] = coord - def _shape(self, location): - coord = getattr(self, f"{location}_x") + def _shape(self, element): + coord = getattr(self, f"{element}_x") shape = coord.shape if coord is not None else None if shape is None: - coord = getattr(self, f"{location}_y") + coord = getattr(self, f"{element}_y") if coord is not None: shape = coord.shape return shape @property def _edge_shape(self): - return self._shape(location="edge") + return self._shape(element="edge") @property def _node_shape(self): - return self._shape(location="node") + return self._shape(element="node") @property def all_members(self): @@ -2153,7 +2178,7 @@ def edge_x(self): @edge_x.setter def edge_x(self, coord): self._setter( - location="edge", axis="x", coord=coord, shape=self._edge_shape + element="edge", axis="x", coord=coord, shape=self._edge_shape ) @property @@ -2163,7 +2188,7 @@ def edge_y(self): @edge_y.setter def edge_y(self, coord): self._setter( - location="edge", axis="y", coord=coord, shape=self._edge_shape + element="edge", axis="y", coord=coord, shape=self._edge_shape ) @property @@ -2177,7 +2202,7 @@ def node_x(self): @node_x.setter def node_x(self, coord): self._setter( - location="node", axis="x", coord=coord, shape=self._node_shape + element="node", axis="x", coord=coord, shape=self._node_shape ) @property @@ -2187,7 +2212,7 @@ def node_y(self): @node_y.setter def node_y(self, coord): self._setter( - location="node", axis="y", coord=coord, shape=self._node_shape + element="node", axis="y", coord=coord, shape=self._node_shape ) def _add(self, coords): @@ -2363,7 +2388,7 @@ def __init__( @property def _face_shape(self): - return self._shape(location="face") + return self._shape(element="face") @property def all_members(self): @@ -2380,7 +2405,7 @@ def face_x(self): @face_x.setter def face_x(self, coord): self._setter( - location="face", axis="x", coord=coord, shape=self._face_shape + element="face", axis="x", coord=coord, shape=self._face_shape ) @property @@ -2390,7 +2415,7 @@ def face_y(self): @face_y.setter def face_y(self, coord): self._setter( - location="face", axis="y", coord=coord, shape=self._face_shape + element="face", axis="y", coord=coord, shape=self._face_shape ) def add( @@ -2510,24 +2535,20 @@ def add(self, *connectivities): # Validate shapes. proposed_members = {**self._members, **add_dict} - locations = set( - [ - c.src_location - for c in proposed_members.values() - if c is not None - ] + elements = set( + [c.location for c in proposed_members.values() if c is not None] ) - for location in locations: + for element in elements: counts = [ - len(c.indices_by_src(c.lazy_indices())) + len(c.indices_by_location(c.lazy_indices())) for c in proposed_members.values() - if c is not None and c.src_location == location + if c is not None and c.location == element ] # Check is list values are identical. if not counts.count(counts[0]) == len(counts): message = ( f"Invalid Connectivities provided - inconsistent " - f"{location} counts." + f"{element} counts." ) raise ValueError(message) @@ -2582,13 +2603,16 @@ def filters( instance for instance in members if instance.cf_role == cf_role ] - def location_filter(instances, loc_arg, loc_name): + def element_filter(instances, loc_arg, loc_name): if loc_arg is False: filtered = [ instance for instance in instances if loc_name - not in (instance.src_location, instance.tgt_location) + not in ( + instance.location, + instance.connected, + ) ] elif loc_arg is None: filtered = instances @@ -2597,8 +2621,7 @@ def location_filter(instances, loc_arg, loc_name): filtered = [ instance for instance in instances - if loc_name - in (instance.src_location, instance.tgt_location) + if loc_name in (instance.location, instance.connected) ] return filtered @@ -2608,7 +2631,7 @@ def location_filter(instances, loc_arg, loc_name): (contains_edge, "edge"), (contains_face, "face"), ): - members = location_filter(members, arg, loc) + members = element_filter(members, arg, loc) # No need to actually modify filtering behaviour - already won't return # any face cf-roles if none are present. @@ -2790,10 +2813,10 @@ def __init__( # NOTE: currently *not* included in metadata. In future it might be. self._mesh = mesh - if location not in Mesh.LOCATIONS: + if location not in Mesh.ELEMENTS: msg = ( f"'location' of {location} is not a valid Mesh location', " - f"must be one of {Mesh.LOCATIONS}." + f"must be one of {Mesh.ELEMENTS}." ) raise ValueError(msg) # Held in metadata, readable as self.location, but cannot set it. @@ -2943,62 +2966,62 @@ def __eq__(self, other): def __hash__(self): return hash(id(self)) - def _string_summary(self, repr_style): - # Note: bypass the immediate parent here, which is Coord, because we - # have no interest in reporting coord_system or climatological, or in - # printing out our points/bounds. - # We also want to list our defining properties, i.e. mesh/location/axis - # *first*, before names/units etc, so different from other Coord types. - - # First construct a shortform text summary to identify the Mesh. - # IN 'str-mode', this attempts to use Mesh.name() if it is set, - # otherwise uses an object-id style (as also for 'repr-mode'). - # TODO: use a suitable method provided by Mesh, e.g. something like - # "Mesh.summary(shorten=True)", when it is available. - mesh_name = None - if not repr_style: - mesh_name = self.mesh.name() - if mesh_name in (None, "", "unknown"): - mesh_name = None - if mesh_name: - # Use a more human-readable form - mesh_string = f"Mesh({mesh_name!r})" + def summary(self, *args, **kwargs): + # We need to specialise _DimensionalMetadata.summary, so that we always + # print the mesh+location of a MeshCoord. + if len(args) > 0: + shorten = args[0] else: - # Mimic the generic object.__str__ style. - mesh_id = id(self.mesh) - mesh_string = f"" - result = ( - f"mesh={mesh_string}" - f", location={self.location!r}" - f", axis={self.axis!r}" - ) - # Add 'other' metadata that is drawn from the underlying node-coord. - # But put these *afterward*, unlike other similar classes. - for item in ( - "shape", - "standard_name", - "units", - "long_name", - "attributes", - ): - # NOTE: order of these matches Coord.summary, but omit var_name. - val = getattr(self, item, None) - if item == "attributes": - is_blank = len(val) == 0 # an empty dict is as good as none - else: - is_blank = val is None - if not is_blank: - result += f", {item}={val!r}" - - result = f"MeshCoord({result})" + shorten = kwargs.get("shorten", False) + + # Get the default-form result. + if shorten: + # NOTE: we simply aren't interested in the values for the repr, + # so fix linewidth to suppress them + kwargs["linewidth"] = 1 + + # Plug private key, to get back the section structure info + section_indices = {} + kwargs["_section_indices"] = section_indices + result = super().summary(*args, **kwargs) + + # Modify the generic 'default-form' result to produce what we want. + if shorten: + # Single-line form : insert mesh+location before the array part + # Construct a text detailing the mesh + location + mesh_string = self.mesh.name() + if mesh_string == "unknown": + # If no name, replace with the one-line summary + mesh_string = self.mesh.summary(shorten=True) + extra_str = f"mesh({mesh_string}) location({self.location}) " + # find where in the line the data-array text begins + i_line, i_array = section_indices["data"] + assert i_line == 0 + # insert the extra text there + result = result[:i_array] + extra_str + result[i_array:] + # NOTE: this invalidates the original width calculation and may + # easily extend the result beyond the intended maximum linewidth. + # We do treat that as an advisory control over array printing, not + # an absolute contract, so just ignore the problem for now. + else: + # Multiline form + # find where the "location: ... " section is + i_location, i_namestart = section_indices["location"] + lines = result.split("\n") + location_line = lines[i_location] + # copy the indent spacing + indent = location_line[:i_namestart] + # use that to construct a suitable 'mesh' line + mesh_string = self.mesh.summary(shorten=True) + mesh_line = f"{indent}mesh: {mesh_string}" + # Move the 'location' line, putting it and the 'mesh' line right at + # the top, immediately after the header line. + del lines[i_location] + lines[1:1] = [mesh_line, location_line] + # Re-join lines to give the result + result = "\n".join(lines) return result - def __str__(self): - return self._string_summary(repr_style=False) - - def __repr__(self): - return self._string_summary(repr_style=True) - def _construct_access_arrays(self): """ Build lazy points and bounds arrays, providing dynamic access via the @@ -3035,7 +3058,7 @@ def _construct_access_arrays(self): # Data can be real or lazy, so operations must work in Dask, too. indices = bounds_connectivity.core_indices() # Normalise indices dimension order to [faces/edges, bounds] - indices = bounds_connectivity.indices_by_src(indices) + indices = bounds_connectivity.indices_by_location(indices) # Normalise the start index indices = indices - bounds_connectivity.start_index diff --git a/lib/iris/experimental/ugrid/metadata.py b/lib/iris/experimental/ugrid/metadata.py index 94128cdf50..ae0b787908 100644 --- a/lib/iris/experimental/ugrid/metadata.py +++ b/lib/iris/experimental/ugrid/metadata.py @@ -28,9 +28,9 @@ class ConnectivityMetadata(BaseMetadata): """ - # The "src_dim" member is stateful only, and does not participate in + # The "location_axis" member is stateful only, and does not participate in # lenient/strict equivalence. - _members = ("cf_role", "start_index", "src_dim") + _members = ("cf_role", "start_index", "location_axis") __slots__ = () @@ -53,7 +53,7 @@ def _combine_lenient(self, other): A list of combined metadata member values. """ - # Perform "strict" combination for "cf_role", "start_index", "src_dim". + # Perform "strict" combination for "cf_role", "start_index", "location_axis". def func(field): left = getattr(self, field) right = getattr(other, field) @@ -82,9 +82,10 @@ def _compare_lenient(self, other): """ # Perform "strict" comparison for "cf_role", "start_index". - # The "src_dim" member is not part of lenient equivalence. + # The "location_axis" member is not part of lenient equivalence. members = filter( - lambda member: member != "src_dim", ConnectivityMetadata._members + lambda member: member != "location_axis", + ConnectivityMetadata._members, ) result = all( [ @@ -112,7 +113,7 @@ def _difference_lenient(self, other): A list of difference metadata member values. """ - # Perform "strict" difference for "cf_role", "start_index", "src_dim". + # Perform "strict" difference for "cf_role", "start_index", "location_axis". def func(field): left = getattr(self, field) right = getattr(other, field) diff --git a/lib/iris/fileformats/__init__.py b/lib/iris/fileformats/__init__.py index f2b0cfc095..96a848deb0 100644 --- a/lib/iris/fileformats/__init__.py +++ b/lib/iris/fileformats/__init__.py @@ -17,7 +17,7 @@ UriProtocol, ) -from . import abf, name, netcdf, nimrod, pp, um +from . import name, netcdf, nimrod, pp, um __all__ = ["FORMAT_AGENT"] @@ -224,16 +224,23 @@ def _load_grib(*args, **kwargs): # # ABF/ABL +# TODO: now deprecated, remove later # +def load_cubes_abf_abl(*args, **kwargs): + from . import abf + + return abf.load_cubes(*args, **kwargs) + + FORMAT_AGENT.add_spec( FormatSpecification( - "ABF", FileExtension(), ".abf", abf.load_cubes, priority=3 + "ABF", FileExtension(), ".abf", load_cubes_abf_abl, priority=3 ) ) FORMAT_AGENT.add_spec( FormatSpecification( - "ABL", FileExtension(), ".abl", abf.load_cubes, priority=3 + "ABL", FileExtension(), ".abl", load_cubes_abf_abl, priority=3 ) ) diff --git a/lib/iris/fileformats/_ff_cross_references.py b/lib/iris/fileformats/_ff_cross_references.py index 0c7af26324..ca41f5257f 100644 --- a/lib/iris/fileformats/_ff_cross_references.py +++ b/lib/iris/fileformats/_ff_cross_references.py @@ -3,11 +3,9 @@ # 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. -# -# DO NOT EDIT: AUTO-GENERATED """ -Auto-generated from iris/tools/gen_stash_refs.py -Relates grid code and field code to the stash code. +Table providing UM grid-code, field-code and pseudolevel-type for (some) +stash codes. Used in UM file i/o. """ diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index e3b63d41d2..d286abbf3d 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -184,16 +184,19 @@ def action_provides_coordinate(engine, dimcoord_fact): # Identify the "type" of a coordinate variable coord_type = None - # NOTE: must test for rotated cases *first*, as 'is_longitude' and - # 'is_latitude' functions also accept rotated cases. - if hh.is_rotated_latitude(engine, var_name): - coord_type = "rotated_latitude" - elif hh.is_rotated_longitude(engine, var_name): - coord_type = "rotated_longitude" - elif hh.is_latitude(engine, var_name): - coord_type = "latitude" + + if hh.is_latitude(engine, var_name): + # N.B. result of 'is_rotated_lat/lon' checks are valid ONLY when the + # relevant 'is_lat/lon' is also True. + if hh.is_rotated_latitude(engine, var_name): + coord_type = "rotated_latitude" + else: + coord_type = "latitude" elif hh.is_longitude(engine, var_name): - coord_type = "longitude" + if hh.is_rotated_longitude(engine, var_name): + coord_type = "rotated_longitude" + else: + coord_type = "longitude" elif hh.is_time(engine, var_name): coord_type = "time" elif hh.is_time_period(engine, var_name): diff --git a/lib/iris/fileformats/abf.py b/lib/iris/fileformats/abf.py index 678d9b04cf..5c70c5acf2 100644 --- a/lib/iris/fileformats/abf.py +++ b/lib/iris/fileformats/abf.py @@ -23,11 +23,20 @@ import numpy.ma as ma import iris +from iris._deprecation import warn_deprecated from iris.coord_systems import GeogCS from iris.coords import AuxCoord, DimCoord import iris.fileformats import iris.io.format_picker +wmsg = ( + "iris.fileformats.abf has been deprecated and will be removed in a " + "future release. If you make use of this functionality, please contact " + "the Iris Developers to discuss how to retain it (which may involve " + "reversing the deprecation)." +) +warn_deprecated(wmsg) + X_SIZE = 4320 Y_SIZE = 2160 diff --git a/lib/iris/fileformats/dot.py b/lib/iris/fileformats/dot.py index cc857c7f6b..2fb628bebf 100644 --- a/lib/iris/fileformats/dot.py +++ b/lib/iris/fileformats/dot.py @@ -12,8 +12,17 @@ import subprocess import iris +from iris._deprecation import warn_deprecated import iris.util +wmsg = ( + "iris.fileformats.dot has been deprecated and will be removed in a " + "future release. If you make use of this functionality, please contact " + "the Iris Developers to discuss how to retain it (which may involve " + "reversing the deprecation)." +) +warn_deprecated(wmsg) + _GRAPH_INDENT = " " * 4 _SUBGRAPH_INDENT = " " * 8 diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 77134259ad..73a137b4af 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -972,9 +972,9 @@ def __setitem__(self, keys, arr): self.target[keys] = arr -# NOTE : this matches :class:`iris.experimental.ugrid.mesh.Mesh.LOCATIONS`, +# NOTE : this matches :class:`iris.experimental.ugrid.mesh.Mesh.ELEMENTS`, # but in the preferred order for coord/connectivity variables in the file. -MESH_LOCATIONS = ("node", "edge", "face") +MESH_ELEMENTS = ("node", "edge", "face") class Saver: @@ -1422,7 +1422,7 @@ def _add_mesh(self, cube_or_mesh): mesh_dims = self._mesh_dims[mesh] # Add all the element coordinate variables. - for location in MESH_LOCATIONS: + for location in MESH_ELEMENTS: coords_meshobj_attr = f"{location}_coords" coords_file_attr = f"{location}_coordinates" mesh_coords = getattr(mesh, coords_meshobj_attr, None) @@ -1460,7 +1460,7 @@ def _add_mesh(self, cube_or_mesh): last_dim = f"{cf_mesh_name}_{loc_from}_N_{loc_to}s" # Create if it does not already exist. if last_dim not in self._dataset.dimensions: - length = conn.shape[1 - conn.src_dim] + length = conn.shape[1 - conn.location_axis] self._dataset.createDimension(last_dim, length) # Create variable. @@ -1470,7 +1470,7 @@ def _add_mesh(self, cube_or_mesh): # when it is first created. loc_dim_name = mesh_dims[loc_from] conn_dims = (loc_dim_name, last_dim) - if conn.src_dim == 1: + if conn.location_axis == 1: # Has the 'other' dimension order, =reversed conn_dims = conn_dims[::-1] if iris.util.is_masked(conn.core_indices()): @@ -1494,7 +1494,7 @@ def _add_mesh(self, cube_or_mesh): _setncattr(cf_mesh_var, cf_conn_attr_name, cf_conn_name) # If the connectivity had the 'alternate' dimension order, add the # relevant dimension property - if conn.src_dim == 1: + if conn.location_axis == 1: loc_dim_attr = f"{loc_from}_dimension" # Should only get here once. assert loc_dim_attr not in cf_mesh_var.ncattrs() @@ -1813,7 +1813,7 @@ def record_dimension(names_list, dim_name, length, matching_coords=[]): # NOTE: one of these will be a cube dimension, but that one does not # get any special handling. We *do* want to list/create them in a # definite order (node,edge,face), and before non-mesh dimensions. - for location in MESH_LOCATIONS: + for location in MESH_ELEMENTS: # Find if this location exists in the mesh, and a characteristic # coordinate to identify it with. # To use only _required_ UGRID components, we use a location @@ -1850,7 +1850,9 @@ def record_dimension(names_list, dim_name, length, matching_coords=[]): (dim_length,) = dim_element.shape else: # extract source dim, respecting dim-ordering - dim_length = dim_element.shape[dim_element.src_dim] + dim_length = dim_element.shape[ + dim_element.location_axis + ] # Name it for the relevant mesh dimension location_dim_attr = f"{location}_dimension" dim_name = getattr(mesh, location_dim_attr) @@ -2736,9 +2738,9 @@ def _create_cf_data_variable( cmin, cmax = _co_realise_lazy_arrays([cmin, cmax]) n = dtype.itemsize * 8 if masked: - scale_factor = (cmax - cmin) / (2 ** n - 2) + scale_factor = (cmax - cmin) / (2**n - 2) else: - scale_factor = (cmax - cmin) / (2 ** n - 1) + scale_factor = (cmax - cmin) / (2**n - 1) if dtype.kind == "u": add_offset = cmin elif dtype.kind == "i": diff --git a/lib/iris/fileformats/pp.py b/lib/iris/fileformats/pp.py index 9f213ec4db..9bda98bf61 100644 --- a/lib/iris/fileformats/pp.py +++ b/lib/iris/fileformats/pp.py @@ -403,7 +403,7 @@ def _calculate_str_value_from_value(self): def _calculate_value_from_str_value(self): self._value = np.sum( - [10 ** i * val for i, val in enumerate(self._strvalue)] + [10**i * val for i, val in enumerate(self._strvalue)] ) def __len__(self): @@ -418,7 +418,7 @@ def __getitem__(self, key): # if the key returns a list of values, then combine them together # to an integer if isinstance(val, list): - val = sum([10 ** i * val for i, val in enumerate(val)]) + val = sum([10**i * val for i, val in enumerate(val)]) return val diff --git a/lib/iris/fileformats/um_cf_map.py b/lib/iris/fileformats/um_cf_map.py index c2a0a5d09e..8aee67ae3e 100644 --- a/lib/iris/fileformats/um_cf_map.py +++ b/lib/iris/fileformats/um_cf_map.py @@ -3,14 +3,6 @@ # 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. -# -# DO NOT EDIT: AUTO-GENERATED -# Created on 29 November 2019 14:11 from -# http://www.metarelate.net/metOcean -# at commit 448f2ef5e676edaaa27408b9f3ddbecbf05e3289 -# -# https://github.com/metarelate/metOcean/commit/448f2ef5e676edaaa27408b9f3ddbecbf05e3289 - """ Provides UM/CF phenomenon translations. @@ -689,7 +681,7 @@ 'm01s09i212': CFName(None, 'cloud_base_altitude_assuming_only_consider_cloud_area_fraction_greater_than_4p5_oktas', 'kft'), 'm01s09i213': CFName(None, 'cloud_base_altitude_assuming_only_consider_cloud_area_fraction_greater_than_5p5_oktas', 'kft'), 'm01s09i214': CFName(None, 'cloud_base_altitude_assuming_only_consider_cloud_area_fraction_greater_than_6p5_oktas', 'kft'), - 'm01s09i215': CFName(None, 'cloud_base_altitude_assuming_only_consider_cloud_area_fraction_greater_than_7p5_oktas', 'kft'), + 'm01s09i215': CFName(None, 'cloud_base_altitude_assuming_only_consider_cloud_area_fraction_greater_than_7p9_oktas', 'kft'), 'm01s09i216': CFName(None, 'cloud_area_fraction_assuming_random_overlap', '1'), 'm01s09i217': CFName(None, 'cloud_area_fraction_assuming_maximum_random_overlap', '1'), 'm01s09i218': CFName(None, 'cloud_area_fraction_assuming_only_consider_surface_to_1000_feet_asl', '1'), diff --git a/lib/iris/io/__init__.py b/lib/iris/io/__init__.py index 64501afd1e..034fa4baab 100644 --- a/lib/iris/io/__init__.py +++ b/lib/iris/io/__init__.py @@ -12,6 +12,7 @@ from collections import OrderedDict import glob import os.path +import pathlib import re import iris.exceptions @@ -85,8 +86,9 @@ def decode_uri(uri, default="file"): r""" Decodes a single URI into scheme and scheme-specific parts. - In addition to well-formed URIs, it also supports bare file paths. - Both Windows and UNIX style paths are accepted. + In addition to well-formed URIs, it also supports bare file paths as strings + or :class:`pathlib.PurePath`. Both Windows and UNIX style paths are + accepted. .. testsetup:: @@ -113,6 +115,8 @@ def decode_uri(uri, default="file"): ('file', 'dataZoo/...') """ + if isinstance(uri, pathlib.PurePath): + uri = str(uri) # make sure scheme has at least 2 letters to avoid windows drives # put - last in the brackets so it refers to the character, not a range # reference on valid schemes: http://tools.ietf.org/html/std66#section-3.1 @@ -312,7 +316,8 @@ def find_saver(filespec): Args: - * filespec - A string such as "my_file.pp" or "PP". + * filespec + A string such as "my_file.pp" or "PP". Returns: A save function or None. @@ -359,7 +364,8 @@ def save(source, target, saver=None, **kwargs): * target: A filename (or writeable, depending on file format). When given a filename or file, Iris can determine the - file format. + file format. Filename can be given as a string or + :class:`pathlib.PurePath`. Kwargs: @@ -414,6 +420,8 @@ def save(source, target, saver=None, **kwargs): from iris.cube import Cube, CubeList # Determine format from filename + if isinstance(target, pathlib.PurePath): + target = str(target) if isinstance(target, str) and saver is None: saver = find_saver(target) elif hasattr(target, "name") and saver is None: diff --git a/lib/iris/plot.py b/lib/iris/plot.py index 1db60d0fae..0e9645c783 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -858,9 +858,9 @@ def _replace_axes_with_cartopy_axes(cartopy_proj): def _ensure_cartopy_axes_and_determine_kwargs(x_coord, y_coord, kwargs): """ - Replace the current non-cartopy axes with :class:`cartopy.mpl.GeoAxes` - and return the appropriate kwargs dict based on the provided coordinates - and kwargs. + Replace the current non-cartopy axes with + :class:`cartopy.mpl.geoaxes.GeoAxes` and return the appropriate kwargs dict + based on the provided coordinates and kwargs. """ # Determine projection. @@ -874,7 +874,7 @@ def _ensure_cartopy_axes_and_determine_kwargs(x_coord, y_coord, kwargs): else: cartopy_proj = ccrs.PlateCarree() - # Ensure the current axes are a cartopy.mpl.GeoAxes instance. + # Ensure the current axes are a cartopy.mpl.geoaxes.GeoAxes instance. axes = kwargs.get("axes") if axes is None: if ( @@ -1430,7 +1430,8 @@ def barbs(u_cube, v_cube, *args, **kwargs): :func:`iris.analysis.cartography.rotate_grid_vectors`. To transform coordinate grid points, you will need to create 2-dimensional arrays of x and y values. These can be transformed with - :meth:`cartopy.crs.CRS.transform_points`. + the :meth:`~cartopy.crs.CRS.transform_points` method of + :class:`cartopy.crs.CRS`. Kwargs: @@ -1478,7 +1479,8 @@ def quiver(u_cube, v_cube, *args, **kwargs): :func:`iris.analysis.cartography.rotate_grid_vectors`. To transform coordinate grid points, you will need to create 2-dimensional arrays of x and y values. These can be transformed with - :meth:`cartopy.crs.CRS.transform_points`. + the :meth:`~cartopy.crs.CRS.transform_points` method of + :class:`cartopy.crs.CRS`. Kwargs: diff --git a/lib/iris/tests/integration/test_netcdf.py b/lib/iris/tests/integration/test_netcdf.py index f7aaa1d05c..2a45561e17 100644 --- a/lib/iris/tests/integration/test_netcdf.py +++ b/lib/iris/tests/integration/test_netcdf.py @@ -416,7 +416,7 @@ def setUp(self): levels.units = "centimeters" levels.positive = "down" levels.axis = "Z" - levels[:] = np.linspace(0, 10 ** 5, 3) + levels[:] = np.linspace(0, 10**5, 3) volcello.id = "volcello" volcello.out_name = "volcello" @@ -507,9 +507,9 @@ def _get_scale_factor_add_offset(cube, datatype): else: masked = False if masked: - scale_factor = (cmax - cmin) / (2 ** n - 2) + scale_factor = (cmax - cmin) / (2**n - 2) else: - scale_factor = (cmax - cmin) / (2 ** n - 1) + scale_factor = (cmax - cmin) / (2**n - 1) if dt.kind == "u": add_offset = cmin elif dt.kind == "i": diff --git a/lib/iris/tests/integration/test_trajectory.py b/lib/iris/tests/integration/test_trajectory.py index a7d6c89994..a8e3acaa41 100644 --- a/lib/iris/tests/integration/test_trajectory.py +++ b/lib/iris/tests/integration/test_trajectory.py @@ -216,8 +216,10 @@ def setUp(self): cube.coord("depth").bounds = b32 self.cube = cube # define a latitude trajectory (put coords in a different order - # to the cube, just to be awkward) - latitudes = list(range(-90, 90, 2)) + # to the cube, just to be awkward) although avoid south pole + # singularity as a sample point and the issue of snapping to + # multi-equidistant closest points from within orca antarctic hole + latitudes = list(range(-80, 90, 2)) longitudes = [-90] * len(latitudes) self.sample_points = [ ("longitude", longitudes), @@ -226,7 +228,9 @@ def setUp(self): def test_tri_polar(self): # extract - sampled_cube = traj_interpolate(self.cube, self.sample_points) + sampled_cube = traj_interpolate( + self.cube, self.sample_points, method="nearest" + ) self.assertCML( sampled_cube, ("trajectory", "tri_polar_latitude_slice.cml") ) @@ -329,8 +333,12 @@ def test_hybrid_height(self): # Put a lazy array into the cube so we can test deferred loading. cube.data = as_lazy_data(cube.data) + # Use opionated grid-latitudes to avoid the issue of platform + # specific behaviour within SciPy cKDTree choosing a different + # equi-distant nearest neighbour point when there are multiple + # valid candidates. traj = ( - ("grid_latitude", [20.5, 21.5, 22.5, 23.5]), + ("grid_latitude", [20.4, 21.6, 22.6, 23.6]), ("grid_longitude", [31, 32, 33, 34]), ) xsec = traj_interpolate(cube, traj, method="nearest") diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/0d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/0d_str.txt index a6738e654f..a4c1157df2 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/0d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/0d_str.txt @@ -10,4 +10,4 @@ air_potential_temperature / (K) (scalar cube) surface_altitude 413.93686 m time 2009-09-09 17:10:00 Attributes: - source Iris test case \ No newline at end of file + source 'Iris test case' \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/1d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/1d_str.txt index 95f7e7b57e..7d43a997da 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/1d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/1d_str.txt @@ -13,4 +13,4 @@ air_potential_temperature / (K) (grid_longitude: 100) sigma 0.9994238, bound=(1.0, 0.99846387) time 2009-09-09 17:10:00 Attributes: - source Iris test case \ No newline at end of file + source 'Iris test case' \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/2d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/2d_str.txt index c4184d199a..9adeb35c73 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/2d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/2d_str.txt @@ -13,4 +13,4 @@ air_potential_temperature / (K) (grid_latitude: 100; grid_longitude: 100) sigma 0.9994238, bound=(1.0, 0.99846387) time 2009-09-09 17:10:00 Attributes: - source Iris test case \ No newline at end of file + source 'Iris test case' \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/3d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/3d_str.txt index af81d4e991..dc5e71433f 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/3d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/3d_str.txt @@ -13,4 +13,4 @@ air_potential_temperature / (K) (model_level_number: 70; grid_latitude: 100; forecast_period 0.0 hours time 2009-09-09 17:10:00 Attributes: - source Iris test case \ No newline at end of file + source 'Iris test case' \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/4d_str.txt b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/4d_str.txt index afcdedf100..52adc03efb 100644 --- a/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/4d_str.txt +++ b/lib/iris/tests/results/cdm/TestStockCubeStringRepresentations/4d_str.txt @@ -13,4 +13,4 @@ air_potential_temperature / (K) (time: 6; model_level_number: 70; grid_latit Scalar coordinates: forecast_period 0.0 hours Attributes: - source Iris test case \ No newline at end of file + source 'Iris test case' \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/0d_cube.__str__.txt b/lib/iris/tests/results/cdm/str_repr/0d_cube.__str__.txt index 6a3276d861..02e9849d38 100644 --- a/lib/iris/tests/results/cdm/str_repr/0d_cube.__str__.txt +++ b/lib/iris/tests/results/cdm/str_repr/0d_cube.__str__.txt @@ -8,4 +8,4 @@ air_temperature / (K) (scalar cube) time 1998-12-01 00:00:00 Attributes: STASH m01s16i203 - source Data from Met Office Unified Model \ No newline at end of file + source 'Data from Met Office Unified Model' \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/0d_cube.__unicode__.txt b/lib/iris/tests/results/cdm/str_repr/0d_cube.__unicode__.txt index 6a3276d861..02e9849d38 100644 --- a/lib/iris/tests/results/cdm/str_repr/0d_cube.__unicode__.txt +++ b/lib/iris/tests/results/cdm/str_repr/0d_cube.__unicode__.txt @@ -8,4 +8,4 @@ air_temperature / (K) (scalar cube) time 1998-12-01 00:00:00 Attributes: STASH m01s16i203 - source Data from Met Office Unified Model \ No newline at end of file + source 'Data from Met Office Unified Model' \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/cell_methods.__str__.txt b/lib/iris/tests/results/cdm/str_repr/cell_methods.__str__.txt index ba93542e51..ffb6a62daf 100644 --- a/lib/iris/tests/results/cdm/str_repr/cell_methods.__str__.txt +++ b/lib/iris/tests/results/cdm/str_repr/cell_methods.__str__.txt @@ -14,4 +14,4 @@ air_temperature / (K) (latitude: 73; longitude: 96) percentile longitude (6 minutes, This is another test comment) Attributes: STASH m01s16i203 - source Data from Met Office Unified Model \ No newline at end of file + source 'Data from Met Office Unified Model' \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/missing_coords_cube.str.txt b/lib/iris/tests/results/cdm/str_repr/missing_coords_cube.str.txt index 1b86bd6597..0ac5bd3c8a 100644 --- a/lib/iris/tests/results/cdm/str_repr/missing_coords_cube.str.txt +++ b/lib/iris/tests/results/cdm/str_repr/missing_coords_cube.str.txt @@ -11,4 +11,4 @@ air_potential_temperature / (K) (-- : 6; -- : 70; grid_latitude: 100; grid_l Scalar coordinates: forecast_period 0.0 hours Attributes: - source Iris test case \ No newline at end of file + source 'Iris test case' \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/similar.__str__.txt b/lib/iris/tests/results/cdm/str_repr/similar.__str__.txt index fc274ed4c1..394e52e5c9 100644 --- a/lib/iris/tests/results/cdm/str_repr/similar.__str__.txt +++ b/lib/iris/tests/results/cdm/str_repr/similar.__str__.txt @@ -15,4 +15,4 @@ air_temperature / (K) (latitude: 73; longitude: 96) time 1998-12-01 00:00:00 Attributes: STASH m01s16i203 - source Data from Met Office Unified Model \ No newline at end of file + source 'Data from Met Office Unified Model' \ No newline at end of file diff --git a/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__unicode__.txt b/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__unicode__.txt index 29c181345c..594ad11688 100644 --- a/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__unicode__.txt +++ b/lib/iris/tests/results/cdm/str_repr/unicode_attribute.__unicode__.txt @@ -2,4 +2,4 @@ thingness / (1) (foo: 11) Dimension coordinates: foo x Attributes: - source ꀀabcd޴ \ No newline at end of file + source 'ꀀabcd\u07b4' \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/aux_nontime_repr.txt b/lib/iris/tests/results/coord_api/str_repr/aux_nontime_repr.txt index c1d62f28e4..3e7aeda309 100644 --- a/lib/iris/tests/results/coord_api/str_repr/aux_nontime_repr.txt +++ b/lib/iris/tests/results/coord_api/str_repr/aux_nontime_repr.txt @@ -1,11 +1 @@ -DimCoord(array([-0.1278, -0.1269, -0.126 , -0.1251, -0.1242, -0.1233, -0.1224, - -0.1215, -0.1206, -0.1197], dtype=float32), bounds=array([[-0.12825, -0.12735], - [-0.12735, -0.12645], - [-0.12645, -0.12555], - [-0.12555, -0.12465], - [-0.12465, -0.12375], - [-0.12375, -0.12285], - [-0.12285, -0.12195], - [-0.12195, -0.12105], - [-0.12105, -0.12015], - [-0.12015, -0.11925]], dtype=float32), standard_name='grid_latitude', units=Unit('degrees'), coord_system=RotatedGeogCS(37.5, 177.5, ellipsoid=GeogCS(6371229.0))) \ No newline at end of file + \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/aux_nontime_str.txt b/lib/iris/tests/results/coord_api/str_repr/aux_nontime_str.txt index c1d62f28e4..3030ea962a 100644 --- a/lib/iris/tests/results/coord_api/str_repr/aux_nontime_str.txt +++ b/lib/iris/tests/results/coord_api/str_repr/aux_nontime_str.txt @@ -1,11 +1,15 @@ -DimCoord(array([-0.1278, -0.1269, -0.126 , -0.1251, -0.1242, -0.1233, -0.1224, - -0.1215, -0.1206, -0.1197], dtype=float32), bounds=array([[-0.12825, -0.12735], - [-0.12735, -0.12645], - [-0.12645, -0.12555], - [-0.12555, -0.12465], - [-0.12465, -0.12375], - [-0.12375, -0.12285], - [-0.12285, -0.12195], - [-0.12195, -0.12105], - [-0.12105, -0.12015], - [-0.12015, -0.11925]], dtype=float32), standard_name='grid_latitude', units=Unit('degrees'), coord_system=RotatedGeogCS(37.5, 177.5, ellipsoid=GeogCS(6371229.0))) \ No newline at end of file +DimCoord : level_height / (m) + points: [ + 5. , 21.666664, 45. , 75. , 111.66668 , + 155. , 205. , 261.6667 , 325. , 395. ] + bounds: [ + [ 0. , 13.333332], + [ 13.333332, 33.333332], + ..., + [293.3333 , 360. ], + [360. , 433.3332 ]] + shape: (10,) bounds(10, 2) + dtype: float32 + long_name: 'level_height' + attributes: + positive 'up' \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/aux_time_repr.txt b/lib/iris/tests/results/coord_api/str_repr/aux_time_repr.txt index 120546311f..57d5882e88 100644 --- a/lib/iris/tests/results/coord_api/str_repr/aux_time_repr.txt +++ b/lib/iris/tests/results/coord_api/str_repr/aux_time_repr.txt @@ -1,2 +1 @@ -DimCoord(array([347921.16666667, 347921.33333333, 347921.5 , 347921.66666666, - 347921.83333333, 347922. ]), standard_name='time', units=Unit('hours since 1970-01-01 00:00:00', calendar='gregorian')) \ No newline at end of file + \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/aux_time_str.txt b/lib/iris/tests/results/coord_api/str_repr/aux_time_str.txt index 9d209402e6..f9cd09223a 100644 --- a/lib/iris/tests/results/coord_api/str_repr/aux_time_str.txt +++ b/lib/iris/tests/results/coord_api/str_repr/aux_time_str.txt @@ -1,2 +1,5 @@ -DimCoord([2009-09-09 17:10:00, 2009-09-09 17:20:00, 2009-09-09 17:30:00, - 2009-09-09 17:40:00, 2009-09-09 17:50:00, 2009-09-09 18:00:00], standard_name='time', calendar='gregorian') \ No newline at end of file +DimCoord : forecast_period / (hours) + points: [0.] + shape: (1,) + dtype: float64 + standard_name: 'forecast_period' \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/dim_nontime_repr.txt b/lib/iris/tests/results/coord_api/str_repr/dim_nontime_repr.txt index c1d62f28e4..cbf44b1dbb 100644 --- a/lib/iris/tests/results/coord_api/str_repr/dim_nontime_repr.txt +++ b/lib/iris/tests/results/coord_api/str_repr/dim_nontime_repr.txt @@ -1,11 +1 @@ -DimCoord(array([-0.1278, -0.1269, -0.126 , -0.1251, -0.1242, -0.1233, -0.1224, - -0.1215, -0.1206, -0.1197], dtype=float32), bounds=array([[-0.12825, -0.12735], - [-0.12735, -0.12645], - [-0.12645, -0.12555], - [-0.12555, -0.12465], - [-0.12465, -0.12375], - [-0.12375, -0.12285], - [-0.12285, -0.12195], - [-0.12195, -0.12105], - [-0.12105, -0.12015], - [-0.12015, -0.11925]], dtype=float32), standard_name='grid_latitude', units=Unit('degrees'), coord_system=RotatedGeogCS(37.5, 177.5, ellipsoid=GeogCS(6371229.0))) \ No newline at end of file + \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/dim_nontime_str.txt b/lib/iris/tests/results/coord_api/str_repr/dim_nontime_str.txt index c1d62f28e4..95c0a60159 100644 --- a/lib/iris/tests/results/coord_api/str_repr/dim_nontime_str.txt +++ b/lib/iris/tests/results/coord_api/str_repr/dim_nontime_str.txt @@ -1,11 +1,14 @@ -DimCoord(array([-0.1278, -0.1269, -0.126 , -0.1251, -0.1242, -0.1233, -0.1224, - -0.1215, -0.1206, -0.1197], dtype=float32), bounds=array([[-0.12825, -0.12735], - [-0.12735, -0.12645], - [-0.12645, -0.12555], - [-0.12555, -0.12465], - [-0.12465, -0.12375], - [-0.12375, -0.12285], - [-0.12285, -0.12195], - [-0.12195, -0.12105], - [-0.12105, -0.12015], - [-0.12015, -0.11925]], dtype=float32), standard_name='grid_latitude', units=Unit('degrees'), coord_system=RotatedGeogCS(37.5, 177.5, ellipsoid=GeogCS(6371229.0))) \ No newline at end of file +DimCoord : grid_latitude / (degrees) + points: [ + -0.1278, -0.1269, -0.126 , -0.1251, -0.1242, -0.1233, -0.1224, + -0.1215, -0.1206, -0.1197] + bounds: [ + [-0.12825, -0.12735], + [-0.12735, -0.12645], + ..., + [-0.12105, -0.12015], + [-0.12015, -0.11925]] + shape: (10,) bounds(10, 2) + dtype: float32 + standard_name: 'grid_latitude' + coord_system: RotatedGeogCS(37.5, 177.5, ellipsoid=GeogCS(6371229.0)) \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/dim_time_repr.txt b/lib/iris/tests/results/coord_api/str_repr/dim_time_repr.txt index 120546311f..ae1b86ae02 100644 --- a/lib/iris/tests/results/coord_api/str_repr/dim_time_repr.txt +++ b/lib/iris/tests/results/coord_api/str_repr/dim_time_repr.txt @@ -1,2 +1 @@ -DimCoord(array([347921.16666667, 347921.33333333, 347921.5 , 347921.66666666, - 347921.83333333, 347922. ]), standard_name='time', units=Unit('hours since 1970-01-01 00:00:00', calendar='gregorian')) \ No newline at end of file + \ No newline at end of file diff --git a/lib/iris/tests/results/coord_api/str_repr/dim_time_str.txt b/lib/iris/tests/results/coord_api/str_repr/dim_time_str.txt index 9d209402e6..6b95b57215 100644 --- a/lib/iris/tests/results/coord_api/str_repr/dim_time_str.txt +++ b/lib/iris/tests/results/coord_api/str_repr/dim_time_str.txt @@ -1,2 +1,7 @@ -DimCoord([2009-09-09 17:10:00, 2009-09-09 17:20:00, 2009-09-09 17:30:00, - 2009-09-09 17:40:00, 2009-09-09 17:50:00, 2009-09-09 18:00:00], standard_name='time', calendar='gregorian') \ No newline at end of file +DimCoord : time / (hours since 1970-01-01 00:00:00, gregorian calendar) + points: [ + 2009-09-09 17:10:00, 2009-09-09 17:20:00, 2009-09-09 17:30:00, + 2009-09-09 17:40:00, 2009-09-09 17:50:00, 2009-09-09 18:00:00] + shape: (6,) + dtype: float64 + standard_name: 'time' \ No newline at end of file diff --git a/lib/iris/tests/results/derived/no_orog.__str__.txt b/lib/iris/tests/results/derived/no_orog.__str__.txt index e277b5d276..de139592a6 100644 --- a/lib/iris/tests/results/derived/no_orog.__str__.txt +++ b/lib/iris/tests/results/derived/no_orog.__str__.txt @@ -13,4 +13,4 @@ air_potential_temperature / (K) (time: 6; model_level_number: 70; grid_latit Scalar coordinates: forecast_period 0.0 hours Attributes: - source Iris test case \ No newline at end of file + source 'Iris test case' \ No newline at end of file diff --git a/lib/iris/tests/results/derived/removed_orog.__str__.txt b/lib/iris/tests/results/derived/removed_orog.__str__.txt index 0c24cded80..a9ef3bd017 100644 --- a/lib/iris/tests/results/derived/removed_orog.__str__.txt +++ b/lib/iris/tests/results/derived/removed_orog.__str__.txt @@ -12,4 +12,4 @@ air_potential_temperature / (K) (time: 6; model_level_number: 70; grid_latit Scalar coordinates: forecast_period 0.0 hours Attributes: - source Iris test case \ No newline at end of file + source 'Iris test case' \ No newline at end of file diff --git a/lib/iris/tests/results/derived/removed_sigma.__str__.txt b/lib/iris/tests/results/derived/removed_sigma.__str__.txt index 94e850ec62..c3eaf48483 100644 --- a/lib/iris/tests/results/derived/removed_sigma.__str__.txt +++ b/lib/iris/tests/results/derived/removed_sigma.__str__.txt @@ -12,4 +12,4 @@ air_potential_temperature / (K) (time: 6; model_level_number: 70; grid_latit Scalar coordinates: forecast_period 0.0 hours Attributes: - source Iris test case \ No newline at end of file + source 'Iris test case' \ No newline at end of file diff --git a/lib/iris/tests/results/trajectory/tri_polar_latitude_slice.cml b/lib/iris/tests/results/trajectory/tri_polar_latitude_slice.cml index 701ca7beb7..750d597493 100644 --- a/lib/iris/tests/results/trajectory/tri_polar_latitude_slice.cml +++ b/lib/iris/tests/results/trajectory/tri_polar_latitude_slice.cml @@ -76,8 +76,25 @@ - + @@ -86,8 +103,26 @@ - + @@ -109,6 +144,6 @@ - + diff --git a/lib/iris/tests/results/util/as_compatible_shape_collapsed.cml b/lib/iris/tests/results/util/as_compatible_shape_collapsed.cml deleted file mode 100644 index 07eeb53157..0000000000 --- a/lib/iris/tests/results/util/as_compatible_shape_collapsed.cml +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/iris/tests/test_basic_maths.py b/lib/iris/tests/test_basic_maths.py index e753adbae8..24f2b89442 100644 --- a/lib/iris/tests/test_basic_maths.py +++ b/lib/iris/tests/test_basic_maths.py @@ -249,7 +249,7 @@ def test_apply_ufunc(self): np.square, a, new_name="squared temperature", - new_unit=a.units ** 2, + new_unit=a.units**2, in_place=False, ) self.assertCMLApproxData(a, ("analysis", "apply_ufunc_original.cml")) @@ -259,14 +259,14 @@ def test_apply_ufunc(self): np.square, a, new_name="squared temperature", - new_unit=a.units ** 2, + new_unit=a.units**2, in_place=True, ) self.assertCMLApproxData(b, ("analysis", "apply_ufunc.cml")) self.assertCMLApproxData(a, ("analysis", "apply_ufunc.cml")) def vec_mag(u, v): - return math.sqrt(u ** 2 + v ** 2) + return math.sqrt(u**2 + v**2) c = a.copy() + 2 @@ -295,7 +295,7 @@ def test_apply_ufunc_fail(self): def test_ifunc(self): a = self.cube - my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units ** 2) + my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units**2) b = my_ifunc(a, new_name="squared temperature", in_place=False) self.assertCMLApproxData(a, ("analysis", "apply_ifunc_original.cml")) @@ -307,7 +307,7 @@ def test_ifunc(self): self.assertCMLApproxData(a, ("analysis", "apply_ifunc.cml")) def vec_mag(u, v): - return math.sqrt(u ** 2 + v ** 2) + return math.sqrt(u**2 + v**2) c = a.copy() + 2 @@ -347,7 +347,7 @@ def test_ifunc_init_fail(self): def test_ifunc_call_fail(self): a = self.cube - my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units ** 2) + my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units**2) # should now NOT fail because giving 2 arguments to an ifunc that # expects only one will now ignore the surplus argument and raise @@ -367,7 +367,7 @@ def test_ifunc_call_fail(self): my_ifunc(a) my_ifunc = iris.analysis.maths.IFunc( - lambda a: (a, a ** 2.0), lambda cube: cf_units.Unit("1") + lambda a: (a, a**2.0), lambda cube: cf_units.Unit("1") ) # should fail because data function returns a tuple @@ -553,9 +553,9 @@ def test_square_root(self): a.data = abs(a.data) a.units **= 2 - e = a ** 0.5 + e = a**0.5 - self.assertArrayAllClose(e.data, a.data ** 0.5) + self.assertArrayAllClose(e.data, a.data**0.5) self.assertCML(e, ("analysis", "sqrt.cml"), checksum=False) self.assertRaises(ValueError, iris.analysis.maths.exponentiate, a, 0.3) @@ -585,26 +585,26 @@ def test_apply_ufunc(self): np.square, a, new_name="more_thingness", - new_unit=a.units ** 2, + new_unit=a.units**2, in_place=False, ) - ans = a.data ** 2 + ans = a.data**2 self.assertArrayEqual(b.data, ans) self.assertEqual(b.name(), "more_thingness") self.assertEqual(b.units, cf_units.Unit("m^2")) def vec_mag(u, v): - return math.sqrt(u ** 2 + v ** 2) + return math.sqrt(u**2 + v**2) c = a.copy() + 2 vec_mag_ufunc = np.frompyfunc(vec_mag, 2, 1) b = iris.analysis.maths.apply_ufunc(vec_mag_ufunc, a, c) - ans = a.data ** 2 + c.data ** 2 - b2 = b ** 2 + ans = a.data**2 + c.data**2 + b2 = b**2 self.assertArrayAlmostEqual(b2.data, ans) @@ -617,17 +617,17 @@ def test_ifunc(self): a = self.cube a.units = cf_units.Unit("meters") - my_ifunc = iris.analysis.maths.IFunc(np.square, lambda x: x.units ** 2) + my_ifunc = iris.analysis.maths.IFunc(np.square, lambda x: x.units**2) b = my_ifunc(a, new_name="more_thingness", in_place=False) - ans = a.data ** 2 + ans = a.data**2 self.assertArrayEqual(b.data, ans) self.assertEqual(b.name(), "more_thingness") self.assertEqual(b.units, cf_units.Unit("m^2")) def vec_mag(u, v): - return math.sqrt(u ** 2 + v ** 2) + return math.sqrt(u**2 + v**2) c = a.copy() + 2 @@ -637,12 +637,12 @@ def vec_mag(u, v): ) b = my_ifunc(a, c) - ans = (a.data ** 2 + c.data ** 2) ** 0.5 + ans = (a.data**2 + c.data**2) ** 0.5 self.assertArrayAlmostEqual(b.data, ans) def vec_mag_data_func(u_data, v_data): - return np.sqrt(u_data ** 2 + v_data ** 2) + return np.sqrt(u_data**2 + v_data**2) vec_mag_ifunc = iris.analysis.maths.IFunc( vec_mag_data_func, lambda a, b: (a + b).units diff --git a/lib/iris/tests/test_coord_api.py b/lib/iris/tests/test_coord_api.py index 1b2ba56300..87270b524c 100644 --- a/lib/iris/tests/test_coord_api.py +++ b/lib/iris/tests/test_coord_api.py @@ -178,7 +178,9 @@ def test_complex(self): @tests.skip_data class TestCoord_ReprStr_nontime(tests.IrisTest): def setUp(self): - self.lat = iris.tests.stock.realistic_4d().coord("grid_latitude")[:10] + cube = iris.tests.stock.realistic_4d() + self.lat = cube.coord("grid_latitude")[:10] + self.height = cube.coord("level_height")[:10] def test_DimCoord_repr(self): self.assertRepr( @@ -187,7 +189,7 @@ def test_DimCoord_repr(self): def test_AuxCoord_repr(self): self.assertRepr( - self.lat, ("coord_api", "str_repr", "aux_nontime_repr.txt") + self.height, ("coord_api", "str_repr", "aux_nontime_repr.txt") ) def test_DimCoord_str(self): @@ -197,14 +199,16 @@ def test_DimCoord_str(self): def test_AuxCoord_str(self): self.assertString( - str(self.lat), ("coord_api", "str_repr", "aux_nontime_str.txt") + str(self.height), ("coord_api", "str_repr", "aux_nontime_str.txt") ) @tests.skip_data class TestCoord_ReprStr_time(tests.IrisTest): def setUp(self): - self.time = iris.tests.stock.realistic_4d().coord("time") + cube = iris.tests.stock.realistic_4d() + self.time = cube.coord("time") + self.fp = cube.coord("forecast_period") def test_DimCoord_repr(self): self.assertRepr( @@ -213,7 +217,7 @@ def test_DimCoord_repr(self): def test_AuxCoord_repr(self): self.assertRepr( - self.time, ("coord_api", "str_repr", "aux_time_repr.txt") + self.fp, ("coord_api", "str_repr", "aux_time_repr.txt") ) def test_DimCoord_str(self): @@ -223,7 +227,7 @@ def test_DimCoord_str(self): def test_AuxCoord_str(self): self.assertString( - str(self.time), ("coord_api", "str_repr", "aux_time_str.txt") + str(self.fp), ("coord_api", "str_repr", "aux_time_str.txt") ) @@ -232,23 +236,29 @@ def test_basic(self): a = iris.coords.AuxCoord( np.arange(10), "air_temperature", units="kelvin" ) - result = ( - "AuxCoord(" - "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," - " standard_name='air_temperature'," - " units=Unit('kelvin'))" + result = "\n".join( + [ + "AuxCoord : air_temperature / (kelvin)", + " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", + " shape: (10,)", + " dtype: int64", + " standard_name: 'air_temperature'", + ] ) self.assertEqual(result, str(a)) b = iris.coords.AuxCoord( list(range(10)), attributes={"monty": "python"} ) - result = ( - "AuxCoord(" - "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," - " standard_name=None," - " units=Unit('unknown')," - " attributes={'monty': 'python'})" + result = "\n".join( + [ + "AuxCoord : unknown / (unknown)", + " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", + " shape: (10,)", + " dtype: int64", + " attributes:", + " monty 'python'", + ] ) self.assertEqual(result, str(b)) @@ -276,12 +286,15 @@ def test_coord_system(self): units="kelvin", coord_system=iris.coord_systems.GeogCS(6000), ) - result = ( - "AuxCoord(" - "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," - " standard_name='air_temperature'," - " units=Unit('kelvin')," - " coord_system=GeogCS(6000.0))" + result = "\n".join( + [ + "AuxCoord : air_temperature / (kelvin)", + " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", + " shape: (10,)", + " dtype: int64", + " standard_name: 'air_temperature'", + " coord_system: GeogCS(6000.0)", + ] ) self.assertEqual(result, str(a)) @@ -292,14 +305,20 @@ def test_bounded(self): units="kelvin", bounds=np.arange(0, 20).reshape(10, 2), ) - result = ( - "AuxCoord(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])" - ", bounds=array([" - "[ 0, 1],\n [ 2, 3],\n [ 4, 5],\n " - "[ 6, 7],\n [ 8, 9],\n [10, 11],\n " - "[12, 13],\n [14, 15],\n [16, 17],\n " - "[18, 19]])" - ", standard_name='air_temperature', units=Unit('kelvin'))" + result = "\n".join( + [ + "AuxCoord : air_temperature / (kelvin)", + " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", + " bounds: [", + " [ 0, 1],", + " [ 2, 3],", + " ...,", + " [16, 17],", + " [18, 19]]", + " shape: (10,) bounds(10, 2)", + " dtype: int64", + " standard_name: 'air_temperature'", + ] ) self.assertEqual(result, str(a)) @@ -322,23 +341,29 @@ def test_basic(self): a = iris.coords.DimCoord( np.arange(10), "air_temperature", units="kelvin" ) - result = ( - "DimCoord(" - "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," - " standard_name='air_temperature'," - " units=Unit('kelvin'))" + result = "\n".join( + [ + "DimCoord : air_temperature / (kelvin)", + " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", + " shape: (10,)", + " dtype: int64", + " standard_name: 'air_temperature'", + ] ) self.assertEqual(result, str(a)) b = iris.coords.DimCoord( list(range(10)), attributes={"monty": "python"} ) - result = ( - "DimCoord(" - "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," - " standard_name=None," - " units=Unit('unknown')," - " attributes={'monty': 'python'})" + result = "\n".join( + [ + "DimCoord : unknown / (unknown)", + " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", + " shape: (10,)", + " dtype: int64", + " attributes:", + " monty 'python'", + ] ) self.assertEqual(result, str(b)) @@ -366,12 +391,15 @@ def test_coord_system(self): units="kelvin", coord_system=iris.coord_systems.GeogCS(6000), ) - result = ( - "DimCoord(" - "array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])," - " standard_name='air_temperature'," - " units=Unit('kelvin')," - " coord_system=GeogCS(6000.0))" + result = "\n".join( + [ + "DimCoord : air_temperature / (kelvin)", + " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", + " shape: (10,)", + " dtype: int64", + " standard_name: 'air_temperature'", + " coord_system: GeogCS(6000.0)", + ] ) self.assertEqual(result, str(a)) @@ -382,14 +410,20 @@ def test_bounded(self): units="kelvin", bounds=np.arange(0, 20).reshape(10, 2), ) - result = ( - "DimCoord(array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])" - ", bounds=array([" - "[ 0, 1],\n [ 2, 3],\n [ 4, 5],\n " - "[ 6, 7],\n [ 8, 9],\n [10, 11],\n " - "[12, 13],\n [14, 15],\n [16, 17],\n " - "[18, 19]])" - ", standard_name='air_temperature', units=Unit('kelvin'))" + result = "\n".join( + [ + "DimCoord : air_temperature / (kelvin)", + " points: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]", + " bounds: [", + " [ 0, 1],", + " [ 2, 3],", + " ...,", + " [16, 17],", + " [18, 19]]", + " shape: (10,) bounds(10, 2)", + " dtype: int64", + " standard_name: 'air_temperature'", + ] ) self.assertEqual(result, str(a)) diff --git a/lib/iris/tests/test_coordsystem.py b/lib/iris/tests/test_coordsystem.py index 2c0569f0d6..4229125969 100644 --- a/lib/iris/tests/test_coordsystem.py +++ b/lib/iris/tests/test_coordsystem.py @@ -188,6 +188,29 @@ def test_as_cartopy_globe(self): self.assertEqual(res, expected) +class Test_GeogCS_as_cartopy_projection(tests.IrisTest): + def test_as_cartopy_projection(self): + geogcs_args = { + "semi_major_axis": 6543210, + "semi_minor_axis": 6500000, + "longitude_of_prime_meridian": 30, + } + cs = GeogCS(**geogcs_args) + res = cs.as_cartopy_projection() + + globe = ccrs.Globe( + semimajor_axis=geogcs_args["semi_major_axis"], + semiminor_axis=geogcs_args["semi_minor_axis"], + ellipse=None, + ) + expected = ccrs.PlateCarree( + globe=globe, + central_longitude=geogcs_args["longitude_of_prime_meridian"], + ) + + self.assertEqual(res, expected) + + class Test_GeogCS_as_cartopy_crs(tests.IrisTest): def test_as_cartopy_crs(self): cs = GeogCS(6543210, 6500000) diff --git a/lib/iris/tests/test_file_save.py b/lib/iris/tests/test_file_save.py index 2b5619c656..3b751cfcbe 100644 --- a/lib/iris/tests/test_file_save.py +++ b/lib/iris/tests/test_file_save.py @@ -13,6 +13,7 @@ from io import StringIO import os +import pathlib import unittest import iris @@ -97,6 +98,23 @@ def test_filename(self): CHKSUM_ERR.format(self.ext), ) + def test_filename_path_object(self): + # Save using iris.save and pp.save, passing filename for + # iris.save as pathlib.Path + save_by_filename( + self.temp_filename1, + pathlib.Path(self.temp_filename2), + self.cube1, + pp.save, + ) + + # Compare files + self.assertEqual( + self.file_checksum(self.temp_filename2), + self.file_checksum(self.temp_filename1), + CHKSUM_ERR.format(self.ext), + ) + def test_filehandle(self): # Save using iris.save and pp.save save_by_filehandle( diff --git a/lib/iris/tests/test_io_init.py b/lib/iris/tests/test_io_init.py index e88eaabaed..d33b76ddeb 100644 --- a/lib/iris/tests/test_io_init.py +++ b/lib/iris/tests/test_io_init.py @@ -12,37 +12,58 @@ import iris.tests as tests # isort:skip from io import BytesIO +from pathlib import Path import iris.fileformats as iff import iris.io class TestDecodeUri(tests.IrisTest): - def test_decode_uri(self): + def test_decode_uri__str(self): tests = { - "/data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp": ( + (uri := "/data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp"): ( "file", - "/data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp", + uri, ), - r"C:\data\local\someDir\PP\COLPEX\COLPEX_16a_pj001.pp": ( + (uri := r"C:\data\local\someDir\PP\COLPEX\COLPEX_16a_pj001.pp"): ( "file", - r"C:\data\local\someDir\PP\COLPEX\COLPEX_16a_pj001.pp", + uri, ), - "file:///data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp": ( + ( + uri := "file:///data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp" + ): ( + uri[:4], + uri[5:], + ), + (uri := "http://www.somehost.com:8080/resource/thing.grib"): ( + uri[:4], + uri[5:], + ), + (uri := "/data/local/someDir/2013-11-25T13:49:17.632797"): ( "file", - "///data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp", + uri, ), - "http://www.somehost.com:8080/resource/thing.grib": ( - "http", - "//www.somehost.com:8080/resource/thing.grib", + } + for uri, expected in tests.items(): + self.assertEqual(expected, iris.io.decode_uri(uri)) + + def test_decode_uri__path(self): + tests = { + (uri := "/data/local/someDir/PP/COLPEX/COLPEX_16a_pj001.pp"): ( + "file", + uri, + ), + (uri := r"C:\data\local\someDir\PP\COLPEX\COLPEX_16a_pj001.pp"): ( + "file", + uri, ), - "/data/local/someDir/2013-11-25T13:49:17.632797": ( + (uri := "/data/local/someDir/2013-11-25T13:49:17.632797"): ( "file", - "/data/local/someDir/2013-11-25T13:49:17.632797", + uri, ), } - for uri, pair in tests.items(): - self.assertEqual(pair, iris.io.decode_uri(uri)) + for uri, expected in tests.items(): + self.assertEqual(expected, iris.io.decode_uri(Path(uri))) class TestFileFormatPicker(tests.IrisTest): diff --git a/lib/iris/tests/test_load.py b/lib/iris/tests/test_load.py index 0674768a54..86ff2f1ece 100644 --- a/lib/iris/tests/test_load.py +++ b/lib/iris/tests/test_load.py @@ -11,6 +11,8 @@ # import iris tests first so that some things can be initialised before importing anything else import iris.tests as tests # isort:skip +import pathlib + import iris import iris.io @@ -22,6 +24,13 @@ def test_normal(self): cubes = iris.load(paths) self.assertEqual(len(cubes), 1) + def test_path_object(self): + paths = ( + pathlib.Path(tests.get_data_path(["PP", "aPPglob1", "global.pp"])), + ) + cubes = iris.load(paths) + self.assertEqual(len(cubes), 1) + def test_nonexist(self): paths = ( tests.get_data_path(["PP", "aPPglob1", "global.pp"]), @@ -71,6 +80,12 @@ def test_normal(self): paths = (tests.get_data_path(["PP", "aPPglob1", "global.pp"]),) _ = iris.load_cube(paths) + def test_path_object(self): + paths = ( + pathlib.Path(tests.get_data_path(["PP", "aPPglob1", "global.pp"])), + ) + _ = iris.load_cube(paths) + def test_not_enough(self): paths = (tests.get_data_path(["PP", "aPPglob1", "global.pp"]),) with self.assertRaises(iris.exceptions.ConstraintMismatchError): @@ -92,6 +107,13 @@ def test_normal(self): cubes = iris.load_cubes(paths) self.assertEqual(len(cubes), 1) + def test_path_object(self): + paths = ( + pathlib.Path(tests.get_data_path(["PP", "aPPglob1", "global.pp"])), + ) + cubes = iris.load_cubes(paths) + self.assertEqual(len(cubes), 1) + def test_not_enough(self): paths = (tests.get_data_path(["PP", "aPPglob1", "global.pp"]),) with self.assertRaises(iris.exceptions.ConstraintMismatchError): @@ -111,6 +133,21 @@ def test_too_many(self): iris.load_cube(paths) +@tests.skip_data +class TestLoadRaw(tests.IrisTest): + def test_normal(self): + paths = (tests.get_data_path(["PP", "aPPglob1", "global.pp"]),) + cubes = iris.load_raw(paths) + self.assertEqual(len(cubes), 1) + + def test_path_object(self): + paths = ( + pathlib.Path(tests.get_data_path(["PP", "aPPglob1", "global.pp"])), + ) + cubes = iris.load_raw(paths) + self.assertEqual(len(cubes), 1) + + class TestOpenDAP(tests.IrisTest): def test_load(self): # Check that calling iris.load_* with a http URI triggers a call to diff --git a/lib/iris/tests/test_mapping.py b/lib/iris/tests/test_mapping.py index 06bedd497b..a71385b5bc 100644 --- a/lib/iris/tests/test_mapping.py +++ b/lib/iris/tests/test_mapping.py @@ -211,7 +211,10 @@ def test_grid(self): def test_default_projection_and_extent(self): self.assertEqual( - iplt.default_projection(self.cube), ccrs.PlateCarree() + iplt.default_projection(self.cube), + ccrs.PlateCarree( + globe=self.cube.coord_system("CoordSystem").as_cartopy_globe() + ), ) np_testing.assert_array_almost_equal( iplt.default_projection_extent(self.cube), diff --git a/lib/iris/tests/test_pickling.py b/lib/iris/tests/test_pickling.py index e01f791d03..26247e795b 100644 --- a/lib/iris/tests/test_pickling.py +++ b/lib/iris/tests/test_pickling.py @@ -16,9 +16,10 @@ import pickle import cf_units +import numpy as np import iris -from iris._lazy_data import as_concrete_data +from iris._lazy_data import as_concrete_data, as_lazy_data class TestPickle(tests.IrisTest): @@ -76,6 +77,14 @@ def test_cube_with_coord_points(self): _, recon_cube = next(self.pickle_then_unpickle(cube)) self.assertEqual(recon_cube, cube) + def test_cube_with_deferred_unit_conversion(self): + real_data = np.arange(12.0).reshape((3, 4)) + lazy_data = as_lazy_data(real_data) + cube = iris.cube.Cube(lazy_data, units="m") + cube.convert_units("ft") + _, recon_cube = next(self.pickle_then_unpickle(cube)) + self.assertEqual(recon_cube, cube) + @tests.skip_data def test_cubelist_pickle(self): cubelist = iris.load( diff --git a/lib/iris/tests/test_util.py b/lib/iris/tests/test_util.py index cf921ae210..ec7f8d1023 100644 --- a/lib/iris/tests/test_util.py +++ b/lib/iris/tests/test_util.py @@ -276,103 +276,5 @@ def test_output_file(self): self.assertFilesEqual(filename, "incompatible_cubes.str.txt") -@tests.skip_data -class TestAsCompatibleShape(tests.IrisTest): - def test_slice(self): - cube = tests.stock.realistic_4d() - sliced = cube[1, :, 2, :-2] - expected = cube[1:2, :, 2:3, :-2] - res = iris.util.as_compatible_shape(sliced, cube) - self.assertEqual(res, expected) - - def test_transpose(self): - cube = tests.stock.realistic_4d() - transposed = cube.copy() - transposed.transpose() - expected = cube - res = iris.util.as_compatible_shape(transposed, cube) - self.assertEqual(res, expected) - - def test_slice_and_transpose(self): - cube = tests.stock.realistic_4d() - sliced_and_transposed = cube[1, :, 2, :-2] - sliced_and_transposed.transpose() - expected = cube[1:2, :, 2:3, :-2] - res = iris.util.as_compatible_shape(sliced_and_transposed, cube) - self.assertEqual(res, expected) - - def test_collapsed(self): - cube = tests.stock.realistic_4d() - collapsed = cube.collapsed("model_level_number", iris.analysis.MEAN) - expected_shape = list(cube.shape) - expected_shape[1] = 1 - expected_data = collapsed.data.reshape(expected_shape) - res = iris.util.as_compatible_shape(collapsed, cube) - self.assertCML( - res, ("util", "as_compatible_shape_collapsed.cml"), checksum=False - ) - self.assertMaskedArrayEqual(expected_data, res.data) - - def test_reduce_dimensionality(self): - # Test that as_compatible_shape() can demote - # length one dimensions to scalars. - cube = tests.stock.realistic_4d() - src = cube[:, 2:3] - expected = reduced = cube[:, 2] - res = iris.util.as_compatible_shape(src, reduced) - self.assertEqual(res, expected) - - def test_anonymous_dims(self): - cube = tests.stock.realistic_4d() - # Move all coords from dim_coords to aux_coords. - for coord in cube.dim_coords: - dim = cube.coord_dims(coord) - cube.remove_coord(coord) - cube.add_aux_coord(coord, dim) - - sliced = cube[1, :, 2, :-2] - expected = cube[1:2, :, 2:3, :-2] - res = iris.util.as_compatible_shape(sliced, cube) - self.assertEqual(res, expected) - - def test_scalar_auxcoord(self): - def dim_to_aux(cube, coord_name): - """Convert coordinate on cube from DimCoord to AuxCoord.""" - coord = cube.coord(coord_name) - coord = iris.coords.AuxCoord.from_coord(coord) - cube.replace_coord(coord) - - cube = tests.stock.realistic_4d() - src = cube[:, :, 3] - dim_to_aux(src, "grid_latitude") - expected = cube[:, :, 3:4] - dim_to_aux(expected, "grid_latitude") - res = iris.util.as_compatible_shape(src, cube) - self.assertEqual(res, expected) - - def test_2d_auxcoord_transpose(self): - dim_coord1 = iris.coords.DimCoord(range(3), long_name="first_dim") - dim_coord2 = iris.coords.DimCoord(range(4), long_name="second_dim") - aux_coord_2d = iris.coords.AuxCoord( - np.arange(12).reshape(3, 4), long_name="spanning" - ) - aux_coord_2d_T = iris.coords.AuxCoord( - np.arange(12).reshape(3, 4).T, long_name="spanning" - ) - src = iris.cube.Cube( - np.ones((3, 4)), - dim_coords_and_dims=[(dim_coord1, 0), (dim_coord2, 1)], - aux_coords_and_dims=[(aux_coord_2d, (0, 1))], - ) - target = iris.cube.Cube( - np.ones((4, 3)), - dim_coords_and_dims=[(dim_coord1, 1), (dim_coord2, 0)], - aux_coords_and_dims=[(aux_coord_2d_T, (0, 1))], - ) - - res = iris.util.as_compatible_shape(src, target) - self.assertEqual(res[0], target[0]) - - if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py b/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py index a44ccb32bd..ecaa028ab3 100644 --- a/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py +++ b/lib/iris/tests/unit/analysis/area_weighted/test_AreaWeightedRegridder.py @@ -16,11 +16,13 @@ import numpy as np from iris import load_cube -from iris.analysis._area_weighted import AreaWeightedRegridder +from iris.analysis._area_weighted import ( + AreaWeightedRegridder, + _regrid_area_weighted_rectilinear_src_and_grid__prepare, +) from iris.coord_systems import GeogCS from iris.coords import DimCoord from iris.cube import Cube -import iris.experimental.regrid as eregrid class Test(tests.IrisTest): @@ -46,19 +48,17 @@ def extract_grid(self, cube): def check_mdtol(self, mdtol=None): src_grid, target_grid = self.grids() # Get _regrid_info result - _regrid_info = ( - eregrid._regrid_area_weighted_rectilinear_src_and_grid__prepare( - src_grid, target_grid - ) + _regrid_info = _regrid_area_weighted_rectilinear_src_and_grid__prepare( + src_grid, target_grid ) self.assertEqual(len(_regrid_info), 10) with mock.patch( - "iris.experimental.regrid." + "iris.analysis._area_weighted." "_regrid_area_weighted_rectilinear_src_and_grid__prepare", return_value=_regrid_info, ) as prepare: with mock.patch( - "iris.experimental.regrid." + "iris.analysis._area_weighted." "_regrid_area_weighted_rectilinear_src_and_grid__perform", return_value=mock.sentinel.result, ) as perform: @@ -253,7 +253,6 @@ class TestLazy(tests.IrisTest): # Setup def setUp(self) -> None: # Prepare a cube and a template - cube_file_path = tests.get_data_path( ["NetCDF", "regrid", "regrid_xyt.nc"] ) diff --git a/lib/iris/tests/unit/analysis/cartography/test_project.py b/lib/iris/tests/unit/analysis/cartography/test_project.py index 4dfa1a4a2e..8649cc55ea 100644 --- a/lib/iris/tests/unit/analysis/cartography/test_project.py +++ b/lib/iris/tests/unit/analysis/cartography/test_project.py @@ -52,7 +52,7 @@ def setUp(self): 1, ) - self.tcs = iris.coord_systems.GeogCS(6000000) + self.tcs = iris.coord_systems.GeogCS(6371229) def test_is_iris_coord_system(self): res, _ = project(self.cube, self.tcs) diff --git a/lib/iris/tests/unit/analysis/cartography/test_rotate_grid_vectors.py b/lib/iris/tests/unit/analysis/cartography/test_rotate_grid_vectors.py index e9294f27dc..f5c882a983 100644 --- a/lib/iris/tests/unit/analysis/cartography/test_rotate_grid_vectors.py +++ b/lib/iris/tests/unit/analysis/cartography/test_rotate_grid_vectors.py @@ -33,7 +33,7 @@ def _check_angles_calculation( u_cube.rename("dx") u_cube.data[...] = 0 v_cube = u_cube.copy() - v_cube.name("dy") + v_cube.rename("dy") # Define 6 different vectors, repeated in each data row. in_vu = np.array([(0, 1), (2, -1), (-1, -1), (-3, 1), (2, 0), (0, 0)]) @@ -71,7 +71,7 @@ def _check_angles_calculation( ang_diffs = out_angs - expect_angs # Fix for null vectors, and +/-360 differences. ang_diffs[np.abs(out_mags) < 0.001] = 0.0 - ang_diffs = ang_diffs % 360.0 + ang_diffs[np.isclose(np.abs(ang_diffs), 360.0)] = 0.0 # Check that any differences are very small. self.assertArrayAllClose(ang_diffs, 0.0) @@ -97,7 +97,7 @@ def test_angles_from_grid(self): u_cube.rename("dx") u_cube.data[...] = 1.0 v_cube = u_cube.copy() - v_cube.name("dy") + v_cube.rename("dy") v_cube.data[...] = 0.0 # Setup a fake angles result from the inner call to 'gridcell_angles'. diff --git a/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py b/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py index 9e3af90603..eafaa20ec8 100644 --- a/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py +++ b/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py @@ -343,8 +343,8 @@ def test_orig_coords(self): def test_magnitude_preservation(self): u, v = self._uv_cubes_limited_extent() ut, vt = rotate_winds(u, v, iris.coord_systems.OSGB()) - orig_sq_mag = u.data ** 2 + v.data ** 2 - res_sq_mag = ut.data ** 2 + vt.data ** 2 + orig_sq_mag = u.data**2 + v.data**2 + res_sq_mag = ut.data**2 + vt.data**2 self.assertArrayAllClose(orig_sq_mag, res_sq_mag, rtol=5e-4) def test_data_values(self): @@ -437,9 +437,9 @@ def test_rotated_to_osgb(self): self.assertArrayEqual(expected_mask, vt.data.mask) # Check unmasked values have sufficiently small error in mag. - expected_mag = np.sqrt(u.data ** 2 + v.data ** 2) + expected_mag = np.sqrt(u.data**2 + v.data**2) # Use underlying data to ignore mask in calculation. - res_mag = np.sqrt(ut.data.data ** 2 + vt.data.data ** 2) + res_mag = np.sqrt(ut.data.data**2 + vt.data.data**2) # Calculate percentage error (note there are no zero magnitudes # so we can divide safely). anom = 100.0 * np.abs(res_mag - expected_mag) / expected_mag diff --git a/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py b/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py index f0dba83748..a018507fb3 100644 --- a/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py +++ b/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py @@ -33,7 +33,7 @@ def setUp(self): self.xs, self.ys = np.meshgrid(self.x.points, self.y.points) def transformation(x, y): - return x + y ** 2 + return x + y**2 # Construct a function which adds dimensions to the 2D data array # so that we can test higher dimensional functionality. diff --git a/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py b/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py index 1fa579ef94..88a88be567 100644 --- a/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py +++ b/lib/iris/tests/unit/common/mixin/test_CFVariableMixin.py @@ -286,13 +286,15 @@ def test_class_cellmeasuremetadata(self): ) def test_class_connectivitymetadata(self): - self.args.update(dict(cf_role=None, start_index=None, src_dim=None)) + self.args.update( + dict(cf_role=None, start_index=None, location_axis=None) + ) metadata = ConnectivityMetadata(**self.args) self.item.metadata = metadata expected = metadata._asdict() del expected["cf_role"] del expected["start_index"] - del expected["src_dim"] + del expected["location_axis"] self.assertEqual(self.item._metadata_manager.values, expected) self.assertIsNot( self.item._metadata_manager.attributes, metadata.attributes diff --git a/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py b/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py index b7c59ff566..0c20f16f5a 100644 --- a/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py +++ b/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py @@ -36,7 +36,7 @@ def test_vectorise_call(self): # The reason we use numpy.vectorize is to support multi-dimensional # coordinate points. def fn(coord, v): - return v ** 2 + return v**2 with mock.patch( "numpy.vectorize", return_value=self.vectorised diff --git a/lib/iris/tests/unit/coords/test_AncillaryVariable.py b/lib/iris/tests/unit/coords/test_AncillaryVariable.py index e94ad0cf70..4d520ac414 100644 --- a/lib/iris/tests/unit/coords/test_AncillaryVariable.py +++ b/lib/iris/tests/unit/coords/test_AncillaryVariable.py @@ -433,11 +433,18 @@ def test_non_time_values(self): units="m", attributes={"notes": "Measured from sea level"}, ) - expected = ( - "AncillaryVariable(array([2, 5, 9]), " - "standard_name='height', units=Unit('m'), " - "long_name='height of detector', var_name='height', " - "attributes={'notes': 'Measured from sea level'})" + expected = "\n".join( + [ + "AncillaryVariable : height / (m)", + " data: [2, 5, 9]", + " shape: (3,)", + " dtype: int64", + " standard_name: 'height'", + " long_name: 'height of detector'", + " var_name: 'height'", + " attributes:", + " notes 'Measured from sea level'", + ] ) self.assertEqual(expected, ancillary_var.__str__()) @@ -447,11 +454,20 @@ def test_time_values(self): units="hours since 1970-01-01 01:00", long_name="time of previous valid detection", ) - expected = ( - "AncillaryVariable([1970-01-01 03:00:00, " - "1970-01-01 06:00:00, 1970-01-01 10:00:00], " - "standard_name=None, calendar='gregorian', " - "long_name='time of previous valid detection')" + expected = "\n".join( + [ + ( + "AncillaryVariable : time of previous valid detection / " + "(hours since 1970-01-01 01:00, gregorian calendar)" + ), + ( + " data: [1970-01-01 03:00:00, 1970-01-01 06:00:00, " + "1970-01-01 10:00:00]" + ), + " shape: (3,)", + " dtype: int64", + " long_name: 'time of previous valid detection'", + ] ) self.assertEqual(expected, ancillary_var.__str__()) @@ -466,12 +482,7 @@ def test_non_time_values(self): units="m", attributes={"notes": "Measured from sea level"}, ) - expected = ( - "AncillaryVariable(array([2, 5, 9]), " - "standard_name='height', units=Unit('m'), " - "long_name='height of detector', var_name='height', " - "attributes={'notes': 'Measured from sea level'})" - ) + expected = "" self.assertEqual(expected, ancillary_var.__repr__()) def test_time_values(self): @@ -481,10 +492,8 @@ def test_time_values(self): long_name="time of previous valid detection", ) expected = ( - "AncillaryVariable(array([2, 5, 9]), standard_name=None, " - "units=Unit('hours since 1970-01-01 01:00', " - "calendar='gregorian'), " - "long_name='time of previous valid detection')" + "" ) self.assertEqual(expected, ancillary_var.__repr__()) diff --git a/lib/iris/tests/unit/coords/test_CellMeasure.py b/lib/iris/tests/unit/coords/test_CellMeasure.py index c5016e6c73..0bd66c6e98 100644 --- a/lib/iris/tests/unit/coords/test_CellMeasure.py +++ b/lib/iris/tests/unit/coords/test_CellMeasure.py @@ -93,30 +93,29 @@ def test_copy(self): copy_measure = self.measure.copy(new_vals) self.assertArrayEqual(copy_measure.data, new_vals) - def test_repr_other_metadata(self): - expected = ( - ", long_name='measured_area', " - "var_name='area', attributes={'notes': '1m accuracy'}" - ) - self.assertEqual(self.measure._repr_other_metadata(), expected) - def test___str__(self): - expected = ( - "CellMeasure(array([10., 12., 16., 9.]), " - "measure='area', standard_name='cell_area', " - "units=Unit('m^2'), long_name='measured_area', " - "var_name='area', attributes={'notes': '1m accuracy'})" + expected = "\n".join( + [ + "CellMeasure : cell_area / (m^2)", + " data: [10., 12., 16., 9.]", + " shape: (4,)", + " dtype: float64", + " standard_name: 'cell_area'", + " long_name: 'measured_area'", + " var_name: 'area'", + " attributes:", + " notes '1m accuracy'", + " measure: 'area'", + ] ) self.assertEqual(self.measure.__str__(), expected) def test___repr__(self): expected = ( - "CellMeasure(array([10., 12., 16., 9.]), " - "measure='area', standard_name='cell_area', " - "units=Unit('m^2'), long_name='measured_area', " - "var_name='area', attributes={'notes': '1m accuracy'})" + "" ) - self.assertEqual(self.measure.__repr__(), expected) + self.assertEqual(expected, self.measure.__repr__()) def test__eq__(self): self.assertEqual(self.measure, self.measure) diff --git a/lib/iris/tests/unit/coords/test_Coord.py b/lib/iris/tests/unit/coords/test_Coord.py index 640dbcd131..43170b6c4e 100644 --- a/lib/iris/tests/unit/coords/test_Coord.py +++ b/lib/iris/tests/unit/coords/test_Coord.py @@ -883,9 +883,14 @@ def test_short_time_interval(self): coord = DimCoord( [5], standard_name="time", units="days since 1970-01-01" ) - expected = ( - "DimCoord([1970-01-06 00:00:00], standard_name='time', " - "calendar='gregorian')" + expected = "\n".join( + [ + "DimCoord : time / (days since 1970-01-01, gregorian calendar)", + " points: [1970-01-06 00:00:00]", + " shape: (1,)", + " dtype: int64", + " standard_name: 'time'", + ] ) result = coord.__str__() self.assertEqual(expected, result) @@ -895,11 +900,17 @@ def test_short_time_interval__bounded(self): [5, 6], standard_name="time", units="days since 1970-01-01" ) coord.guess_bounds() - expected = ( - "DimCoord([1970-01-06 00:00:00, 1970-01-07 00:00:00], " - "bounds=[[1970-01-05 12:00:00, 1970-01-06 12:00:00],\n" - " [1970-01-06 12:00:00, 1970-01-07 12:00:00]], " - "standard_name='time', calendar='gregorian')" + expected = "\n".join( + [ + "DimCoord : time / (days since 1970-01-01, gregorian calendar)", + " points: [1970-01-06 00:00:00, 1970-01-07 00:00:00]", + " bounds: [", + " [1970-01-05 12:00:00, 1970-01-06 12:00:00],", + " [1970-01-06 12:00:00, 1970-01-07 12:00:00]]", + " shape: (2,) bounds(2, 2)", + " dtype: int64", + " standard_name: 'time'", + ] ) result = coord.__str__() self.assertEqual(expected, result) @@ -908,7 +919,15 @@ def test_long_time_interval(self): coord = DimCoord( [5], standard_name="time", units="years since 1970-01-01" ) - expected = "DimCoord([5], standard_name='time', calendar='gregorian')" + expected = "\n".join( + [ + "DimCoord : time / (years since 1970-01-01, gregorian calendar)", + " points: [5]", + " shape: (1,)", + " dtype: int64", + " standard_name: 'time'", + ] + ) result = coord.__str__() self.assertEqual(expected, result) @@ -917,16 +936,31 @@ def test_long_time_interval__bounded(self): [5, 6], standard_name="time", units="years since 1970-01-01" ) coord.guess_bounds() - expected = ( - "DimCoord([5 6], bounds=[[4.5 5.5]\n [5.5 6.5]], " - "standard_name='time', calendar='gregorian')" + expected = "\n".join( + [ + "DimCoord : time / (years since 1970-01-01, gregorian calendar)", + " points: [5, 6]", + " bounds: [", + " [4.5, 5.5],", + " [5.5, 6.5]]", + " shape: (2,) bounds(2, 2)", + " dtype: int64", + " standard_name: 'time'", + ] ) result = coord.__str__() self.assertEqual(expected, result) def test_non_time_unit(self): coord = DimCoord([1.0]) - expected = repr(coord) + expected = "\n".join( + [ + "DimCoord : unknown / (unknown)", + " points: [1.]", + " shape: (1,)", + " dtype: float64", + ] + ) result = coord.__str__() self.assertEqual(expected, result) diff --git a/lib/iris/tests/unit/coords/test__DimensionalMetadata.py b/lib/iris/tests/unit/coords/test__DimensionalMetadata.py index 82bd51a8af..fd10a6f264 100644 --- a/lib/iris/tests/unit/coords/test__DimensionalMetadata.py +++ b/lib/iris/tests/unit/coords/test__DimensionalMetadata.py @@ -9,7 +9,22 @@ # importing anything else. import iris.tests as tests # isort:skip -from iris.coords import _DimensionalMetadata + +from cf_units import Unit +import numpy as np + +import iris._lazy_data as lazy +from iris.coord_systems import GeogCS +from iris.coords import ( + AncillaryVariable, + AuxCoord, + CellMeasure, + DimCoord, + _DimensionalMetadata, +) +from iris.experimental.ugrid.mesh import Connectivity +from iris.tests.stock import climatology_3d as cube_with_climatology +from iris.tests.stock.mesh import sample_meshcoord class Test___init____abstractmethod(tests.IrisTest): @@ -22,5 +37,1046 @@ def test(self): _ = _DimensionalMetadata(0) +class Mixin__string_representations: + """ + Common testcode for generic `__str__`, `__repr__` and `summary` methods. + + Effectively, __str__ and __repr__ are thin wrappers around `summary`. + These are used by all the subclasses : notably Coord/DimCoord/AuxCoord, + but also AncillaryVariable, CellMeasure and MeshCoord. + + There are a lot of different aspects to consider: + + * different object classes with different class-specific properties + * changing with array sizes + dimensionalities + * masked data + * data types : int, float, string and (special) dates + * for Coords, handling of bounds + * "summary" controls (also can be affected by numpy printoptions). + + NOTE: since the details of formatting are important to us here, the basic + test method is to check printout results against an exact 'snapshot' + embedded (visibly) in the test itself. + + """ + + def repr_str_strings(self, dm, linewidth=55): + """ + Return a simple combination of repr and str printouts. + + N.B. we control linewidth to make the outputs easier to compare. + """ + with np.printoptions(linewidth=linewidth): + result = repr(dm) + "\n" + str(dm) + return result + + def sample_data(self, datatype=float, units="m", shape=(5,), masked=False): + """Make a sample data array for a test _DimensionalMetadata object.""" + # Get an actual Unit + units = Unit(units) + if units.calendar: + # fix string datatypes for date-based units + datatype = float + + # Get a dtype + dtype = np.dtype(datatype) + + # Make suitable test values for type/shape/masked + length = int(np.prod(shape)) + if dtype.kind == "U": + # String content. + digit_strs = [str(i) * (i + 1) for i in range(0, 10)] + if length < 10: + # ['0', '11', '222, '3333', ..] + values = np.array(digit_strs[:length]) + else: + # [... '9999999999', '0', '11' ....] + indices = [(i % 10) for i in range(length)] + values = np.array(digit_strs)[indices] + else: + # numeric content : a simple [0, 1, 2 ...] + values = np.arange(length).astype(dtype) + + if masked: + if np.prod(shape) >= 3: + # Mask 1 in 3 points : [x -- x x -- x ...] + i_firstmasked = 1 + else: + # Few points, mask 1 in 3 starting at 0 [-- x x -- x x -- ...] + i_firstmasked = 0 + masked_points = [(i % 3) == i_firstmasked for i in range(length)] + values = np.ma.masked_array(values, mask=masked_points) + + values = values.reshape(shape) + return values + + # Make a sample Coord, as _DimensionalMetadata is abstract and this is the + # obvious concrete subclass to use for testing + def sample_coord( + self, + datatype=float, + dates=False, + units="m", + long_name="x", + shape=(5,), + masked=False, + bounded=False, + dimcoord=False, + lazy_points=False, + lazy_bounds=False, + *coord_args, + **coord_kwargs, + ): + if masked: + dimcoord = False + if dates: + # Use a pre-programmed date unit. + units = Unit("days since 1970-03-5") + if not isinstance(units, Unit): + # This operation is *not* a no-op, it will wipe calendars ! + units = Unit(units) + values = self.sample_data( + datatype=datatype, units=units, shape=shape, masked=masked + ) + cls = DimCoord if dimcoord else AuxCoord + coord = cls( + points=values, + units=units, + long_name=long_name, + *coord_args, + **coord_kwargs, + ) + if bounded or lazy_bounds: + if shape == (1,): + # Guess-bounds doesn't work ! + val = coord.points[0] + bounds = [val - 10, val + 10] + # NB preserve masked/unmasked : avoid converting masks to NaNs + if np.ma.isMaskedArray(coord.points): + array = np.ma.array + else: + array = np.array + coord.bounds = array(bounds) + else: + coord.guess_bounds() + if lazy_points: + coord.points = lazy.as_lazy_data(coord.points) + if lazy_bounds: + coord.bounds = lazy.as_lazy_data(coord.bounds) + return coord + + def coord_representations(self, *args, **kwargs): + """ + Create a test coord and return its string representations. + + Pass args+kwargs to 'sample_coord' and return the 'repr_str_strings'. + + """ + coord = self.sample_coord(*args, **kwargs) + return self.repr_str_strings(coord) + + def assertLines(self, list_of_expected_lines, string_result): + """ + Assert equality between a result and expected output lines. + + For convenience, the 'expected lines' are joined with a '\\n', + because a list of strings is nicer to construct in code. + They should then match the actual result, which is a simple string. + + """ + self.assertEqual(list_of_expected_lines, string_result.split("\n")) + + +class Test__print_common(Mixin__string_representations, tests.IrisTest): + """ + Test aspects of __str__ and __repr__ output common to all + _DimensionalMetadata instances. + I.E. those from CFVariableMixin, plus values array (data-manager). + + Aspects : + * standard_name: + * long_name: + * var_name: + * attributes + * units + * shape + * dtype + + """ + + def test_simple(self): + result = self.coord_representations() + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0., 1., 2., 3., 4.]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_minimal(self): + result = self.coord_representations( + long_name=None, units=None, shape=(1,) + ) + expected = [ + "", + "AuxCoord : unknown / (unknown)", + " points: [0.]", + " shape: (1,)", + " dtype: float64", + ] + self.assertLines(expected, result) + + def test_names(self): + result = self.coord_representations( + standard_name="height", long_name="this", var_name="x_var" + ) + expected = [ + "", + "AuxCoord : height / (m)", + " points: [0., 1., 2., 3., 4.]", + " shape: (5,)", + " dtype: float64", + " standard_name: 'height'", + " long_name: 'this'", + " var_name: 'x_var'", + ] + self.assertLines(expected, result) + + def test_bounded(self): + result = self.coord_representations(shape=(3,), bounded=True) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0., 1., 2.]", + " bounds: [", + " [-0.5, 0.5],", + " [ 0.5, 1.5],", + " [ 1.5, 2.5]]", + " shape: (3,) bounds(3, 2)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_masked(self): + result = self.coord_representations(masked=True) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0.0, -- , 2.0, 3.0, -- ]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_dtype_int(self): + result = self.coord_representations(units="1", datatype=np.int16) + expected = [ + "", + "AuxCoord : x / (1)", + " points: [0, 1, 2, 3, 4]", + " shape: (5,)", + " dtype: int16", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_dtype_date(self): + # Note: test with a date 'longer' than the built-in one in + # 'sample_coord(dates=True)', because it includes a time-of-day + full_date_unit = Unit( + "days since 1892-05-17 03:00:25", calendar="360_day" + ) + result = self.coord_representations(units=full_date_unit) + expected = [ + ( + "" + ), + ( + "AuxCoord : x / (days since 1892-05-17 03:00:25, " + "360_day calendar)" + ), + " points: [", + " 1892-05-17 03:00:25, 1892-05-18 03:00:25,", + " 1892-05-19 03:00:25, 1892-05-20 03:00:25,", + " 1892-05-21 03:00:25]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_attributes(self): + # NOTE: scheduled for future change, to put each attribute on a line + coord = self.sample_coord( + attributes={ + "array": np.arange(7.0), + "list": [1, 2, 3], + "empty": [], + "None": None, + "string": "this", + "long_long_long_long_long_name": 3, + "other": ( + "long_long_long_long_long_long_long_long_" + "long_long_long_long_long_long_long_long_value" + ), + "float": 4.3, + } + ) + result = self.repr_str_strings(coord) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0., 1., 2., 3., 4.]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + " attributes:", + " array [0. 1. 2. 3. 4. 5. 6.]", + " list [1, 2, 3]", + " empty []", + " None None", + " string 'this'", + " long_long_long_long_long_name 3", + ( + " other " + "'long_long_long_long_long_long_long_long_" + "long_long_long_long_long_long..." + ), + " float 4.3", + ] + self.assertLines(expected, result) + + def test_lazy_points(self): + result = self.coord_representations(lazy_points=True) + expected = [ + " shape(5,)>", + "AuxCoord : x / (m)", + " points: ", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_lazy_bounds(self): + result = self.coord_representations(lazy_bounds=True) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0., 1., 2., 3., 4.]", + " bounds: ", + " shape: (5,) bounds(5, 2)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_lazy_points_and_bounds(self): + result = self.coord_representations(lazy_points=True, lazy_bounds=True) + expected = [ + "+bounds shape(5,)>", + "AuxCoord : x / (m)", + " points: ", + " bounds: ", + " shape: (5,) bounds(5, 2)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_scalar(self): + result = self.coord_representations(shape=(1,), bounded=True) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0.]", + " bounds: [[-10., 10.]]", + " shape: (1,) bounds(1, 2)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_scalar_masked(self): + result = self.coord_representations( + shape=(1,), bounded=True, masked=True + ) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [--]", + " bounds: [[--, --]]", + " shape: (1,) bounds(1, 2)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_length_short(self): + result = self.coord_representations(shape=(2,), bounded=True) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0., 1.]", + " bounds: [", + " [-0.5, 0.5],", + " [ 0.5, 1.5]]", + " shape: (2,) bounds(2, 2)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_length_medium(self): + # Where bounds are truncated, but points not. + result = self.coord_representations(shape=(14,), bounded=True) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [", + " 0., 1., 2., 3., 4., 5., 6., 7., 8.,", + " 9., 10., 11., 12., 13.]", + " bounds: [", + " [-0.5, 0.5],", + " [ 0.5, 1.5],", + " ...,", + " [11.5, 12.5],", + " [12.5, 13.5]]", + " shape: (14,) bounds(14, 2)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_length_long(self): + # Completely truncated representations + result = self.coord_representations(shape=(150,), bounded=True) + expected = [ + ( + "" + ), + "AuxCoord : x / (m)", + " points: [ 0., 1., ..., 148., 149.]", + " bounds: [", + " [ -0.5, 0.5],", + " [ 0.5, 1.5],", + " ...,", + " [147.5, 148.5],", + " [148.5, 149.5]]", + " shape: (150,) bounds(150, 2)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_strings(self): + result = self.coord_representations(datatype=str) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0 , 11 , 222 , 3333 , 44444]", + " shape: (5,)", + " dtype: ", + "AuxCoord : x / (m)", + " points: [", + " 0 , 11 , 222 ,", + " 3333 , 44444 , 555555 ,", + " 6666666 , 77777777 , 888888888 ,", + " 9999999999, 0 , 11 ,", + " 222 , 3333 , 44444 ]", + " shape: (15,)", + " dtype: ", + "AuxCoord : x / (days since 1970-03-5, gregorian calendar)", + " points: [1970-03-05 00:00:00, 1970-03-06 00:00:00]", + " shape: (2,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_dates_scalar(self): + # Printouts for a scalar date coord. + # Demonstrate that a "typical" datetime coord can print with the date + # value visible in the repr. + long_time_unit = Unit("hours since 2025-03-23 01:00:00") + coord = self.sample_coord( + standard_name="time", + long_name=None, + shape=(1,), + units=long_time_unit, + ) + # Do this one with a default linewidth, not our default reduced one, so + # that we can get the date value in the repr output. + result = self.repr_str_strings(coord, linewidth=None) + expected = [ + ( + "" + ), + ( + "AuxCoord : time / (hours since 2025-03-23 01:00:00, " + "gregorian calendar)" + ), + " points: [2025-03-23 01:00:00]", + " shape: (1,)", + " dtype: float64", + " standard_name: 'time'", + ] + self.assertLines(expected, result) + + def test_dates_bounds(self): + result = self.coord_representations(dates=True, bounded=True) + expected = [ + "", + "AuxCoord : x / (days since 1970-03-5, gregorian calendar)", + " points: [", + " 1970-03-05 00:00:00, 1970-03-06 00:00:00,", + " 1970-03-07 00:00:00, 1970-03-08 00:00:00,", + " 1970-03-09 00:00:00]", + " bounds: [", + " [1970-03-04 12:00:00, 1970-03-05 12:00:00],", + " [1970-03-05 12:00:00, 1970-03-06 12:00:00],", + " [1970-03-06 12:00:00, 1970-03-07 12:00:00],", + " [1970-03-07 12:00:00, 1970-03-08 12:00:00],", + " [1970-03-08 12:00:00, 1970-03-09 12:00:00]]", + " shape: (5,) bounds(5, 2)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_dates_masked(self): + result = self.coord_representations(dates=True, masked=True) + expected = [ + "", + "AuxCoord : x / (days since 1970-03-5, gregorian calendar)", + " points: [", + " 1970-03-05 00:00:00, -- ,", + " 1970-03-07 00:00:00, 1970-03-08 00:00:00,", + " -- ]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_untypical_bounds(self): + # Check printing when n-bounds > 2 + coord = self.sample_coord() + bounds = coord.points.reshape((5, 1)) + np.array([[-3.0, -2, 2, 3]]) + coord.bounds = bounds + result = self.repr_str_strings(coord) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0., 1., 2., 3., 4.]", + " bounds: [", + " [-3., -2., 2., 3.],", + " [-2., -1., 3., 4.],", + " ...,", + " [ 0., 1., 5., 6.],", + " [ 1., 2., 6., 7.]]", + " shape: (5,) bounds(5, 4)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_multidimensional(self): + # Demonstrate formatting of multdimensional arrays + result = self.coord_representations(shape=(7, 5, 3)) + # This one is a bit unavoidably long .. + expected = [ + "", + "AuxCoord : x / (m)", + " points: [", + " [[ 0., 1., 2.],", + " [ 3., 4., 5.],", + " ...,", + " [ 9., 10., 11.],", + " [ 12., 13., 14.]],", + " ", + " [[ 15., 16., 17.],", + " [ 18., 19., 20.],", + " ...,", + " [ 24., 25., 26.],", + " [ 27., 28., 29.]],", + " ", + " ...,", + " ", + " [[ 75., 76., 77.],", + " [ 78., 79., 80.],", + " ...,", + " [ 84., 85., 86.],", + " [ 87., 88., 89.]],", + " ", + " [[ 90., 91., 92.],", + " [ 93., 94., 95.],", + " ...,", + " [ 99., 100., 101.],", + " [102., 103., 104.]]]", + " shape: (7, 5, 3)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_multidimensional_small(self): + # Demonstrate that a small-enough multidim will print in the repr. + result = self.coord_representations(shape=(2, 2), datatype=int) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [", + " [0, 1],", + " [2, 3]]", + " shape: (2, 2)", + " dtype: int64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_integers_short(self): + result = self.coord_representations(datatype=np.int16) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0, 1, 2, 3, 4]", + " shape: (5,)", + " dtype: int16", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_integers_masked(self): + result = self.coord_representations(datatype=int, masked=True) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0 , --, 2 , 3 , --]", + " shape: (5,)", + " dtype: int64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_integers_masked_long(self): + result = self.coord_representations( + shape=(20,), datatype=int, masked=True + ) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0 , --, ..., 18, --]", + " shape: (20,)", + " dtype: int64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + +class Test__print_Coord(Mixin__string_representations, tests.IrisTest): + """ + Test Coord-specific aspects of __str__ and __repr__ output. + + Aspects : + * DimCoord / AuxCoord + * coord_system + * climatological + * circular + + """ + + def test_dimcoord(self): + result = self.coord_representations(dimcoord=True) + expected = [ + "", + "DimCoord : x / (m)", + " points: [0., 1., 2., 3., 4.]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_coord_system(self): + result = self.coord_representations(coord_system=GeogCS(1000.0)) + expected = [ + "", + "AuxCoord : x / (m)", + " points: [0., 1., 2., 3., 4.]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + " coord_system: GeogCS(1000.0)", + ] + self.assertLines(expected, result) + + def test_climatological(self): + cube = cube_with_climatology() + coord = cube.coord("time") + coord = coord[:1] # Just to make it a bit shorter + result = self.repr_str_strings(coord) + expected = [ + ( + "" + ), + ( + "DimCoord : time / (days since 1970-01-01 00:00:00-00, " + "gregorian calendar)" + ), + " points: [2001-01-10 00:00:00]", + " bounds: [[2001-01-10 00:00:00, 2011-01-10 00:00:00]]", + " shape: (1,) bounds(1, 2)", + " dtype: float64", + " standard_name: 'time'", + " climatological: True", + ] + self.assertLines(expected, result) + + def test_circular(self): + coord = self.sample_coord(shape=(2,), dimcoord=True) + coord.circular = True + result = self.repr_str_strings(coord) + expected = [ + "", + "DimCoord : x / (m)", + " points: [0., 1.]", + " shape: (2,)", + " dtype: float64", + " long_name: 'x'", + " circular: True", + ] + self.assertLines(expected, result) + + +class Test__print_noncoord(Mixin__string_representations, tests.IrisTest): + """ + Limited testing of other _DimensionalMetadata subclasses. + + * AncillaryVariable + * CellMeasure + * Connectivity + * MeshCoord + + """ + + def test_ancillary(self): + # Check we can print an AncillaryVariable + # Practically, ~identical to an AuxCoord, but without bounds, and the + # array is called 'data'. + data = self.sample_data() + ancil = AncillaryVariable(data, long_name="v_aux", units="m s-1") + result = self.repr_str_strings(ancil) + expected = [ + "", + "AncillaryVariable : v_aux / (m s-1)", + " data: [0., 1., 2., 3., 4.]", + " shape: (5,)", + " dtype: float64", + " long_name: 'v_aux'", + ] + self.assertLines(expected, result) + + def test_cellmeasure(self): + # Check we can print an AncillaryVariable + # N.B. practically, identical to an AuxCoord (without bounds) + # Check we can print an AncillaryVariable + # Practically, ~identical to an AuxCoord, but without bounds, and the + # array is called 'data'. + data = self.sample_data() + cell_measure = CellMeasure( + data, measure="area", long_name="cell_area", units="m^2" + ) + result = self.repr_str_strings(cell_measure) + expected = [ + "", + "CellMeasure : cell_area / (m^2)", + " data: [0., 1., 2., 3., 4.]", + " shape: (5,)", + " dtype: float64", + " long_name: 'cell_area'", + " measure: 'area'", + ] + self.assertLines(expected, result) + + def test_connectivity(self): + # Check we can print a Connectivity + # Like a Coord, but always print : cf_role, location_axis, start_index + data = self.sample_data(shape=(3, 2), datatype=int) + conn = Connectivity( + data, cf_role="edge_node_connectivity", long_name="enc", units="1" + ) + result = self.repr_str_strings(conn) + expected = [ + "", + "Connectivity : enc / (1)", + " data: [", + " [0, 1],", + " [2, 3],", + " [4, 5]]", + " shape: (3, 2)", + " dtype: int64", + " long_name: 'enc'", + " cf_role: 'edge_node_connectivity'", + " start_index: 0", + " location_axis: 0", + ] + self.assertLines(expected, result) + + def test_connectivity__start_index(self): + # Check we can print a Connectivity + # Like a Coord, but always print : cf_role, location_axis, start_index + data = self.sample_data(shape=(3, 2), datatype=int) + conn = Connectivity( + data + 1, + start_index=1, + cf_role="edge_node_connectivity", + long_name="enc", + units="1", + ) + result = self.repr_str_strings(conn) + expected = [ + "", + "Connectivity : enc / (1)", + " data: [", + " [1, 2],", + " [3, 4],", + " [5, 6]]", + " shape: (3, 2)", + " dtype: int64", + " long_name: 'enc'", + " cf_role: 'edge_node_connectivity'", + " start_index: 1", + " location_axis: 0", + ] + self.assertLines(expected, result) + + def test_connectivity__location_axis(self): + # Check we can print a Connectivity + # Like a Coord, but always print : cf_role, location_axis, start_index + data = self.sample_data(shape=(3, 2), datatype=int) + conn = Connectivity( + data.transpose(), + location_axis=1, + cf_role="edge_node_connectivity", + long_name="enc", + units="1", + ) + result = self.repr_str_strings(conn) + expected = [ + "", + "Connectivity : enc / (1)", + " data: [", + " [0, 2, 4],", + " [1, 3, 5]]", + " shape: (2, 3)", + " dtype: int64", + " long_name: 'enc'", + " cf_role: 'edge_node_connectivity'", + " start_index: 0", + " location_axis: 1", + ] + self.assertLines(expected, result) + + def test_meshcoord(self): + meshco = sample_meshcoord() + meshco.mesh.long_name = "test_mesh" # For stable printout of the Mesh + result = self.repr_str_strings(meshco) + expected = [ + ( + "" + ), + "MeshCoord : longitude / (degrees_east)", + " mesh: ", + " location: 'face'", + " points: [3100, 3101, 3102]", + " bounds: [", + " [1100, 1101, 1102, 1103],", + " [1104, 1105, 1106, 1107],", + " [1108, 1109, 1110, 1111]]", + " shape: (3,) bounds(3, 4)", + " dtype: int64", + " standard_name: 'longitude'", + " long_name: 'long-name'", + " attributes:", + " a 1", + " b 'c'", + " axis: 'x'", + ] + self.assertLines(expected, result) + + +class Test_summary(Mixin__string_representations, tests.IrisTest): + """ + Test the controls of the 'summary' method. + """ + + def test_shorten(self): + coord = self.sample_coord() + expected = self.repr_str_strings(coord) + result = coord.summary(shorten=True) + "\n" + coord.summary() + self.assertEqual(expected, result) + + def test_max_values__default(self): + coord = self.sample_coord() + result = coord.summary() + expected = [ + "AuxCoord : x / (m)", + " points: [0., 1., 2., 3., 4.]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_max_values__2(self): + coord = self.sample_coord() + result = coord.summary(max_values=2) + expected = [ + "AuxCoord : x / (m)", + " points: [0., 1., ..., 3., 4.]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_max_values__bounded__2(self): + coord = self.sample_coord(bounded=True) + result = coord.summary(max_values=2) + expected = [ + "AuxCoord : x / (m)", + " points: [0., 1., ..., 3., 4.]", + " bounds: [", + " [-0.5, 0.5],", + " [ 0.5, 1.5],", + " ...,", + " [ 2.5, 3.5],", + " [ 3.5, 4.5]]", + " shape: (5,) bounds(5, 2)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_max_values__0(self): + coord = self.sample_coord(bounded=True) + result = coord.summary(max_values=0) + expected = [ + "AuxCoord : x / (m)", + " points: [...]", + " bounds: [...]", + " shape: (5,) bounds(5, 2)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_linewidth__default(self): + coord = self.sample_coord() + coord.points = coord.points + 1000.003 # Make the output numbers wider + result = coord.summary() + expected = [ + "AuxCoord : x / (m)", + " points: [1000.003, 1001.003, 1002.003, 1003.003, 1004.003]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + # Show that, when unset, it follows the numpy setting + with np.printoptions(linewidth=35): + result = coord.summary() + expected = [ + "AuxCoord : x / (m)", + " points: [", + " 1000.003, 1001.003,", + " 1002.003, 1003.003,", + " 1004.003]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + def test_linewidth__set(self): + coord = self.sample_coord() + coord.points = coord.points + 1000.003 # Make the output numbers wider + expected = [ + "AuxCoord : x / (m)", + " points: [", + " 1000.003, 1001.003,", + " 1002.003, 1003.003,", + " 1004.003]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + result = coord.summary(linewidth=35) + self.assertLines(expected, result) + + with np.printoptions(linewidth=999): + # Show that, when set, it ignores the numpy setting + result = coord.summary(linewidth=35) + self.assertLines(expected, result) + + def test_convert_dates(self): + coord = self.sample_coord(dates=True) + result = coord.summary() + expected = [ + "AuxCoord : x / (days since 1970-03-5, gregorian calendar)", + " points: [", + ( + " 1970-03-05 00:00:00, 1970-03-06 00:00:00, " + "1970-03-07 00:00:00," + ), + " 1970-03-08 00:00:00, 1970-03-09 00:00:00]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + result = coord.summary(convert_dates=False) + expected = [ + "AuxCoord : x / (days since 1970-03-5, gregorian calendar)", + " points: [0., 1., 2., 3., 4.]", + " shape: (5,)", + " dtype: float64", + " long_name: 'x'", + ] + self.assertLines(expected, result) + + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index f4daf64bae..7d56b505bd 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -757,7 +757,29 @@ def test_agg_by_label(self): res_cube = self.cube.aggregated_by("label", self.mock_agg) val_coord = AuxCoord( np.array([1.0, 0.5, 1.0]), - bounds=np.array([[0, 2], [0, 1], [2, 0]]), + bounds=np.array([[0, 2], [0, 1], [0, 2]]), + long_name="val", + ) + label_coord = AuxCoord( + np.array(["alpha", "beta", "gamma"]), + long_name="label", + units="no_unit", + ) + self.assertEqual(res_cube.coord("val"), val_coord) + self.assertEqual(res_cube.coord("label"), label_coord) + + def test_agg_by_label_bounded(self): + # Aggregate a cube on a string coordinate label where label + # and val entries are not in step; the resulting cube has a val + # coord of bounded cells and a label coord of single string entries. + val_points = self.cube.coord("val").points + self.cube.coord("val").bounds = np.array( + [val_points - 0.5, val_points + 0.5] + ).T + res_cube = self.cube.aggregated_by("label", self.mock_agg) + val_coord = AuxCoord( + np.array([1.0, 0.5, 1.0]), + bounds=np.array([[-0.5, 2.5], [-0.5, 1.5], [-0.5, 2.5]]), long_name="val", ) label_coord = AuxCoord( @@ -890,7 +912,7 @@ def test_agg_by_label__lazy(self): res_cube = self.cube.aggregated_by("label", MEAN) val_coord = AuxCoord( np.array([1.0, 0.5, 1.0]), - bounds=np.array([[0, 2], [0, 1], [2, 0]]), + bounds=np.array([[0, 2], [0, 1], [0, 2]]), long_name="val", ) label_coord = AuxCoord( diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Connectivity.py b/lib/iris/tests/unit/experimental/ugrid/mesh/test_Connectivity.py index 5d6f48fdda..9a81c79d44 100644 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Connectivity.py +++ b/lib/iris/tests/unit/experimental/ugrid/mesh/test_Connectivity.py @@ -29,28 +29,30 @@ def setUp(self): "var_name": "face_nodes", "attributes": {"notes": "this is a test"}, "start_index": 1, - "src_dim": 1, + "location_axis": 1, } self.connectivity = Connectivity(**self.kwargs) def test_cf_role(self): self.assertEqual(self.kwargs["cf_role"], self.connectivity.cf_role) - def test_src_location(self): + def test_location(self): expected = self.kwargs["cf_role"].split("_")[0] - self.assertEqual(expected, self.connectivity.src_location) + self.assertEqual(expected, self.connectivity.location) - def test_tgt_location(self): + def test_connected(self): expected = self.kwargs["cf_role"].split("_")[1] - self.assertEqual(expected, self.connectivity.tgt_location) + self.assertEqual(expected, self.connectivity.connected) def test_start_index(self): self.assertEqual( self.kwargs["start_index"], self.connectivity.start_index ) - def test_src_dim(self): - self.assertEqual(self.kwargs["src_dim"], self.connectivity.src_dim) + def test_location_axis(self): + self.assertEqual( + self.kwargs["location_axis"], self.connectivity.location_axis + ) def test_indices(self): self.assertArrayEqual( @@ -58,7 +60,7 @@ def test_indices(self): ) def test_read_only(self): - attributes = ("indices", "cf_role", "start_index", "src_dim") + attributes = ("indices", "cf_role", "start_index", "location_axis") for attribute in attributes: self.assertRaisesRegex( AttributeError, @@ -70,10 +72,10 @@ def test_read_only(self): ) def test_transpose(self): - expected_dim = 1 - self.kwargs["src_dim"] + expected_dim = 1 - self.kwargs["location_axis"] expected_indices = self.kwargs["indices"].transpose() new_connectivity = self.connectivity.transpose() - self.assertEqual(expected_dim, new_connectivity.src_dim) + self.assertEqual(expected_dim, new_connectivity.location_axis) self.assertArrayEqual(expected_indices, new_connectivity.indices) def test_lazy_indices(self): @@ -87,39 +89,52 @@ def test_core_indices(self): def test_has_lazy_indices(self): self.assertFalse(self.connectivity.has_lazy_indices()) - def test_lazy_src_lengths(self): - self.assertTrue(is_lazy_data(self.connectivity.lazy_src_lengths())) + def test_lazy_location_lengths(self): + self.assertTrue( + is_lazy_data(self.connectivity.lazy_location_lengths()) + ) - def test_src_lengths(self): + def test_location_lengths(self): expected = [4, 4, 4] - self.assertArrayEqual(expected, self.connectivity.src_lengths()) + self.assertArrayEqual(expected, self.connectivity.location_lengths()) def test___str__(self): - expected = ( - "Connectivity(cf_role='face_node_connectivity', start_index=1)" + expected = "\n".join( + [ + "Connectivity : my_face_nodes / (unknown)", + " data: [", + " [ 1, 2, 3],", + " [ 4, 5, 6],", + " [ 7, 8, 9],", + " [10, 11, 12]]", + " shape: (4, 3)", + " dtype: int64", + " long_name: 'my_face_nodes'", + " var_name: 'face_nodes'", + " attributes:", + " notes 'this is a test'", + " cf_role: 'face_node_connectivity'", + " start_index: 1", + " location_axis: 1", + ] ) self.assertEqual(expected, self.connectivity.__str__()) def test___repr__(self): - expected = ( - "Connectivity(array([[ 1, 2, 3], [ 4, 5, 6], [ 7, 8, 9], [10, 11, 12]]), " - "cf_role='face_node_connectivity', long_name='my_face_nodes', " - "var_name='face_nodes', attributes={'notes': 'this is a test'}, " - "start_index=1, src_dim=1)" - ) + expected = "" self.assertEqual(expected, self.connectivity.__repr__()) def test_xml_element(self): doc = minidom.Document() connectivity_element = self.connectivity.xml_element(doc) self.assertEqual(connectivity_element.tagName, "connectivity") - for attribute in ("cf_role", "start_index", "src_dim"): + for attribute in ("cf_role", "start_index", "location_axis"): self.assertIn(attribute, connectivity_element.attributes) def test___eq__(self): equivalent_kwargs = self.kwargs equivalent_kwargs["indices"] = self.kwargs["indices"].transpose() - equivalent_kwargs["src_dim"] = 1 - self.kwargs["src_dim"] + equivalent_kwargs["location_axis"] = 1 - self.kwargs["location_axis"] equivalent = Connectivity(**equivalent_kwargs) self.assertFalse( np.array_equal(equivalent.indices, self.connectivity.indices) @@ -150,16 +165,18 @@ def test_copy(self): copy_connectivity = self.connectivity.copy(new_indices) self.assertArrayEqual(new_indices, copy_connectivity.indices) - def test_indices_by_src(self): + def test_indices_by_location(self): expected = self.kwargs["indices"].transpose() - self.assertArrayEqual(expected, self.connectivity.indices_by_src()) + self.assertArrayEqual( + expected, self.connectivity.indices_by_location() + ) - def test_indices_by_src_input(self): + def test_indices_by_location_input(self): expected = as_lazy_data(self.kwargs["indices"].transpose()) - by_src = self.connectivity.indices_by_src( + by_location = self.connectivity.indices_by_location( self.connectivity.lazy_indices() ) - self.assertArrayEqual(expected, by_src) + self.assertArrayEqual(expected, by_location) class TestAltIndices(tests.IrisTest): @@ -210,14 +227,14 @@ def test_start_index(self): ValueError, "Invalid start_index .", Connectivity, **kwargs ) - def test_src_dim(self): + def test_location_axis(self): kwargs = { "indices": np.linspace(1, 9, 9, dtype=int).reshape((-1, 3)), "cf_role": "face_node_connectivity", - "src_dim": 2, + "location_axis": 2, } self.assertRaisesRegex( - ValueError, "Invalid src_dim .", Connectivity, **kwargs + ValueError, "Invalid location_axis .", Connectivity, **kwargs ) def test_cf_role(self): @@ -275,7 +292,7 @@ def test_indices_locations_edge(self): } self.assertRaisesRegex( ValueError, - "Not all src_locations meet requirement: len=2", + "Not all edges meet requirement: len=2", Connectivity, **kwargs, ) @@ -287,7 +304,7 @@ def test_indices_locations_face(self): } self.assertRaisesRegex( ValueError, - "Not all src_locations meet requirement: len>=3", + "Not all faces meet requirement: len>=3", Connectivity, **kwargs, ) @@ -299,7 +316,7 @@ def test_indices_locations_volume_face(self): } self.assertRaisesRegex( ValueError, - "Not all src_locations meet requirement: len>=4", + "Not all volumes meet requirement: len>=4", Connectivity, **kwargs, ) @@ -311,7 +328,7 @@ def test_indices_locations_volume_edge(self): } self.assertRaisesRegex( ValueError, - "Not all src_locations meet requirement: len>=6", + "Not all volumes meet requirement: len>=6", Connectivity, **kwargs, ) @@ -321,11 +338,11 @@ def test_indices_locations_alt_dim(self): kwargs = { "indices": np.linspace(1, 9, 9, dtype=int).reshape((3, -1)), "cf_role": "volume_face_connectivity", - "src_dim": 1, + "location_axis": 1, } self.assertRaisesRegex( ValueError, - "Not all src_locations meet requirement: len>=4", + "Not all volumes meet requirement: len>=4", Connectivity, **kwargs, ) @@ -342,6 +359,10 @@ def test_indices_locations_masked(self): connectivity = Connectivity(**kwargs) self.assertRaisesRegex( ValueError, - "Not all src_locations meet requirement: len>=3", + "Not all faces meet requirement: len>=3", connectivity.validate_indices, ) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh.py b/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh.py index 9808660016..f39f3706ee 100644 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh.py +++ b/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh.py @@ -106,23 +106,33 @@ def test___getstate__(self): self.assertEqual(expected, self.mesh.__getstate__()) def test___repr__(self): - expected = ( - "Mesh(topology_dimension=1, node_coords_and_axes=[(AuxCoord(" - "array([0, 2, 1]), standard_name='longitude', units=Unit(" - "'unknown'), long_name='long_name', var_name='node_lon', " - "attributes={'test': 1}), 'x'), (AuxCoord(array([0, 0, 1]), " - "standard_name='latitude', units=Unit('unknown'), " - "var_name='node_lat'), 'y')], connectivities=Connectivity(" - "cf_role='edge_node_connectivity', start_index=0), " - "edge_coords_and_axes=[(AuxCoord(array([1. , 1.5, 0.5]), " - "standard_name='longitude', units=Unit('unknown'), " - "var_name='edge_lon'), 'x'), (AuxCoord(array([0. , 0.5, 0.5]), " - "standard_name='latitude', units=Unit('unknown'), " - "var_name='edge_lat'), 'y')], long_name='my_topology_mesh', " - "var_name='mesh', attributes={'notes': 'this is a test'}, " - "node_dimension='NodeDim', edge_dimension='EdgeDim')" - ) - self.assertEqual(expected, self.mesh.__repr__()) + expected = "" + self.assertEqual(expected, repr(self.mesh)) + + def test___str__(self): + expected = [ + "Mesh : 'my_topology_mesh'", + " topology_dimension: 1", + " node", + " node_dimension: 'NodeDim'", + " node coordinates", + " ", + " ", + " edge", + " edge_dimension: 'EdgeDim'", + ( + " edge_node_connectivity: " + "" + ), + " edge coordinates", + " ", + " ", + " long_name: 'my_topology_mesh'", + " var_name: 'mesh'", + " attributes:", + " notes 'this is a test'", + ] + self.assertEqual(expected, str(self.mesh).split("\n")) def test___eq__(self): # The dimension names do not participate in equality. @@ -201,7 +211,7 @@ def test_connectivities(self): for kwargs in negative_kwargs: self.assertEqual([], func(**kwargs)) - def test_connectivities_locations(self): + def test_connectivities_elements(self): # topology_dimension-specific results. Method intended to be overridden. positive_kwargs = ( {"contains_node": True}, @@ -261,7 +271,7 @@ def test_coords(self): for kwargs in negative_kwargs: self.assertNotIn(self.NODE_LON, func(**kwargs)) - def test_coords_locations(self): + def test_coords_elements(self): # topology_dimension-specific results. Method intended to be overridden. all_expected = { "node_x": self.NODE_LON, @@ -373,33 +383,103 @@ def setUpClass(cls): cls.mesh = mesh.Mesh(**cls.kwargs) def test___repr__(self): - expected = ( - "Mesh(topology_dimension=2, node_coords_and_axes=[(AuxCoord(" - "array([0, 2, 1]), standard_name='longitude', units=Unit(" - "'unknown'), long_name='long_name', var_name='node_lon', " - "attributes={'test': 1}), 'x'), (AuxCoord(array([0, 0, 1]), " - "standard_name='latitude', units=Unit('unknown'), " - "var_name='node_lat'), 'y')], connectivities=[Connectivity(" - "cf_role='face_node_connectivity', start_index=0), Connectivity(" - "cf_role='edge_node_connectivity', start_index=0), Connectivity(" - "cf_role='face_edge_connectivity', start_index=0), Connectivity(" - "cf_role='face_face_connectivity', start_index=0), Connectivity(" - "cf_role='edge_face_connectivity', start_index=0), Connectivity(" - "cf_role='boundary_node_connectivity', start_index=0)], " - "edge_coords_and_axes=[(AuxCoord(array([1. , 1.5, 0.5]), " - "standard_name='longitude', units=Unit('unknown'), " - "var_name='edge_lon'), 'x'), (AuxCoord(array([0. , 0.5, 0.5]), " - "standard_name='latitude', units=Unit('unknown'), " - "var_name='edge_lat'), 'y')], face_coords_and_axes=[(AuxCoord(" - "array([0.5]), standard_name='longitude', units=Unit('unknown'), " - "var_name='face_lon'), 'x'), (AuxCoord(array([0.5]), " - "standard_name='latitude', units=Unit('unknown'), " - "var_name='face_lat'), 'y')], long_name='my_topology_mesh', " - "var_name='mesh', attributes={'notes': 'this is a test'}, " - "node_dimension='NodeDim', edge_dimension='EdgeDim', " - "face_dimension='FaceDim')" - ) - self.assertEqual(expected, self.mesh.__repr__()) + expected = "" + self.assertEqual(expected, repr(self.mesh)) + + def test___str__(self): + expected = [ + "Mesh : 'my_topology_mesh'", + " topology_dimension: 2", + " node", + " node_dimension: 'NodeDim'", + " node coordinates", + " ", + " ", + " edge", + " edge_dimension: 'EdgeDim'", + ( + " edge_node_connectivity: " + "" + ), + " edge coordinates", + " ", + " ", + " face", + " face_dimension: 'FaceDim'", + ( + " face_node_connectivity: " + "" + ), + " face coordinates", + " ", + " ", + " optional connectivities", + ( + " face_face_connectivity: " + "" + ), + ( + " face_edge_connectivity: " + "" + ), + ( + " edge_face_connectivity: " + "" + ), + " long_name: 'my_topology_mesh'", + " var_name: 'mesh'", + " attributes:", + " notes 'this is a test'", + ] + self.assertEqual(expected, str(self.mesh).split("\n")) + + # Test some different options of the str() operation here. + def test___str__noedgecoords(self): + mesh_kwargs = self.kwargs.copy() + del mesh_kwargs["edge_coords_and_axes"] + alt_mesh = mesh.Mesh(**mesh_kwargs) + expected = [ + "Mesh : 'my_topology_mesh'", + " topology_dimension: 2", + " node", + " node_dimension: 'NodeDim'", + " node coordinates", + " ", + " ", + " edge", + " edge_dimension: 'EdgeDim'", + ( + " edge_node_connectivity: " + "" + ), + " face", + " face_dimension: 'FaceDim'", + ( + " face_node_connectivity: " + "" + ), + " face coordinates", + " ", + " ", + " optional connectivities", + ( + " face_face_connectivity: " + "" + ), + ( + " face_edge_connectivity: " + "" + ), + ( + " edge_face_connectivity: " + "" + ), + " long_name: 'my_topology_mesh'", + " var_name: 'mesh'", + " attributes:", + " notes 'this is a test'", + ] + self.assertEqual(expected, str(alt_mesh).split("\n")) def test_all_connectivities(self): expected = mesh.Mesh2DConnectivities( @@ -445,7 +525,7 @@ def test_connectivity(self): contains_face=False, ) - def test_connectivities_locations(self): + def test_connectivities_elements(self): kwargs_expected = ( ( {"contains_node": True}, @@ -501,7 +581,7 @@ def test_connectivities_locations(self): for item in expected: self.assertIn(item, result) - def test_coords_locations(self): + def test_coords_elements(self): all_expected = { "node_x": self.NODE_LON, "node_y": self.NODE_LAT, @@ -569,6 +649,93 @@ def test_face_node(self): self.assertEqual(self.FACE_NODE, self.mesh.face_node_connectivity) +class Test__str__various(TestMeshCommon): + # Some extra testing for the str() operation : based on 1D meshes as simpler + def setUp(self): + # All the tests here want modified meshes, so use standard setUp to + # create afresh for each test, allowing them to modify it. + super().setUp() + # Mesh kwargs with topology_dimension=1 and all applicable + # arguments populated - this tests correct property setting. + self.kwargs = { + "topology_dimension": 1, + "node_coords_and_axes": ( + (self.NODE_LON, "x"), + (self.NODE_LAT, "y"), + ), + "connectivities": [self.EDGE_NODE], + "long_name": "my_topology_mesh", + "var_name": "mesh", + "attributes": {"notes": "this is a test"}, + "node_dimension": "NodeDim", + "edge_dimension": "EdgeDim", + "edge_coords_and_axes": ( + (self.EDGE_LON, "x"), + (self.EDGE_LAT, "y"), + ), + } + self.mesh = mesh.Mesh(**self.kwargs) + + def test___repr__basic(self): + expected = "" + self.assertEqual(expected, repr(self.mesh)) + + def test___repr__varname(self): + self.mesh.long_name = None + expected = "" + self.assertEqual(expected, repr(self.mesh)) + + def test___repr__noname(self): + self.mesh.long_name = None + self.mesh.var_name = None + expected = "" + self.assertRegex(repr(self.mesh), expected) + + def test___str__noattributes(self): + self.mesh.attributes = None + self.assertNotIn("attributes", str(self.mesh)) + + def test___str__emptyattributes(self): + self.mesh.attributes.clear() + self.assertNotIn("attributes", str(self.mesh)) + + def test__str__longstringattribute(self): + self.mesh.attributes["long_string"] = ( + "long_x_10_long_x_20_long_x_30_long_x_40_" + "long_x_50_long_x_60_long_x_70_long_x_80_" + ) + result = str(self.mesh) + # Note: initial single-quote, but no final one : this is correct ! + expected = ( + "'long_x_10_long_x_20_long_x_30_long_x_40_" + "long_x_50_long_x_60_long_x_70..." + ) + self.assertIn(expected + ":END", result + ":END") + + def test___str__units_stdname(self): + # These are usually missing, but they *can* be present. + mesh_kwargs = self.kwargs.copy() + mesh_kwargs["standard_name"] = "height" # Odd choice ! + mesh_kwargs["units"] = "m" + alt_mesh = mesh.Mesh(**mesh_kwargs) + result = str(alt_mesh) + # We expect these to appear at the end. + expected = "\n".join( + [ + " edge coordinates", + " ", + " ", + " standard_name: 'height'", + " long_name: 'my_topology_mesh'", + " var_name: 'mesh'", + " units: Unit('m')", + " attributes:", + " notes 'this is a test'", + ] + ) + self.assertTrue(result.endswith(expected)) + + class TestOperations1D(TestMeshCommon): # Tests that cannot re-use an existing Mesh instance, instead need a new # one each time. @@ -582,7 +749,7 @@ def setUp(self): @staticmethod def new_connectivity(connectivity, new_len=False): """Provide a new connectivity recognisably different from the original.""" - # NOTE: assumes non-transposed connectivity (src_dim=0). + # NOTE: assumes non-transposed connectivity (location_axis=0). if new_len: shape = (connectivity.shape[0] + 1, connectivity.shape[1]) else: @@ -1175,3 +1342,7 @@ def test_minimum_coords(self): mesh.Mesh, **kwargs, ) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/test_MeshCoord.py b/lib/iris/tests/unit/experimental/ugrid/mesh/test_MeshCoord.py index 740258b77c..ce99a8b4be 100644 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/test_MeshCoord.py +++ b/lib/iris/tests/unit/experimental/ugrid/mesh/test_MeshCoord.py @@ -11,12 +11,13 @@ # importing anything else. import iris.tests as tests # isort:skip +import re import unittest.mock as mock import dask.array as da import numpy as np -from iris._lazy_data import is_lazy_data +from iris._lazy_data import as_lazy_data, is_lazy_data from iris.common.metadata import BaseMetadata from iris.coords import AuxCoord, Coord from iris.cube import Cube @@ -60,7 +61,7 @@ def test_fail_bad_mesh(self): sample_meshcoord(mesh=mock.sentinel.odd) def test_valid_locations(self): - for loc in Mesh.LOCATIONS: + for loc in Mesh.ELEMENTS: meshcoord = sample_meshcoord(location=loc) self.assertEqual(meshcoord.location, loc) @@ -268,42 +269,117 @@ def setUp(self): def _expected_elements_regexp( self, - mesh_strstyle=True, - standard_name=True, - long_name=True, + standard_name="longitude", + long_name="long-name", attributes=True, + location="face", + axis="x", ): - regexp = r"^MeshCoord\(mesh=" - if mesh_strstyle: - regexp += r"Mesh\('test_mesh'\)" - else: - regexp += "" - regexp += r", location='face', axis='x', shape=\(3,\)" + # Printed name is standard or long -- we don't have a case with neither + coord_name = standard_name or long_name + # Construct regexp in 'sections' + # NB each consumes upto first non-space in the next line + regexp = f"MeshCoord : {coord_name} / [^\n]+\n *" + regexp += r"mesh: \\n *" + regexp += f"location: '{location}'\n *" + # Now some optional sections : whichever comes first will match + # arbitrary content leading up to it. + matched_any_upto = False if standard_name: - regexp += ", standard_name='longitude'" - regexp += r", units=Unit\('degrees_east'\)" + regexp += ".*" + matched_any_upto = True + regexp += f"standard_name: '{standard_name}'\n *" if long_name: - regexp += ", long_name='long-name'" + if not matched_any_upto: + regexp += ".*" + matched_any_upto = True + regexp += f"long_name: '{long_name}'\n *" if attributes: - regexp += r", attributes={'a': 1, 'b': 'c'}" - regexp += r"\)$" + # if we expected attributes, they should come next + # TODO: change this when each attribute goes on a new line + if not matched_any_upto: + regexp += ".*" + matched_any_upto = True + # match 'attributes:' followed by N*lines with larger indent + regexp += "attributes:(\n [^ \n]+ +[^ \n]+)+\n " + # After those items, expect 'axis' next + # N.B. this FAILS if we had attributes when we didn't expect them + regexp += f"axis: '{axis}'$" # N.B. this is always the end + + # Compile regexp, also allowing matches across newlines + regexp = re.compile(regexp, flags=re.DOTALL) return regexp def test_repr(self): + # A simple check for the condensed form. + result = repr(self.meshcoord) + expected = ( + "" + ) + self.assertEqual(expected, result) + + def test_repr_lazy(self): + # Displays lazy content (and does not realise!). + self.meshcoord.points = as_lazy_data(self.meshcoord.points) + self.meshcoord.bounds = as_lazy_data(self.meshcoord.bounds) + self.assertTrue(self.meshcoord.has_lazy_points()) + self.assertTrue(self.meshcoord.has_lazy_bounds()) + + result = repr(self.meshcoord) + self.assertTrue(self.meshcoord.has_lazy_points()) + self.assertTrue(self.meshcoord.has_lazy_bounds()) + + expected = ( + "+bounds shape(3,)>" + ) + self.assertEqual(expected, result) + + def test_repr__nameless_mesh(self): + # Check what it does when the Mesh doesn't have a name. + self.mesh.long_name = None + assert self.mesh.name() == "unknown" result = repr(self.meshcoord) - re_expected = self._expected_elements_regexp(mesh_strstyle=False) + re_expected = ( + r".MeshCoord: longitude / \(degrees_east\) " + r"mesh\(.Mesh object at 0x[^>]+.\) location\(face\) " + ) self.assertRegex(result, re_expected) def test__str__(self): + # Basic output contains mesh, location, standard_name, long_name, + # attributes, mesh, location and axis + result = str(self.meshcoord) + re_expected = self._expected_elements_regexp() + self.assertRegex(result, re_expected) + + def test__str__lazy(self): + # Displays lazy content (and does not realise!). + self.meshcoord.points = as_lazy_data(self.meshcoord.points) + self.meshcoord.bounds = as_lazy_data(self.meshcoord.bounds) + result = str(self.meshcoord) - re_expected = self._expected_elements_regexp(mesh_strstyle=True) + self.assertTrue(self.meshcoord.has_lazy_points()) + self.assertTrue(self.meshcoord.has_lazy_bounds()) + + self.assertIn("points: ", result) + self.assertIn("bounds: ", result) + re_expected = self._expected_elements_regexp() self.assertRegex(result, re_expected) def test_alternative_location_and_axis(self): meshcoord = sample_meshcoord(mesh=self.mesh, location="edge", axis="y") result = str(meshcoord) - re_expected = r", location='edge', axis='y'" + re_expected = self._expected_elements_regexp( + standard_name="latitude", + long_name=None, + location="edge", + axis="y", + attributes=None, + ) self.assertRegex(result, re_expected) + # Basic output contains standard_name, long_name, attributes def test_str_no_long_name(self): mesh = self.mesh @@ -461,12 +537,12 @@ def _make_test_meshcoord( lazy_sources=False, location="face", inds_start_index=0, - inds_src_dim=0, + inds_location_axis=0, facenodes_changes=None, ): # Construct a miniature face-nodes mesh for testing. # NOTE: we will make our connectivity arrays with standard - # start_index=0 and src_dim=0 : We only adjust that (if required) when + # start_index=0 and location_axis=0 : We only adjust that (if required) when # creating the actual connectivities. face_nodes_array = np.array( [ @@ -551,26 +627,26 @@ def lazify(arr): inds_start_index + ( face_nodes_array.transpose() - if inds_src_dim == 1 + if inds_location_axis == 1 else face_nodes_array ), cf_role="face_node_connectivity", long_name="face_nodes", start_index=inds_start_index, - src_dim=inds_src_dim, + location_axis=inds_location_axis, ) edge_node_conn = Connectivity( inds_start_index + ( edge_nodes_array.transpose() - if inds_src_dim == 1 + if inds_location_axis == 1 else edge_nodes_array ), cf_role="edge_node_connectivity", long_name="edge_nodes", start_index=inds_start_index, - src_dim=inds_src_dim, + location_axis=inds_location_axis, ) self.mesh = Mesh( @@ -654,9 +730,9 @@ def test_edge_bounds(self): # NB simpler than faces : no possibility of missing points self.assertArrayAlmostEqual(result, expected) - def test_bounds_connectivity__src_dim_1(self): + def test_bounds_connectivity__location_axis_1(self): # Test with a transposed indices array. - self._make_test_meshcoord(inds_src_dim=1) + self._make_test_meshcoord(inds_location_axis=1) self._check_expected_bounds_values() def test_bounds_connectivity__start_index_1(self): diff --git a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh__from_coords.py b/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh__from_coords.py index aee5018e5b..edd34f94a1 100644 --- a/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh__from_coords.py +++ b/lib/iris/tests/unit/experimental/ugrid/mesh/test_Mesh__from_coords.py @@ -217,7 +217,7 @@ def test_mixed_shapes(self): mesh = self.create() self.assertArrayEqual( - mesh.face_node_connectivity.src_lengths(), [4, 4, 3] + mesh.face_node_connectivity.location_lengths(), [4, 4, 3] ) self.assertEqual(mesh.node_coords.node_x.points[-1], 0.0) self.assertEqual(mesh.node_coords.node_y.points[-1], 0.0) diff --git a/lib/iris/tests/unit/experimental/ugrid/metadata/test_ConnectivityMetadata.py b/lib/iris/tests/unit/experimental/ugrid/metadata/test_ConnectivityMetadata.py index f119f53729..af92e69b08 100644 --- a/lib/iris/tests/unit/experimental/ugrid/metadata/test_ConnectivityMetadata.py +++ b/lib/iris/tests/unit/experimental/ugrid/metadata/test_ConnectivityMetadata.py @@ -29,7 +29,7 @@ def setUp(self): self.attributes = mock.sentinel.attributes self.cf_role = mock.sentinel.cf_role self.start_index = mock.sentinel.start_index - self.src_dim = mock.sentinel.src_dim + self.location_axis = mock.sentinel.location_axis self.cls = ConnectivityMetadata def test_repr(self): @@ -41,12 +41,12 @@ def test_repr(self): attributes=self.attributes, cf_role=self.cf_role, start_index=self.start_index, - src_dim=self.src_dim, + location_axis=self.location_axis, ) fmt = ( "ConnectivityMetadata(standard_name={!r}, long_name={!r}, " "var_name={!r}, units={!r}, attributes={!r}, cf_role={!r}, " - "start_index={!r}, src_dim={!r})" + "start_index={!r}, location_axis={!r})" ) expected = fmt.format( self.standard_name, @@ -56,7 +56,7 @@ def test_repr(self): self.attributes, self.cf_role, self.start_index, - self.src_dim, + self.location_axis, ) self.assertEqual(expected, repr(metadata)) @@ -69,7 +69,7 @@ def test__fields(self): "attributes", "cf_role", "start_index", - "src_dim", + "location_axis", ) self.assertEqual(self.cls._fields, expected) @@ -87,14 +87,14 @@ def setUp(self): attributes=sentinel.attributes, cf_role=sentinel.cf_role, start_index=sentinel.start_index, - src_dim=sentinel.src_dim, + location_axis=sentinel.location_axis, ) self.dummy = sentinel.dummy self.cls = ConnectivityMetadata - # The "src_dim" member is stateful only, and does not participate in + # The "location_axis" member is stateful only, and does not participate in # lenient/strict equivalence. - self.members_no_src_dim = filter( - lambda member: member != "src_dim", self.cls._members + self.members_no_location_axis = filter( + lambda member: member != "location_axis", self.cls._members ) def test_wraps_docstring(self): @@ -140,7 +140,7 @@ def test_op_lenient_same_none(self): self.assertTrue(rmetadata.__eq__(lmetadata)) def test_op_lenient_same_members_none(self): - for member in self.members_no_src_dim: + for member in self.members_no_location_axis: lmetadata = self.cls(**self.values) right = self.values.copy() right[member] = None @@ -152,10 +152,10 @@ def test_op_lenient_same_members_none(self): self.assertFalse(lmetadata.__eq__(rmetadata)) self.assertFalse(rmetadata.__eq__(lmetadata)) - def test_op_lenient_same_src_dim_none(self): + def test_op_lenient_same_location_axis_none(self): lmetadata = self.cls(**self.values) right = self.values.copy() - right["src_dim"] = None + right["location_axis"] = None rmetadata = self.cls(**right) with mock.patch("iris.common.metadata._LENIENT", return_value=True): @@ -173,7 +173,7 @@ def test_op_lenient_different(self): self.assertFalse(rmetadata.__eq__(lmetadata)) def test_op_lenient_different_members(self): - for member in self.members_no_src_dim: + for member in self.members_no_location_axis: lmetadata = self.cls(**self.values) right = self.values.copy() right[member] = self.dummy @@ -185,10 +185,10 @@ def test_op_lenient_different_members(self): self.assertFalse(lmetadata.__eq__(rmetadata)) self.assertFalse(rmetadata.__eq__(lmetadata)) - def test_op_lenient_different_src_dim(self): + def test_op_lenient_different_location_axis(self): lmetadata = self.cls(**self.values) right = self.values.copy() - right["src_dim"] = self.dummy + right["location_axis"] = self.dummy rmetadata = self.cls(**right) with mock.patch("iris.common.metadata._LENIENT", return_value=True): @@ -214,7 +214,7 @@ def test_op_strict_different(self): self.assertFalse(rmetadata.__eq__(lmetadata)) def test_op_strict_different_members(self): - for member in self.members_no_src_dim: + for member in self.members_no_location_axis: lmetadata = self.cls(**self.values) right = self.values.copy() right[member] = self.dummy @@ -226,10 +226,10 @@ def test_op_strict_different_members(self): self.assertFalse(lmetadata.__eq__(rmetadata)) self.assertFalse(rmetadata.__eq__(lmetadata)) - def test_op_strict_different_src_dim(self): + def test_op_strict_different_location_axis(self): lmetadata = self.cls(**self.values) right = self.values.copy() - right["src_dim"] = self.dummy + right["location_axis"] = self.dummy rmetadata = self.cls(**right) with mock.patch("iris.common.metadata._LENIENT", return_value=False): @@ -247,7 +247,7 @@ def test_op_strict_different_none(self): self.assertFalse(rmetadata.__eq__(lmetadata)) def test_op_strict_different_members_none(self): - for member in self.members_no_src_dim: + for member in self.members_no_location_axis: lmetadata = self.cls(**self.values) right = self.values.copy() right[member] = None @@ -259,10 +259,10 @@ def test_op_strict_different_members_none(self): self.assertFalse(lmetadata.__eq__(rmetadata)) self.assertFalse(rmetadata.__eq__(lmetadata)) - def test_op_strict_different_src_dim_none(self): + def test_op_strict_different_location_axis_none(self): lmetadata = self.cls(**self.values) right = self.values.copy() - right["src_dim"] = None + right["location_axis"] = None rmetadata = self.cls(**right) with mock.patch("iris.common.metadata._LENIENT", return_value=False): @@ -311,7 +311,7 @@ def setUp(self): attributes=sentinel.attributes, cf_role=sentinel.cf_role, start_index=sentinel.start_index, - src_dim=sentinel.src_dim, + location_axis=sentinel.location_axis, ) self.dummy = sentinel.dummy self.cls = ConnectivityMetadata @@ -508,7 +508,7 @@ def setUp(self): attributes=sentinel.attributes, cf_role=sentinel.cf_role, start_index=sentinel.start_index, - src_dim=sentinel.src_dim, + location_axis=sentinel.location_axis, ) self.dummy = sentinel.dummy self.cls = ConnectivityMetadata diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py index 717e5b5c41..0d3ed932e8 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py @@ -6,9 +6,6 @@ """ Unit tests for the module :mod:`iris.fileformats._nc_load_rules.actions`. -This module provides the engine.activate() call used in the function -`iris.fileformats.netcdf._load_cube`. - """ from pathlib import Path import shutil diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__hybrid_formulae.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__hybrid_formulae.py index ca92926542..3413090a3d 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__hybrid_formulae.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__hybrid_formulae.py @@ -238,44 +238,75 @@ def test_two_formulae(self): formula_terms=["a", "b", "depth", "eta", "orog", "sigma"], ) + def test_atmosphere_sigma_coordinate(self): + hybrid_type = "atmosphere_sigma_coordinate" + term_names = hh.CF_COORD_VERTICAL[hybrid_type] + result = self.run_testcase( + formula_root_name=hybrid_type, term_names=term_names + ) + self.check_result( + result, factory_type=hybrid_type, formula_terms=term_names + ) -# Add in tests methods to exercise each (supported) vertical coordinate type -# individually. -# NOTE: hh.CF_COORD_VERTICAL lists all the valid types, but we don't yet -# support all of them. -_SUPPORTED_FORMULA_TYPES = ( - # NOTE: omit "atmosphere_hybrid_height_coordinate" : our basic testcase - "atmosphere_sigma_coordinate", - "atmosphere_hybrid_sigma_pressure_coordinate", - "ocean_sigma_z_coordinate", - "ocean_sigma_coordinate", - "ocean_s_coordinate", - "ocean_s_coordinate_g1", - "ocean_s_coordinate_g2", -) -for hybrid_type in _SUPPORTED_FORMULA_TYPES: + def test_atmosphere_hybrid_sigma_pressure_coordinate(self): + hybrid_type = "atmosphere_hybrid_sigma_pressure_coordinate" + term_names = hh.CF_COORD_VERTICAL[hybrid_type] + result = self.run_testcase( + formula_root_name=hybrid_type, term_names=term_names + ) + self.check_result( + result, factory_type=hybrid_type, formula_terms=term_names + ) + + def test_ocean_sigma_z_coordinate(self): + hybrid_type = "ocean_sigma_z_coordinate" + term_names = hh.CF_COORD_VERTICAL[hybrid_type] + result = self.run_testcase( + formula_root_name=hybrid_type, term_names=term_names + ) + self.check_result( + result, factory_type=hybrid_type, formula_terms=term_names + ) + + def test_ocean_sigma_coordinate(self): + hybrid_type = "ocean_sigma_coordinate" + term_names = hh.CF_COORD_VERTICAL[hybrid_type] + result = self.run_testcase( + formula_root_name=hybrid_type, term_names=term_names + ) + self.check_result( + result, factory_type=hybrid_type, formula_terms=term_names + ) - def construct_inner_func(hybrid_type): + def test_ocean_s_coordinate(self): + hybrid_type = "ocean_s_coordinate" term_names = hh.CF_COORD_VERTICAL[hybrid_type] + result = self.run_testcase( + formula_root_name=hybrid_type, term_names=term_names + ) + self.check_result( + result, factory_type=hybrid_type, formula_terms=term_names + ) - def inner(self): - result = self.run_testcase( - formula_root_name=hybrid_type, term_names=term_names - ) - self.check_result( - result, factory_type=hybrid_type, formula_terms=term_names - ) - - return inner - - # Note: use an intermediate function to generate each test method, simply to - # generate a new local variable for 'hybrid_type' on each iteration. - # Otherwise all the test methods will refer to the *same* 'hybrid_type' - # variable, i.e. the loop variable, which does not work ! - method_name = f"test_{hybrid_type}_coord" - setattr( - Test__formulae_tests, method_name, construct_inner_func(hybrid_type) - ) + def test_ocean_s_coordinate_g1(self): + hybrid_type = "ocean_s_coordinate_g1" + term_names = hh.CF_COORD_VERTICAL[hybrid_type] + result = self.run_testcase( + formula_root_name=hybrid_type, term_names=term_names + ) + self.check_result( + result, factory_type=hybrid_type, formula_terms=term_names + ) + + def test_ocean_s_coordinate_g2(self): + hybrid_type = "ocean_s_coordinate_g2" + term_names = hh.CF_COORD_VERTICAL[hybrid_type] + result = self.run_testcase( + formula_root_name=hybrid_type, term_names=term_names + ) + self.check_result( + result, factory_type=hybrid_type, formula_terms=term_names + ) if __name__ == "__main__": diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__latlon_dimcoords.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__latlon_dimcoords.py new file mode 100644 index 0000000000..dfa862c4d1 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__latlon_dimcoords.py @@ -0,0 +1,337 @@ +# 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 engine.activate() call within the +`iris.fileformats.netcdf._load_cube` function. + +Tests for rules behaviour in identifying latitude/longitude dim-coords, both +rotated and non-rotated. + +""" +import iris.tests as tests # isort: skip + +from iris.coord_systems import GeogCS, RotatedGeogCS +from iris.tests.unit.fileformats.nc_load_rules.actions import ( + Mixin__nc_load_actions, +) + + +class Mixin_latlon_dimcoords(Mixin__nc_load_actions): + # Tests for the recognition and construction of latitude/longitude coords. + + # Control to test either longitude or latitude coords. + # Set by inheritor classes, which are actual TestCases. + lat_1_or_lon_0 = None + + def setUp(self): + super().setUp() + # Generate some useful settings : just to generalise operation over + # both latitude and longitude. + islat = self.lat_1_or_lon_0 + assert islat in (0, 1) + self.unrotated_name = "latitude" if islat else "longitude" + self.rotated_name = "grid_latitude" if islat else "grid_longitude" + self.unrotated_units = "degrees_north" if islat else "degrees_east" + # Note: there are many alternative valid forms for the rotated units, + # but we are not testing that here. + self.rotated_units = "degrees" # NB this one is actually constant + self.axis = "y" if islat else "x" + + def _make_testcase_cdl( + self, + standard_name=None, + long_name=None, + var_name=None, + units=None, + axis=None, + grid_mapping=None, + ): + # Inner routine called by 'run_testcase' (in Mixin__nc_load_actions), + # to generate CDL which is then translated into a testfile and loaded. + if var_name is None: + # Can't have *no* var-name + # N.B. it is also the name of the dimension. + var_name = "dim" + + def attribute_str(name, value): + if value is None or value == "": + result = "" + else: + result = f'{var_name}:{name} = "{value}" ;' + + return result + + standard_name_str = attribute_str("standard_name", standard_name) + long_name_str = attribute_str("long_name", long_name) + units_str = attribute_str("units", units) + axis_str = attribute_str("axis", axis) + if grid_mapping: + grid_mapping_str = 'phenom:grid_mapping = "crs" ;' + else: + grid_mapping_str = "" + + assert grid_mapping in (None, "latlon", "rotated") + if grid_mapping is None: + crs_str = "" + elif grid_mapping == "latlon": + crs_str = """ + int crs ; + crs:grid_mapping_name = "latitude_longitude" ; + crs:semi_major_axis = 6371000.0 ; + crs:inverse_flattening = 1000. ; +""" + elif grid_mapping == "rotated": + crs_str = """ + int crs ; + crs:grid_mapping_name = "rotated_latitude_longitude" ; + crs:grid_north_pole_latitude = 32.5 ; + crs:grid_north_pole_longitude = 170. ; +""" + + cdl_string = f""" +netcdf test {{ + dimensions: + {var_name} = 2 ; + variables: + double {var_name}({var_name}) ; + {standard_name_str} + {units_str} + {long_name_str} + {axis_str} + double phenom({var_name}) ; + phenom:standard_name = "air_temperature" ; + phenom:units = "K" ; + {grid_mapping_str} + {crs_str} + data: + {var_name} = 0., 1. ; +}} +""" + return cdl_string + + def check_result( + self, + cube, + standard_name, + long_name, + units, + crs=None, + context_message="", + ): + # Check the existence, standard-name, long-name, units and coord-system + # of the resulting coord. Also that it is always a dim-coord. + # NOTE: there is no "axis" arg, as this information does *not* appear + # as a separate property (or attribute) of the resulting coord. + # However, whether the file variable has an axis attribute *does* + # affect the results here, in some cases. + coords = cube.coords() + # There should be one and only one coord. + self.assertEqual(1, len(coords)) + # It should also be a dim-coord + self.assertEqual(1, len(cube.coords(dim_coords=True))) + (coord,) = coords + if self.debug: + print("") + print("DEBUG : result coord =", coord) + print("") + + coord_stdname, coord_longname, coord_units, coord_crs = [ + getattr(coord, name) + for name in ("standard_name", "long_name", "units", "coord_system") + ] + self.assertEqual(standard_name, coord_stdname, context_message) + self.assertEqual(long_name, coord_longname, context_message) + self.assertEqual(units, coord_units, context_message) + assert crs in (None, "latlon", "rotated") + if crs is None: + self.assertEqual(None, coord_crs, context_message) + elif crs == "latlon": + self.assertIsInstance(coord_crs, GeogCS, context_message) + elif crs == "rotated": + self.assertIsInstance(coord_crs, RotatedGeogCS, context_message) + + # + # Testcase routines + # + # NOTE: all these testcases have been verified against the older behaviour + # in v3.0.4, based on Pyke rules. + # + + def test_minimal(self): + # Nothing but a var-name --> unrecognised dim-coord. + result = self.run_testcase() + self.check_result(result, None, None, "unknown") + + def test_fullinfo_unrotated(self): + # Check behaviour with all normal info elements for 'unrotated' case. + # Includes a grid-mapping, but no axis (should not be needed). + result = self.run_testcase( + standard_name=self.unrotated_name, + units=self.unrotated_units, + grid_mapping="latlon", + ) + self.check_result( + result, self.unrotated_name, None, "degrees", "latlon" + ) + + def test_fullinfo_rotated(self): + # Check behaviour with all normal info elements for 'rotated' case. + # Includes a grid-mapping, but no axis (should not be needed). + result = self.run_testcase( + standard_name=self.rotated_name, + units=self.rotated_units, + grid_mapping="rotated", + ) + self.check_result( + result, self.rotated_name, None, "degrees", "rotated" + ) + + def test_axis(self): + # A suitable axis --> unrotated lat/lon coord, but unknown units. + result = self.run_testcase(axis=self.axis) + self.check_result(result, self.unrotated_name, None, "unknown") + + def test_units_unrotated(self): + # With a unit like 'degrees_east', we automatically identify this as a + # latlon coord, *and* convert units to plain 'degrees' on loading. + result = self.run_testcase(units=self.unrotated_units) + self.check_result(result, self.unrotated_name, None, "degrees") + + def test_units_rotated(self): + # With no info except a "degrees" unit, we **don't** identify a latlon, + # i.e. we do not set the standard-name + result = self.run_testcase(units="degrees") + self.check_result(result, None, None, "degrees") + + def test_units_unrotated_gridmapping(self): + # With an unrotated unit *AND* a suitable grid-mapping, we identify a + # rotated latlon coordinate + assign it the coord-system. + result = self.run_testcase( + units=self.unrotated_units, grid_mapping="latlon" + ) + self.check_result( + result, self.unrotated_name, None, "degrees", "latlon" + ) + + def test_units_rotated_gridmapping_noname(self): + # Rotated units and grid-mapping, but *without* the expected name. + # Does not translate, no coord-system (i.e. grid-mapping is discarded). + result = self.run_testcase( + units="degrees", + grid_mapping="rotated", + ) + self.check_result(result, None, None, "degrees", None) + + def test_units_rotated_gridmapping_withname(self): + # With a "degrees" unit, a rotated grid-mapping *AND* a suitable + # standard-name, it recognises a rotated dimcoord. + result = self.run_testcase( + standard_name=self.rotated_name, + units="degrees", + grid_mapping="rotated", + ) + self.check_result( + result, self.rotated_name, None, "degrees", "rotated" + ) + + def test_units_rotated_gridmapping_varname(self): + # Same but with var-name containing the standard-name : in this case we + # get NO COORDINATE-SYSTEM (which is a bit weird). + result = self.run_testcase( + var_name=self.rotated_name, + units="degrees", + grid_mapping="rotated", + ) + self.check_result(result, self.rotated_name, None, "degrees", None) + + def test_varname_unrotated(self): + # With a recognised name in the var-name, we set standard-name. + # But units are left undetermined. + result = self.run_testcase(var_name=self.unrotated_name) + self.check_result(result, self.unrotated_name, None, "unknown") + + def test_varname_rotated(self): + # With a *rotated* name in the var-name, we set standard-name. + # But units are left undetermined. + result = self.run_testcase(var_name=self.rotated_name) + self.check_result(result, self.rotated_name, None, "unknown") + + def test_varname_unrotated_units_rotated(self): + # With a "degrees" unit and a suitable var-name, we do identify + # (= set standard-name). + # N.B. this accepts "degrees" as a generic term, and so does *not* + # interpret it as a rotated coordinate. + result = self.run_testcase( + var_name=self.unrotated_name, units="degrees" + ) + self.check_result(result, self.unrotated_name, None, "degrees") + + def test_longname(self): + # A recognised form in long-name is *not* translated into standard-name. + result = self.run_testcase(long_name=self.unrotated_name) + self.check_result(result, None, self.unrotated_name, "unknown") + + def test_stdname_unrotated(self): + # Only an (unrotated) standard name : units is not specified + result = self.run_testcase(standard_name=self.unrotated_name) + self.check_result(result, self.unrotated_name, None, None) + + def test_stdname_rotated(self): + # Only a (rotated) standard name : units is not specified + result = self.run_testcase(standard_name=self.rotated_name) + self.check_result(result, self.rotated_name, None, None) + + def test_stdname_unrotated_gridmapping(self): + # An unrotated standard-name and grid-mapping, translates into a + # coordinate system. + result = self.run_testcase( + standard_name=self.unrotated_name, grid_mapping="latlon" + ) + self.check_result( + result, self.unrotated_name, None, "unknown", "latlon" + ) + + def test_stdname_rotated_gridmapping(self): + # An *rotated* standard-name and grid-mapping, translates into a + # coordinate system. + result = self.run_testcase( + standard_name=self.rotated_name, grid_mapping="rotated" + ) + self.check_result(result, self.rotated_name, None, None, "rotated") + + +class Test__longitude_coords(Mixin_latlon_dimcoords, tests.IrisTest): + lat_1_or_lon_0 = 0 + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + + +class Test__latitude_coords(Mixin_latlon_dimcoords, tests.IrisTest): + lat_1_or_lon_0 = 1 + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/fileformats/netcdf/test_Saver__ugrid.py b/lib/iris/tests/unit/fileformats/netcdf/test_Saver__ugrid.py index 87c2df7d45..a914dd3314 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test_Saver__ugrid.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test_Saver__ugrid.py @@ -184,7 +184,7 @@ def mesh_location_size(mesh, location): if conn is None: result = 0 else: - result = conn.shape[conn.src_dim] + result = conn.shape[conn.location_axis] return result @@ -705,7 +705,7 @@ def test_connectivity_dim_order(self): # Get the face-node and edge-node connectivities face_nodes_conn = mesh.face_node_connectivity edge_nodes_conn = mesh.edge_node_connectivity - # Transpose them : N.B. this sets src_dim=1, as it should be. + # Transpose them : N.B. this sets location_axis=1, as it should be. nodesfirst_faces_conn = face_nodes_conn.transpose() nodesfirst_edges_conn = edge_nodes_conn.transpose() # Make a new mesh with both face and edge connectivities 'transposed'. diff --git a/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py b/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py index 0f2a8a2d4b..c9c4821e0a 100644 --- a/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py +++ b/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py @@ -65,7 +65,7 @@ def test_3d(self): def test_multiple_odd_dims(self): # Test to ensure multiple collapsed dimensions don't interfere. # make a 5-D array where dimensions 0, 2 and 3 are degenerate. - array = np.arange(3 ** 5).reshape([3] * 5) + array = np.arange(3**5).reshape([3] * 5) array[1:] = array[0:1] array[:, :, 1:] = array[:, :, 0:1] array[:, :, :, 1:] = array[:, :, :, 0:1] diff --git a/lib/iris/tests/unit/io/test__generate_cubes.py b/lib/iris/tests/unit/io/test__generate_cubes.py new file mode 100755 index 0000000000..3a896a111c --- /dev/null +++ b/lib/iris/tests/unit/io/test__generate_cubes.py @@ -0,0 +1,37 @@ +# 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.io._generate_cubes` function.""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests # isort:skip + +from pathlib import Path + +import iris + + +class TestGenerateCubes(tests.IrisTest): + def test_pathlib_paths(self): + test_variants = [ + ("string", "string"), + (["string"], "string"), + (Path("string"), Path("string")), + ] + + decode_uri_mock = self.patch( + "iris.iris.io.decode_uri", return_value=("file", None) + ) + self.patch("iris.iris.io.load_files") + + for gc_arg, du_arg in test_variants: + decode_uri_mock.reset_mock() + list(iris._generate_cubes(gc_arg, None, None)) + decode_uri_mock.assert_called_with(du_arg) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/io/test_save.py b/lib/iris/tests/unit/io/test_save.py new file mode 100755 index 0000000000..b92e26f2d1 --- /dev/null +++ b/lib/iris/tests/unit/io/test_save.py @@ -0,0 +1,45 @@ +# 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.io.save` function.""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests # isort:skip + +from pathlib import Path +from unittest import mock + +import iris +from iris.cube import Cube + + +class TestSave(tests.IrisTest): + def test_pathlib_save(self): + file_mock = mock.Mock() + # Have to configure after creation because "name" is special + file_mock.configure_mock(name="string") + + find_saver_mock = self.patch( + "iris.io.find_saver", return_value=(lambda *args, **kwargs: None) + ) + + test_variants = [ + ("string", "string"), + (Path("string/string"), "string/string"), + (file_mock, "string"), + ] + + for target, fs_val in test_variants: + try: + iris.save(Cube([]), target) + except ValueError: + print("ValueError") + pass + find_saver_mock.assert_called_with(fs_val) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py b/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py index 8370c719f0..40a932b9e0 100644 --- a/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py +++ b/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py @@ -142,7 +142,7 @@ def test_columns_long_attribute(self): " Attributes:", ( " very_very_very_very_very_long_name " - "longish string extends beyond dim columns" + "'longish string extends beyond dim columns'" ), ] self.assertEqual(rep, expected) @@ -442,7 +442,7 @@ def test_section_cube_attributes(self): " Attributes:", " list [3]", " number 1.2", - " string four five in a string", + " string 'four five in a string'", " z_tupular (6, (7, 8))", ] self.assertEqual(rep, expected) @@ -464,7 +464,7 @@ def test_section_cube_attributes__string_extras(self): " Attributes:", " escaped 'escaped\\tstring'", ( - " long this is very very very " + " long 'this is very very very " "very very very very very very very very very very..." ), ( diff --git a/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py b/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py index c8af3437e6..3e411c020d 100644 --- a/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py +++ b/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py @@ -183,7 +183,11 @@ def test_attributes(self): attribute_section = rep.scalar_sections["Attributes:"] attribute_contents = attribute_section.contents - expected_contents = ["a: 1", "b: two", "c: ' this \\n that\\tand.'"] + expected_contents = [ + "a: 1", + "b: 'two'", + "c: ' this \\n that\\tand.'", + ] # Note: a string with \n or \t in it gets "repr-d". # Other strings don't (though in coord 'extra' lines, they do.) diff --git a/lib/iris/util.py b/lib/iris/util.py index 9ab413a493..53cd78724e 100644 --- a/lib/iris/util.py +++ b/lib/iris/util.py @@ -10,8 +10,6 @@ from abc import ABCMeta, abstractmethod from collections.abc import Hashable, Iterable -from contextlib import contextmanager -import copy import functools import inspect import os @@ -396,10 +394,24 @@ def normalise_array(array): def approx_equal(a, b, max_absolute_error=1e-10, max_relative_error=1e-10): """ - Returns whether two numbers are almost equal, allowing for the - finite precision of floating point numbers. + Returns whether two numbers are almost equal, allowing for the finite + precision of floating point numbers. + + .. deprecated:: 3.2.0 + + Instead use :func:`math.isclose`. For example, rather than calling + ``approx_equal(a, b, max_abs, max_rel)`` replace with ``math.isclose(a, + b, max_rel, max_abs)``. Note that :func:`~math.isclose` will return True + if the actual error equals the maximum, whereas :func:`util.approx_equal` + will return False. """ + wmsg = ( + "iris.util.approx_equal has been deprecated and will be removed, " + "please use math.isclose instead." + ) + warn_deprecated(wmsg) + # Deal with numbers close to zero if abs(a - b) < max_absolute_error: return True @@ -1054,18 +1066,20 @@ def format_array(arr): """ - summary_insert = "" summary_threshold = 85 + summary_insert = "..." if arr.size > summary_threshold else "" edge_items = 3 ffunc = str - formatArray = np.core.arrayprint._formatArray max_line_len = 50 - legacy = "1.13" - if arr.size > summary_threshold: - summary_insert = "..." - options = np.get_printoptions() - options["legacy"] = legacy - with _printopts_context(**options): + + # Format the array with version 1.13 legacy behaviour + with np.printoptions(legacy="1.13"): + # Use this (private) routine for more control. + formatArray = np.core.arrayprint._formatArray + # N.B. the 'legacy' arg had different forms in different numpy versions + # -- fetch the required form from the internal options dict + format_options_legacy = np.core.arrayprint._format_options["legacy"] + result = formatArray( arr, ffunc, @@ -1074,29 +1088,12 @@ def format_array(arr): separator=", ", edge_items=edge_items, summary_insert=summary_insert, - legacy=legacy, + legacy=format_options_legacy, ) return result -@contextmanager -def _printopts_context(**kwargs): - """ - Update the numpy printoptions for the life of this context manager. - - Note: this function can be removed with numpy>=1.15 thanks to - https://github.com/numpy/numpy/pull/10406 - - """ - original_opts = np.get_printoptions() - np.set_printoptions(**kwargs) - try: - yield - finally: - np.set_printoptions(**original_opts) - - def new_axis(src_cube, scalar_coord=None): """ Create a new axis as the leading dimension of the cube, promoting a scalar @@ -1169,133 +1166,6 @@ def new_axis(src_cube, scalar_coord=None): return new_cube -def as_compatible_shape(src_cube, target_cube): - """ - Return a cube with added length one dimensions to match the dimensionality - and dimension ordering of `target_cube`. - - This function can be used to add the dimensions that have been collapsed, - aggregated or sliced out, promoting scalar coordinates to length one - dimension coordinates where necessary. It operates by matching coordinate - metadata to infer the dimensions that need modifying, so the provided - cubes must have coordinates with the same metadata - (see :class:`iris.common.CoordMetadata`). - - .. note:: This function will load and copy the data payload of `src_cube`. - - .. deprecated:: 3.0.0 - - Instead use :class:`~iris.common.resolve.Resolve`. For example, rather - than calling ``as_compatible_shape(src_cube, target_cube)`` replace - with ``Resolve(src_cube, target_cube)(target_cube.core_data())``. - - Args: - - * src_cube: - An instance of :class:`iris.cube.Cube` with missing dimensions. - - * target_cube: - An instance of :class:`iris.cube.Cube` with the desired dimensionality. - - Returns: - A instance of :class:`iris.cube.Cube` with the same dimensionality as - `target_cube` but with the data and coordinates from `src_cube` - suitably reshaped to fit. - - """ - from iris.cube import Cube - - wmsg = ( - "iris.util.as_compatible_shape has been deprecated and will be " - "removed, please use iris.common.resolve.Resolve instead." - ) - warn_deprecated(wmsg) - - dim_mapping = {} - for coord in target_cube.aux_coords + target_cube.dim_coords: - dims = target_cube.coord_dims(coord) - try: - collapsed_dims = src_cube.coord_dims(coord) - except iris.exceptions.CoordinateNotFoundError: - continue - if collapsed_dims: - if len(collapsed_dims) == len(dims): - for dim_from, dim_to in zip(dims, collapsed_dims): - dim_mapping[dim_from] = dim_to - elif dims: - for dim_from in dims: - dim_mapping[dim_from] = None - - if len(dim_mapping) != target_cube.ndim: - raise ValueError( - "Insufficient or conflicting coordinate " - "metadata. Cannot infer dimension mapping " - "to restore cube dimensions." - ) - - new_shape = [1] * target_cube.ndim - for dim_from, dim_to in dim_mapping.items(): - if dim_to is not None: - new_shape[dim_from] = src_cube.shape[dim_to] - - new_data = src_cube.data.copy() - - # Transpose the data (if necessary) to prevent assignment of - # new_shape doing anything except adding length one dims. - order = [v for k, v in sorted(dim_mapping.items()) if v is not None] - if order != sorted(order): - new_order = [order.index(i) for i in range(len(order))] - new_data = np.transpose(new_data, new_order).copy() - - new_cube = Cube(new_data.reshape(new_shape)) - new_cube.metadata = copy.deepcopy(src_cube.metadata) - - # Record a mapping from old coordinate IDs to new coordinates, - # for subsequent use in creating updated aux_factories. - coord_mapping = {} - - reverse_mapping = {v: k for k, v in dim_mapping.items() if v is not None} - - def add_coord(coord): - """Closure used to add a suitably reshaped coord to new_cube.""" - all_dims = target_cube.coord_dims(coord) - src_dims = [ - dim - for dim in src_cube.coord_dims(coord) - if src_cube.shape[dim] > 1 - ] - mapped_dims = [reverse_mapping[dim] for dim in src_dims] - length1_dims = [dim for dim in all_dims if new_cube.shape[dim] == 1] - dims = length1_dims + mapped_dims - shape = [new_cube.shape[dim] for dim in dims] - if not shape: - shape = [1] - points = coord.points.reshape(shape) - bounds = None - if coord.has_bounds(): - bounds = coord.bounds.reshape(shape + [coord.nbounds]) - new_coord = coord.copy(points=points, bounds=bounds) - # If originally in dim_coords, add to dim_coords, otherwise add to - # aux_coords. - if target_cube.coords(coord, dim_coords=True): - try: - new_cube.add_dim_coord(new_coord, dims) - except ValueError: - # Catch cases where the coord is an AuxCoord and therefore - # cannot be added to dim_coords. - new_cube.add_aux_coord(new_coord, dims) - else: - new_cube.add_aux_coord(new_coord, dims) - coord_mapping[id(coord)] = new_coord - - for coord in src_cube.aux_coords + src_cube.dim_coords: - add_coord(coord) - for factory in src_cube.aux_factories: - new_cube.add_aux_factory(factory.updated(coord_mapping)) - - return new_cube - - def squeeze(cube): """ Removes any dimension of length one. If it has an associated DimCoord or diff --git a/noxfile.py b/noxfile.py index e7b7e43f1b..820959060a 100755 --- a/noxfile.py +++ b/noxfile.py @@ -16,7 +16,7 @@ nox.options.reuse_existing_virtualenvs = True #: Python versions we can run sessions under -_PY_VERSIONS_ALL = ["3.7", "3.8"] +_PY_VERSIONS_ALL = ["3.8"] _PY_VERSION_LATEST = _PY_VERSIONS_ALL[-1] #: One specific python version for docs builds @@ -28,6 +28,13 @@ #: Default cartopy cache directory. CARTOPY_CACHE_DIR = os.environ.get("HOME") / Path(".local/share/cartopy") +# https://github.com/numpy/numpy/pull/19478 +# https://github.com/matplotlib/matplotlib/pull/22099 +#: Common session environment variables. +ENV = dict( + NPY_DISABLE_CPU_FEATURES="AVX512F,AVX512CD,AVX512VL,AVX512BW,AVX512DQ,AVX512_SKX" +) + def session_lockfile(session: nox.sessions.Session) -> Path: """Return the path of the session lockfile.""" @@ -210,6 +217,7 @@ def tests(session: nox.sessions.Session): """ prepare_venv(session) session.install("--no-deps", "--editable", ".") + session.env.update(ENV) session.run( "python", "-m", @@ -232,6 +240,7 @@ def doctest(session: nox.sessions.Session): """ prepare_venv(session) session.install("--no-deps", "--editable", ".") + session.env.update(ENV) session.cd("docs") session.run( "make", diff --git a/pyproject.toml b/pyproject.toml index 8d01db2af7..26e6ae727a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 79 -target-version = ['py37', 'py38'] +target-version = ['py38'] include = '\.pyi?$' extend-exclude = ''' ( diff --git a/requirements/ci/nox.lock/py37-linux-64.lock b/requirements/ci/nox.lock/py37-linux-64.lock deleted file mode 100644 index a9a1c52811..0000000000 --- a/requirements/ci/nox.lock/py37-linux-64.lock +++ /dev/null @@ -1,231 +0,0 @@ -# Generated by conda-lock. -# platform: linux-64 -# input_hash: f2449723874977e1bb5b571ab6f475f8d251e472d6f8826bd40fbb774e3916f7 -@EXPLICIT -https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 -https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2021.10.8-ha878542_0.tar.bz2#575611b8a84f45960e87722eeb51fa26 -https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 -https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 -https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb -https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-hab24e00_0.tar.bz2#19410c3df09dfb12d1206132a1d357c5 -https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.36.1-hea4e1c9_2.tar.bz2#bd4f2e711b39af170e7ff15163fe87ee -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-11.2.0-h5c6108e_11.tar.bz2#2dcb18a9a0fa31f4f29e5a9b3eade394 -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-11.2.0-he4da1e4_11.tar.bz2#0bf83958e788f1e75ba26154cb702afe -https://conda.anaconda.org/conda-forge/linux-64/mpi-1.0-mpich.tar.bz2#c1fcff3417b5a22bbc4cf6e8c23648cf -https://conda.anaconda.org/conda-forge/linux-64/mysql-common-8.0.27-ha770c72_1.tar.bz2#ca56dd3e2768f99f9e19869efb3434ec -https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-11.2.0-h69a702a_11.tar.bz2#4ea2f9f83b617a7682e8aa05dcb37c6a -https://conda.anaconda.org/conda-forge/linux-64/libgomp-11.2.0-h1d223b6_11.tar.bz2#1d16527c76842bf9c41e9399d39d8097 -https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-1_gnu.tar.bz2#561e277319a41d4f24f5c05a9ef63c04 -https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-11.2.0-h1d223b6_11.tar.bz2#e3495f4f93cfd6b68021cbe2b5844cd5 -https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.3-h516909a_0.tar.bz2#1378b88874f42ac31b2f8e4f6975cb7b -https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h7f98852_4.tar.bz2#a1fd65c7ccbf10880423d82bca54eb54 -https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.18.1-h7f98852_0.tar.bz2#f26ef8098fab1f719c91eb760d63381a -https://conda.anaconda.org/conda-forge/linux-64/expat-2.4.1-h9c3ff4c_0.tar.bz2#16054ef3cb3ec5d8d29d08772662f65d -https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8 -https://conda.anaconda.org/conda-forge/linux-64/geos-3.10.1-h9c3ff4c_1.tar.bz2#17a5f413039ce1e105fab5df9c668eb5 -https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.1-h36c2ea0_2.tar.bz2#626e68ae9cc5912d6adb79d318cf962d -https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.13-h58526e2_1001.tar.bz2#8c54672728e8ec6aa6db90cf2806d220 -https://conda.anaconda.org/conda-forge/linux-64/icu-68.2-h9c3ff4c_0.tar.bz2#6618c9b191638993f2a818c6529e1b49 -https://conda.anaconda.org/conda-forge/linux-64/jbig-2.1-h7f98852_2003.tar.bz2#1aa0cee79792fa97b7ff4545110b60bf -https://conda.anaconda.org/conda-forge/linux-64/jpeg-9d-h36c2ea0_0.tar.bz2#ea02ce6037dbe81803ae6123e5ba1568 -https://conda.anaconda.org/conda-forge/linux-64/lerc-3.0-h9c3ff4c_0.tar.bz2#7fcefde484980d23f0ec24c11e314d2e -https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.0.9-h7f98852_6.tar.bz2#b0f44f63f7d771d7670747a1dd5d5ac1 -https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.8-h7f98852_0.tar.bz2#91d22aefa665265e8e31988b15145c8a -https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-h516909a_1.tar.bz2#6f8720dff19e17ce5d48cfe7f3d2f0a3 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h7f98852_5.tar.bz2#d645c6d2ac96843a2bfaccd2d62b3ac3 -https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.16-h516909a_0.tar.bz2#5c0f338a513a2943c659ae619fca9211 -https://conda.anaconda.org/conda-forge/linux-64/libmo_unpack-3.1.2-hf484d3e_1001.tar.bz2#95f32a6a5a666d33886ca5627239f03d -https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.0-h7f98852_0.tar.bz2#39b1328babf85c7c3a61636d9cd50206 -https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.18-pthreads_h8fe5266_0.tar.bz2#41532e4448c0cce086d6570f95e4e12e -https://conda.anaconda.org/conda-forge/linux-64/libopus-1.3.1-h7f98852_1.tar.bz2#15345e56d527b330e1cacbdf58676e8f -https://conda.anaconda.org/conda-forge/linux-64/libtool-2.4.6-h9c3ff4c_1008.tar.bz2#16e143a1ed4b4fd169536373957f6fee -https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.32.1-h7f98852_1000.tar.bz2#772d69f030955d9646d3d0eaf21d859d -https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.2.1-h7f98852_0.tar.bz2#90607c4c0247f04ec98b48997de71c1a -https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.11-h36c2ea0_1013.tar.bz2#dcddf696ff5dfcab567100d691678e18 -https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.3-h9c3ff4c_1.tar.bz2#fbe97e8fa6f275d7c76a09e795adc3e6 -https://conda.anaconda.org/conda-forge/linux-64/mpich-3.4.2-h846660c_101.tar.bz2#9478c28797583eff62a58853f92ad7f6 -https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.2-h58526e2_4.tar.bz2#509f2a21c4a09214cd737a480dfd80c9 -https://conda.anaconda.org/conda-forge/linux-64/nspr-4.32-h9c3ff4c_1.tar.bz2#29ded371806431b0499aaee146abfc3e -https://conda.anaconda.org/conda-forge/linux-64/openssl-1.1.1l-h7f98852_0.tar.bz2#de7b38a1542dbe6f41653a8ae71adc53 -https://conda.anaconda.org/conda-forge/linux-64/pcre-8.45-h9c3ff4c_0.tar.bz2#c05d1820a6d34ff07aaaab7a9b7eddaa -https://conda.anaconda.org/conda-forge/linux-64/pixman-0.40.0-h36c2ea0_0.tar.bz2#660e72c82f2e75a6b3fe6a6e75c79f19 -https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036 -https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2#4b230e8381279d76131116660f5a241a -https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.0.10-h7f98852_0.tar.bz2#d6b0b50b49eccfe0be0373be628be0f3 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.9-h7f98852_0.tar.bz2#bf6f803a544f26ebbdc3bfff272eb179 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.3-h7f98852_0.tar.bz2#be93aabceefa2fac576e971aef407908 -https://conda.anaconda.org/conda-forge/linux-64/xorg-renderproto-0.11.1-h7f98852_1002.tar.bz2#06feff3d2634e3097ce2fe681474b534 -https://conda.anaconda.org/conda-forge/linux-64/xorg-xextproto-7.3.0-h7f98852_1002.tar.bz2#1e15f6ad85a7d743a2ac68dae6c82b98 -https://conda.anaconda.org/conda-forge/linux-64/xorg-xproto-7.0.31-h7f98852_1007.tar.bz2#b4a4381d54784606820704f7b5f05a15 -https://conda.anaconda.org/conda-forge/linux-64/xxhash-0.8.0-h7f98852_3.tar.bz2#52402c791f35e414e704b7a113f99605 -https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.5-h516909a_1.tar.bz2#33f601066901f3e1a85af3522a8113f9 -https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h516909a_0.tar.bz2#03a530e925414902547cf48da7756db8 -https://conda.anaconda.org/conda-forge/linux-64/gettext-0.19.8.1-h73d1719_1008.tar.bz2#af49250eca8e139378f8ff0ae9e57251 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-12_linux64_openblas.tar.bz2#4f93ba28c628a2c27cf39c055e6b219c -https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.0.9-h7f98852_6.tar.bz2#c7c03a2592cac92246a13a0732bd1573 -https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.0.9-h7f98852_6.tar.bz2#28bfe0a70154e6881da7bae97517c948 -https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 -https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.10-h9b69904_4.tar.bz2#390026683aef81db27ff1b8570ca1336 -https://conda.anaconda.org/conda-forge/linux-64/libvorbis-1.3.7-h9c3ff4c_0.tar.bz2#309dec04b70a3cc0f1e84a4013683bc0 -https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.13-h7f98852_1004.tar.bz2#b3653fdc58d03face9724f602218a904 -https://conda.anaconda.org/conda-forge/linux-64/readline-8.1-h46c0cb4_0.tar.bz2#5788de3c8d7a7d64ac56c784c4ef48e6 -https://conda.anaconda.org/conda-forge/linux-64/udunits2-2.2.27.27-hc3e0081_3.tar.bz2#a47110f41fcbf88fcdf8549d7f69a6d8 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.3-hd9c2040_1000.tar.bz2#9e856f78d5c80d5a78f61e72d1d473a3 -https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.11-h36c2ea0_1013.tar.bz2#cf7190238072a41e9579e4476a6a60b8 -https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.0.9-h7f98852_6.tar.bz2#9e94bf16f14c78a36561d5019f490d22 -https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h10796ff_3.tar.bz2#21a8d66dc17f065023b33145c42652fe -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-12_linux64_openblas.tar.bz2#2e5082d4a9a18c21100e6ce5b6bcb4ec -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.70.1-h174f98d_0.tar.bz2#b14ca7dc99d099dccf38d4d31ead3d63 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-12_linux64_openblas.tar.bz2#9f401a6807a97e0c859d7522ae3d51ec -https://conda.anaconda.org/conda-forge/linux-64/libllvm11-11.1.0-hf817b99_2.tar.bz2#646fa2f7c60b69ee8f918668e9c2fd31 -https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.43.0-h812cca2_1.tar.bz2#d0a7846b7b3b8fb0d8b36904a53b8155 -https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.37-h21135ba_2.tar.bz2#b6acf807307d033d4b7e758b4f44b036 -https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.10.0-ha56f1ee_2.tar.bz2#6ab4eaa11ff01801cffca0a27489dc04 -https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.9.12-h72842e0_0.tar.bz2#bd14fdf5b9ee5568056a40a6a2f41866 -https://conda.anaconda.org/conda-forge/linux-64/libzip-1.8.0-h4de3113_1.tar.bz2#175a746a43d42c053b91aa765fbc197d -https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.37.0-h9cd32fc_0.tar.bz2#eb66fc098824d25518a79e83d12a81d6 -https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.11-h27826a3_1.tar.bz2#84e76fb280e735fec1efd2d21fd9cb27 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.7.2-h7f98852_0.tar.bz2#12a61e640b8894504326aadafccbb790 -https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.0-ha95c52a_0.tar.bz2#b56f94865e2de36abf054e7bfa499034 -https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.36.0-h3371d22_4.tar.bz2#661e1ed5d92552785d9f8c781ce68685 -https://conda.anaconda.org/conda-forge/linux-64/brotli-1.0.9-h7f98852_6.tar.bz2#612385c4a83edb0619fe911d9da317f4 -https://conda.anaconda.org/conda-forge/linux-64/freetype-2.10.4-h0708190_1.tar.bz2#4a06f2ac2e5bfae7b6b245171c3f07aa -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.70.1-h780b84a_0.tar.bz2#e60c13f740e8988a6efa2822d63738bd -https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.18.5-h9f60fe5_2.tar.bz2#6221115a24700aa8598ae5aa1574902d -https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h64030ff_2.tar.bz2#112eb9b5b93f0c02e59aea4fd1967363 -https://conda.anaconda.org/conda-forge/linux-64/krb5-1.19.2-hcc1bbae_3.tar.bz2#e29650992ae593bc05fc93722483e5c3 -https://conda.anaconda.org/conda-forge/linux-64/libclang-11.1.0-default_ha53f305_1.tar.bz2#b9b71585ca4fcb5d442c5a9df5dd7e98 -https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.3.0-h6f004c6_2.tar.bz2#34fda41ca84e67232888c9a885903055 -https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.0.3-he3ba5ed_0.tar.bz2#f9dbabc7e01c459ed7a1d1d64b206e9b -https://conda.anaconda.org/conda-forge/linux-64/mysql-libs-8.0.27-hfa10184_1.tar.bz2#577ac13be280c34b2f0cfd44267d2eac -https://conda.anaconda.org/conda-forge/linux-64/nss-3.73-hb5efdd6_0.tar.bz2#a5b91a14292ac34bac1f0506a3772fd5 -https://conda.anaconda.org/conda-forge/linux-64/python-3.7.12-hb7a2778_100_cpython.tar.bz2#2d94b3e6a9fdaf83f6955d008c8011a7 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.4-h7f98852_1.tar.bz2#536cc5db4d0a3ba0630541aec064b5e4 -https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.10-h7f98852_1003.tar.bz2#f59c1242cc1dd93e72c2ee2b360979eb -https://conda.anaconda.org/conda-forge/noarch/alabaster-0.7.12-py_0.tar.bz2#2489a97287f90176ecdc3ca982b4b0a0 -https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyh9f0ad1d_0.tar.bz2#5f095bc6454094e96f146491fd03633b -https://conda.anaconda.org/conda-forge/noarch/cfgv-3.3.1-pyhd8ed1ab_0.tar.bz2#ebb5f5f7dc4f1a3780ef7ea7738db08c -https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-2.0.8-pyhd8ed1ab_0.tar.bz2#7bc3465ff423c243bf8b2e77c5075bc7 -https://conda.anaconda.org/conda-forge/noarch/cloudpickle-2.0.0-pyhd8ed1ab_0.tar.bz2#3a8fc8b627d5fb6af827e126a10a86c6 -https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.4-pyh9f0ad1d_0.tar.bz2#c08b4c1326b880ed44f3ffb04803332f -https://conda.anaconda.org/conda-forge/noarch/cycler-0.11.0-pyhd8ed1ab_0.tar.bz2#a50559fad0affdbb33729a68669ca1cb -https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.3-pyhd8ed1ab_0.tar.bz2#cc7dae067bb31c1598e23e151dfca986 -https://conda.anaconda.org/conda-forge/noarch/filelock-3.4.0-pyhd8ed1ab_0.tar.bz2#caff9785491992b3250ed4048fe51e2c -https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.13.1-hba837de_1005.tar.bz2#fd3611672eb91bc9d24fd6fb970037eb -https://conda.anaconda.org/conda-forge/noarch/fsspec-2021.11.1-pyhd8ed1ab_0.tar.bz2#a510ec93fdb50775091d2afba98a8acb -https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.42.6-h04a7f16_0.tar.bz2#b24a1e18325a6e8f8b6b4a2ec5860ce2 -https://conda.anaconda.org/conda-forge/linux-64/glib-2.70.1-h780b84a_0.tar.bz2#bc4795f9d11dba7990c7e9cf9f7021b3 -https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.18.5-hf529b03_2.tar.bz2#3cf866063f2803944ddaee8b1d6da531 -https://conda.anaconda.org/conda-forge/noarch/idna-3.1-pyhd3deb0d_0.tar.bz2#9c9aea4b8391264477df484f798562d0 -https://conda.anaconda.org/conda-forge/noarch/imagesize-1.3.0-pyhd8ed1ab_0.tar.bz2#be807e7606fff9436e5e700f6bffb7c6 -https://conda.anaconda.org/conda-forge/noarch/iris-sample-data-2.4.0-pyhd8ed1ab_0.tar.bz2#18ee9c07cf945a33f92caf1ee3d23ad9 -https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.12-hddcbb42_0.tar.bz2#797117394a4aa588de6d741b06fad80f -https://conda.anaconda.org/conda-forge/linux-64/libcurl-7.80.0-h2574ce0_0.tar.bz2#5d0784b790350f7939bb5d3f2c32e700 -https://conda.anaconda.org/conda-forge/linux-64/libpq-13.5-hd57d9b9_0.tar.bz2#20a3e094316bbaa7b890ccbd9c97acd5 -https://conda.anaconda.org/conda-forge/linux-64/libwebp-1.2.1-h3452ae3_0.tar.bz2#6d4bf6265d998b6c975c26a6a24062a2 -https://conda.anaconda.org/conda-forge/noarch/locket-0.2.0-py_2.tar.bz2#709e8671651c7ec3d1ad07800339ff1d -https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 -https://conda.anaconda.org/conda-forge/noarch/nose-1.3.7-py_1006.tar.bz2#382019d5f8e9362ef6f60a8d4e7bce8f -https://conda.anaconda.org/conda-forge/noarch/olefile-0.46-pyh9f0ad1d_1.tar.bz2#0b2e68acc8c78c8cc392b90983481f58 -https://conda.anaconda.org/conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2#076becd9e05608f8dc72757d5f3a91ff -https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.0.6-pyhd8ed1ab_0.tar.bz2#3087df8c636c5a00e694605c39ce4982 -https://conda.anaconda.org/conda-forge/noarch/pyshp-2.1.3-pyh44b312d_0.tar.bz2#2d1867b980785eb44b8122184d8b42a6 -https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.7-2_cp37m.tar.bz2#afff88bf9a7048da740c70aeb8cdbb82 -https://conda.anaconda.org/conda-forge/noarch/pytz-2021.3-pyhd8ed1ab_0.tar.bz2#7e4f811bff46a5a6a7e0094921389395 -https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 -https://conda.anaconda.org/conda-forge/noarch/snowballstemmer-2.2.0-pyhd8ed1ab_0.tar.bz2#4d22a9315e78c6827f806065957d566e -https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-applehelp-1.0.2-py_0.tar.bz2#20b2eaeaeea4ef9a9a0d99770620fd09 -https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-devhelp-1.0.2-py_0.tar.bz2#68e01cac9d38d0e717cd5c87bc3d2cc9 -https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-htmlhelp-2.0.0-pyhd8ed1ab_0.tar.bz2#77dad82eb9c8c1525ff7953e0756d708 -https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-jsmath-1.0.1-py_0.tar.bz2#67cd9d9c0382d37479b4d306c369a2d4 -https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-qthelp-1.0.3-py_0.tar.bz2#d01180388e6d1838c3e1ad029590aa7a -https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_0.tar.bz2#f832c45a477c78bebd107098db465095 -https://conda.anaconda.org/conda-forge/noarch/toolz-0.11.2-pyhd8ed1ab_0.tar.bz2#f348d1590550371edfac5ed3c1d44f7e -https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.0.1-pyha770c72_0.tar.bz2#1fc03816925d3cb7fdab9ab234e7fea7 -https://conda.anaconda.org/conda-forge/noarch/wheel-0.37.0-pyhd8ed1ab_1.tar.bz2#3aa2c3e25dd361b453d010388b9cdff1 -https://conda.anaconda.org/conda-forge/noarch/zipp-3.6.0-pyhd8ed1ab_0.tar.bz2#855e2c4622f5eb50a4f6f7167b9ba17a -https://conda.anaconda.org/conda-forge/linux-64/antlr-python-runtime-4.7.2-py37h89c1867_1003.tar.bz2#490366305378c8690b65c4bce9b9f6a4 -https://conda.anaconda.org/conda-forge/noarch/babel-2.9.1-pyh44b312d_0.tar.bz2#74136ed39bfea0832d338df1e58d013e -https://conda.anaconda.org/conda-forge/linux-64/cairo-1.16.0-h6cf1ce9_1008.tar.bz2#a43fb47d15e116f8be4be7e6b17ab59f -https://conda.anaconda.org/conda-forge/linux-64/certifi-2021.10.8-py37h89c1867_1.tar.bz2#48e8442b6097c7d4a0e3494c74ff9eeb -https://conda.anaconda.org/conda-forge/linux-64/cffi-1.15.0-py37h036bc23_0.tar.bz2#05ab26c7685bcb7dd8bc8752c121f823 -https://conda.anaconda.org/conda-forge/linux-64/curl-7.80.0-h2574ce0_0.tar.bz2#4d8fd67e5ab7e00fde8ad085464f43b7 -https://conda.anaconda.org/conda-forge/linux-64/cython-0.29.24-py37hcd2ae1e_1.tar.bz2#c32a8d5bea21a1a7074b005199a45ede -https://conda.anaconda.org/conda-forge/linux-64/dbus-1.13.6-h48d8840_2.tar.bz2#eba672c69baf366fdedd1c6f702dbb81 -https://conda.anaconda.org/conda-forge/linux-64/docutils-0.17.1-py37h89c1867_1.tar.bz2#e0a3be74a594032b73f22762ba9941cc -https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.12.1-mpi_mpich_h9c45103_2.tar.bz2#39a57ab5ac7b7ea3342ba45d8c905a5e -https://conda.anaconda.org/conda-forge/linux-64/importlib-metadata-4.8.2-py37h89c1867_0.tar.bz2#a62f2bad6654c1a91e3241ca7979fe05 -https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.3.2-py37h2527ec5_1.tar.bz2#441ac4d93d0d57d21ea9dcac48cb5d0d -https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h6ad9fb6_0.tar.bz2#45142dc44fcd04934f9ad68ce205e54d -https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.0.1-py37h5e8e339_1.tar.bz2#6c7c14c95d4c435b66261639b64c7c51 -https://conda.anaconda.org/conda-forge/linux-64/mpi4py-3.1.3-py37h1e5cb63_0.tar.bz2#3d5ca9f081a7756df4f027776ff23b73 -https://conda.anaconda.org/conda-forge/linux-64/numpy-1.21.4-py37h31617e3_0.tar.bz2#332cef055880482fe23f6ac1e7ee6e57 -https://conda.anaconda.org/conda-forge/noarch/packaging-21.3-pyhd8ed1ab_0.tar.bz2#71f1ab2de48613876becddd496371c85 -https://conda.anaconda.org/conda-forge/noarch/partd-1.2.0-pyhd8ed1ab_0.tar.bz2#0c32f563d7f22e3a34c95cad8cc95651 -https://conda.anaconda.org/conda-forge/linux-64/pillow-6.2.2-py37h718be6c_0.tar.bz2#ecac4e308b87ff93d44ea5e56ab39084 -https://conda.anaconda.org/conda-forge/noarch/pockets-0.9.1-py_0.tar.bz2#1b52f0c42e8077e5a33e00fe72269364 -https://conda.anaconda.org/conda-forge/linux-64/proj-8.2.0-h277dcde_0.tar.bz2#7ba8c7a9bf1c2fedf4a6d6dc92839baf -https://conda.anaconda.org/conda-forge/linux-64/pyqt5-sip-4.19.18-py37hcd2ae1e_8.tar.bz2#ae12b17bbd5733cb8884b42dcc5c59f0 -https://conda.anaconda.org/conda-forge/linux-64/pysocks-1.7.1-py37h89c1867_4.tar.bz2#44df88d27e2891f90e3f06dcfcca0927 -https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2#dd999d1cc9f79e67dbb855c8924c7984 -https://conda.anaconda.org/conda-forge/linux-64/python-xxhash-2.0.2-py37h5e8e339_1.tar.bz2#c89489cddb9e53155e241e9aacd35e4b -https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0-py37h5e8e339_3.tar.bz2#7f167ecf4d4771ee33589e09479238e7 -https://conda.anaconda.org/conda-forge/linux-64/setuptools-59.4.0-py37h89c1867_0.tar.bz2#01f5271a7c204862ca564ee63b35548d -https://conda.anaconda.org/conda-forge/linux-64/tornado-6.1-py37h5e8e339_2.tar.bz2#ec86ae00c96dea5f2d810957a8fabc26 -https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-13.0.0.post2-py37h5e8e339_4.tar.bz2#6702ec56e12a0be3c70bf05991187bfd -https://conda.anaconda.org/conda-forge/linux-64/asv-0.4.2-py37hcd2ae1e_3.tar.bz2#0f076ca34a73c99253999aa65d787034 -https://conda.anaconda.org/conda-forge/linux-64/brotlipy-0.7.0-py37h5e8e339_1003.tar.bz2#4ad2e74470a3c08b0f6d59699f0d9a32 -https://conda.anaconda.org/conda-forge/linux-64/cftime-1.5.1.1-py37hb1e94ed_1.tar.bz2#1b5b81088bc7d7e0bef7de4ef4bd1221 -https://conda.anaconda.org/conda-forge/linux-64/cryptography-36.0.0-py37hf1a17b8_0.tar.bz2#2fd357ed549ff7f39a04f329938de4b3 -https://conda.anaconda.org/conda-forge/noarch/dask-core-2021.11.2-pyhd8ed1ab_0.tar.bz2#c3615fd90d2e5b11c5da6ea1f9a5730b -https://conda.anaconda.org/conda-forge/linux-64/editdistance-s-1.0.0-py37h2527ec5_2.tar.bz2#9aba6bcb02d12dbd2fead23b85720712 -https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.28.3-py37h5e8e339_0.tar.bz2#5b7840e065bcdbfc6aaa72d772fcd190 -https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-3.1.1-h83ec7ef_0.tar.bz2#ca8faaee04a83e3c4d6f708a35ac2ec3 -https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-4.8.2-hd8ed1ab_0.tar.bz2#4fcc9776fdf4c35c5a10662bbbd21a61 -https://conda.anaconda.org/conda-forge/noarch/jinja2-3.0.3-pyhd8ed1ab_0.tar.bz2#036d872c653780cb26e797e2e2f61b4c -https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.8.1-mpi_mpich_h319fa22_1.tar.bz2#7583fbaea3648f692c0c019254bc196c -https://conda.anaconda.org/conda-forge/linux-64/mo_pack-0.2.0-py37hb1e94ed_1006.tar.bz2#e06cf91c2624284413641be2cb8c3198 -https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.6.0-pyhd8ed1ab_0.tar.bz2#0941325bf48969e2b3b19d0951740950 -https://conda.anaconda.org/conda-forge/linux-64/pandas-1.3.4-py37he8f5f7f_1.tar.bz2#b8e4308f454bc3e9ceeab49eb7fe7405 -https://conda.anaconda.org/conda-forge/noarch/pip-21.3.1-pyhd8ed1ab_0.tar.bz2#e4fe2a9af78ff11f1aced7e62128c6a8 -https://conda.anaconda.org/conda-forge/noarch/pygments-2.10.0-pyhd8ed1ab_0.tar.bz2#32bcce837f1316f1c3208118b6c5e5fc -https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.2.1-py37hb589d83_5.tar.bz2#ea78cbba7d43ad17ec043a9ebdee3bf5 -https://conda.anaconda.org/conda-forge/linux-64/python-stratify-0.1.1-py37h6f94858_1004.tar.bz2#42b37830a63405589fef3d13db505e7d -https://conda.anaconda.org/conda-forge/linux-64/pywavelets-1.2.0-py37hb1e94ed_1.tar.bz2#3a94b25c520754b56cdfa7d865806524 -https://conda.anaconda.org/conda-forge/linux-64/qt-5.12.9-hda022c4_4.tar.bz2#afebab1f5049d66baaaec67d9ce893f0 -https://conda.anaconda.org/conda-forge/linux-64/scipy-1.7.3-py37hf2a6cf1_0.tar.bz2#129c613e1d0f09d9fd0473a0da6161a9 -https://conda.anaconda.org/conda-forge/linux-64/shapely-1.8.0-py37h9b0f7a3_4.tar.bz2#568474687cd6be5f834cb682637ac0de -https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-napoleon-0.7-py_0.tar.bz2#0bc25ff6f2e34af63ded59692df5f749 -https://conda.anaconda.org/conda-forge/linux-64/virtualenv-20.4.7-py37h89c1867_1.tar.bz2#cbe5a8c8ae88d1e73b4297a73d08408a -https://conda.anaconda.org/conda-forge/linux-64/cf-units-3.0.1-py37hb1e94ed_1.tar.bz2#63d03e033e18576d104474a76d56b6de -https://conda.anaconda.org/conda-forge/noarch/identify-2.3.7-pyhd8ed1ab_0.tar.bz2#ae1a5e834fbca62ee88ab55fb276be63 -https://conda.anaconda.org/conda-forge/noarch/imagehash-4.2.1-pyhd8ed1ab_0.tar.bz2#01cc8698b6e1a124dc4f585516c27643 -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.5.0-py37h1058ff1_0.tar.bz2#de800e20af9257645b36eac046b98080 -https://conda.anaconda.org/conda-forge/linux-64/netcdf-fortran-4.5.3-mpi_mpich_h1364a43_6.tar.bz2#9caa0cf923af3d037897c6d7f8ea57c0 -https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.5.8-nompi_py37hf784469_101.tar.bz2#5b05dc55e51be0696878e9a575c12f77 -https://conda.anaconda.org/conda-forge/linux-64/pango-1.48.10-h54213e6_2.tar.bz2#b7ed7c76c9360db1f91afba2e220007b -https://conda.anaconda.org/conda-forge/noarch/pyopenssl-21.0.0-pyhd8ed1ab_0.tar.bz2#8c49efecb7dca466e18b06015e8c88ce -https://conda.anaconda.org/conda-forge/linux-64/pyqt-impl-5.12.3-py37hac37412_8.tar.bz2#148f2e971a67831ed0691f63cd826468 -https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.20.1-py37h9a08e6e_5.tar.bz2#e44dc116f747b0a7bceaf1533acc6b48 -https://conda.anaconda.org/conda-forge/linux-64/esmf-8.2.0-mpi_mpich_h4975321_100.tar.bz2#56f5c650937b1667ad0a557a0dff3bc4 -https://conda.anaconda.org/conda-forge/linux-64/gtk2-2.24.33-h539f30e_1.tar.bz2#606777b4da3664d5c9415f5f165349fd -https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.52.4-hc3c00ef_0.tar.bz2#310ae1e113d6febc26fe3b44e3519757 -https://conda.anaconda.org/conda-forge/noarch/nc-time-axis-1.4.0-pyhd8ed1ab_0.tar.bz2#9113b4e4fa2fa4a7f129c71a6f319475 -https://conda.anaconda.org/conda-forge/linux-64/pre-commit-2.16.0-py37h89c1867_0.tar.bz2#43b270fe44130353e540037ad27da097 -https://conda.anaconda.org/conda-forge/linux-64/pyqtchart-5.12-py37he336c9b_8.tar.bz2#2fe25d82cb4e59191df561c40870ca6b -https://conda.anaconda.org/conda-forge/linux-64/pyqtwebengine-5.12.1-py37he336c9b_8.tar.bz2#0a67d477c0524897883ca0f86d6fb15c -https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.7-pyhd8ed1ab_0.tar.bz2#be75bab4820a56f77ba1a3fc9139c36a -https://conda.anaconda.org/conda-forge/linux-64/esmpy-8.2.0-mpi_mpich_py37h7352969_101.tar.bz2#64fd02e7a0cefe0b5c604fea03774c73 -https://conda.anaconda.org/conda-forge/linux-64/graphviz-2.49.3-h85b4f2f_0.tar.bz2#71062453180260ef85c861996b29bd6a -https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.12.3-py37h89c1867_8.tar.bz2#8038f9765a907fcf6fdfa6a9db71e371 -https://conda.anaconda.org/conda-forge/noarch/requests-2.26.0-pyhd8ed1ab_1.tar.bz2#358581cc782802270d77c454c73a811a -https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.5.0-py37h89c1867_0.tar.bz2#dc2167a62be1ca5253727201368ddc89 -https://conda.anaconda.org/conda-forge/noarch/sphinx-4.3.1-pyh6c4a22f_0.tar.bz2#74d14a40bf09d1d8d788a8433f04e0d8 -https://conda.anaconda.org/conda-forge/noarch/sphinx-copybutton-0.4.0-pyhd8ed1ab_0.tar.bz2#80fd2cc25ad45911b4e42d5b91593e2f -https://conda.anaconda.org/conda-forge/noarch/sphinx-gallery-0.10.1-pyhd8ed1ab_0.tar.bz2#4918585fe5e5341740f7e63c61743efb -https://conda.anaconda.org/conda-forge/noarch/sphinx-panels-0.6.0-pyhd8ed1ab_0.tar.bz2#6eec6480601f5d15babf9c3b3987f34a -https://conda.anaconda.org/conda-forge/noarch/sphinx_rtd_theme-1.0.0-pyhd8ed1ab_0.tar.bz2#9f633f2f2869184e31acfeae95b24345 -https://conda.anaconda.org/conda-forge/noarch/sphinxcontrib-serializinghtml-1.1.5-pyhd8ed1ab_1.tar.bz2#63d2f874f990fdcab47c822b608d6ade diff --git a/requirements/ci/py37.yml b/requirements/ci/py37.yml deleted file mode 100644 index 8e4b9d9c10..0000000000 --- a/requirements/ci/py37.yml +++ /dev/null @@ -1,48 +0,0 @@ -name: iris-dev - -channels: - - conda-forge - -dependencies: - - python =3.7 - -# Setup dependencies. - - setuptools >=40.8.0 - -# Core dependencies. - - cartopy >=0.20 - - cf-units >=3 - - cftime >=1.5 - - dask-core >=2 - - matplotlib - - netcdf4 - - numpy >=1.19 - - python-xxhash - - scipy - -# Optional dependencies. - - esmpy >=7.0 - - graphviz - - iris-sample-data >=2.4.0 - - mo_pack - - nc-time-axis >=1.3 - - pandas - - pip - - python-stratify - -# Test dependencies. - - asv - - filelock - - imagehash >=4.0 - - nose - - pillow <7 - - pre-commit - - requests - -# Documentation dependencies. - - sphinx - - sphinxcontrib-napoleon - - sphinx-copybutton - - sphinx-gallery - - sphinx-panels - - sphinx_rtd_theme diff --git a/requirements/ci/py38.yml b/requirements/ci/py38.yml index d7ccb0b9d5..d3d7f9d0c2 100644 --- a/requirements/ci/py38.yml +++ b/requirements/ci/py38.yml @@ -31,7 +31,6 @@ dependencies: - python-stratify # Test dependencies. - - asv - filelock - imagehash >=4.0 - nose diff --git a/setup.cfg b/setup.cfg index 26516ee1af..1d3fb8b7c9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,7 +59,7 @@ packages = find: package_dir = =lib python_requires = - >=3.7 + >=3.8 zip_safe = False [options.packages.find] @@ -74,7 +74,6 @@ docs = sphinxcontrib-napoleon sphinx-panels test = - asv filelock imagehash>=4.0 nose diff --git a/tools/gen_helpers.py b/tools/gen_helpers.py deleted file mode 100644 index 825c78139e..0000000000 --- a/tools/gen_helpers.py +++ /dev/null @@ -1,36 +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. - -from datetime import datetime -import os -import os.path - -HEADER = \ - '''# 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. -# -# DO NOT EDIT: AUTO-GENERATED''' - - -def absolute_path(path): - return os.path.abspath(os.path.join(os.path.dirname(__file__), path)) - - -def prep_module_file(module_path): - """ - prepare a module file, creating directory if needed and writing the - header into that file - - """ - module_path = absolute_path(module_path) - module_dir = os.path.dirname(module_path) - if not os.path.isdir(module_dir): - os.makedirs(module_dir) - with open(module_path, 'w') as module_file: - module_file.write(HEADER.format(datetime.utcnow().year)) diff --git a/tools/gen_stash_refs.py b/tools/gen_stash_refs.py deleted file mode 100644 index e614b52ab2..0000000000 --- a/tools/gen_stash_refs.py +++ /dev/null @@ -1,126 +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. - -import json -import urllib -import urllib2 - -from iris.fileformats.pp import STASH - -import gen_helpers - - -HEADER = ''' -""" -Auto-generated from iris/tools/gen_stash_refs.py -Relates grid code and field code to the stash code. - -""" -''' - -CODE_PREAMBLE = ("from collections import namedtuple\n\n\n" - "Stash = namedtuple('Stash', " - "'grid_code field_code pseudo_level_type')\n\n\n") - - -def _value_from_xref(xref, name): - """Return the value for the key name from xref. - - Will return 0 if the key does not look like an integer. - """ - - result = xref.get(name) - try: - int(result) - except (ValueError, TypeError): - result = 0 - return result - - -def write_cross_reference_module(module_path, xrefs): - gen_helpers.prep_module_file(module_path) - with open(module_path, 'a') as module_file: - module_file.write(HEADER) - module_file.write(CODE_PREAMBLE) - module_file.write('STASH_TRANS = {\n') - for xref in xrefs: - stash = xref.get('stash') - try: - STASH.from_msi(stash.replace('"', '')) - except ValueError: - msg = ('stash code is not of a recognised' - '"m??s??i???" form: {}'.format(stash)) - print(msg) - grid = xref.get('grid') - if grid is not None: - try: - int(grid) - except ValueError: - msg = ('grid code retrieved from STASH lookup' - 'is not an integer: {}'.format(grid)) - print(msg) - else: - grid = 0 - - lbfc = _value_from_xref(xref, 'lbfcn') - pseudT = _value_from_xref(xref, 'pseudT') - - module_file.write( - ' "{}": Stash({}, {}, {}),\n'.format(stash, - grid, - lbfc, - pseudT)) - module_file.write('}\n') - - -def stash_grid_retrieve(): - """return a dictionary of stash codes and rel;ated information from - the Met Office Reference Registry - """ - baseurl = 'http://reference.metoffice.gov.uk/system/query?query=' - query = '''prefix rdf: -prefix rdfs: -prefix skos: - -SELECT ?stash ?grid ?lbfcn ?pseudT -WHERE { - ?stashcode rdf:type ; - skos:notation ?stash ; - ?gridcode . -OPTIONAL { ?gridcode skos:notation ?grid .} -OPTIONAL {?stashcode ?lbfc . - ?lbfc skos:notation ?lbfcn .} -OPTIONAL {?stashcode ?pseudT_id . - ?pseudT_id skos:notation ?pseudT . } -} -order by ?stash''' - - encquery = urllib.quote_plus(query) - out_format = '&output=json' - url = baseurl + encquery + out_format - - response = urllib2.urlopen(url) - stash = json.loads(response.read()) - - ## heads will be of the form [u'stash', u'grid', u'lbfcn', u'pseudT'] - ## as defined in the query string - heads = stash['head']['vars'] - - stashcodes = [] - - for result in stash['results']['bindings']: - res = {} - for head in heads: - if head in result: - res[head] = result[head]['value'] - stashcodes.append(res) - return stashcodes - - -if __name__ == '__main__': - xrefs = stash_grid_retrieve() - outfile = '../lib/iris/fileformats/_ff_cross_references.py' - write_cross_reference_module(outfile, xrefs) diff --git a/tools/gen_translations.py b/tools/gen_translations.py deleted file mode 100644 index 5ac0dc02ba..0000000000 --- a/tools/gen_translations.py +++ /dev/null @@ -1,216 +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. -""" -Processing of metarelate metOcean content to provide Iris encodings of -metOcean mapping translations. - -""" - -from datetime import datetime -import os.path -import requests -import sys - -import metarelate -from metarelate.fuseki import FusekiServer - -from translator import (FORMAT_URIS, FieldcodeCFMappings, StashCFNameMappings, - StashCFHeightConstraintMappings, - CFFieldcodeMappings, - GRIB1LocalParamCFConstrainedMappings, - GRIB1LocalParamCFMappings, GRIB2ParamCFMappings, - CFConstrainedGRIB1LocalParamMappings, - CFGRIB2ParamMappings, CFGRIB1LocalParamMappings) - -HEADER = """# Copyright {name} contributors -# -# This file is part of {name} and is released under the LGPL license. -# See COPYING and COPYING.LESSER in the root of the repository for full -# licensing details. -# -# DO NOT EDIT: AUTO-GENERATED -# Created on {datestamp} from -# http://www.metarelate.net/metOcean -# at commit {git_sha} -# https://github.com/metarelate/metOcean/commit/{git_sha} -{doc_string} - - -from collections import namedtuple - - -CFName = namedtuple('CFName', 'standard_name long_name units') -""" - -HEADER_GRIB = """ -DimensionCoordinate = namedtuple('DimensionCoordinate', - 'standard_name units points') - -G1LocalParam = namedtuple('G1LocalParam', 'edition t2version centre iParam') -G2Param = namedtuple('G2Param', 'edition discipline category number') -""" - -DOC_STRING_GRIB = r'''""" -Provides GRIB/CF phenomenon translations. - -"""''' - -DOC_STRING_UM = r'''""" -Provides UM/CF phenomenon translations. - -"""''' - -YEAR = datetime.utcnow().year - -def _retrieve_mappings(fuseki, source, target): - """ - Interrogate the metarelate triple store for all - phenomenon translation mappings from the source - scheme to the target scheme. - - Args: - * fuseki: - The :class:`metrelate.fuseki.FusekiServer` instance. - * source: - The source metarelate metadata type for the mapping. - * target: - The target metarelate metadata type for the mapping. - - Return: - The sequence of :class:`metarelate.Mapping` - instances. - - """ - suri = 'http://www.metarelate.net/sparql/metOcean' - msg = 'Retrieving {!r} to {!r} mappings ...' - print(msg.format(source, target)) - return fuseki.retrieve_mappings(source, target, service=suri) - - -def build_um_cf_map(fuseki, now, git_sha, base_dir): - """ - Encode the UM/CF phenomenon translation mappings - within the specified file. - - Args: - * fuseki: - The :class:`metarelate.fuseki.FusekiServer` instance. - * now: - Time stamp to write into the file - * git_sha: - The git SHA1 of the metarelate commit - * base_dir: - The root directory of the Iris source. - - """ - filename = os.path.join(base_dir, 'lib', 'iris', 'fileformats', - 'um_cf_map.py') - - # Create the base directory. - if not os.path.exists(os.path.dirname(filename)): - os.makedirs(os.path.dirname(filename)) - - # Create the file to contain UM/CF translations. - with open(filename, 'w') as fh: - fh.write(HEADER.format(year=YEAR, doc_string=DOC_STRING_UM, - datestamp=now, git_sha=git_sha, name='Iris')) - fh.write('\n') - - # Encode the relevant UM to CF translations. - maps = _retrieve_mappings(fuseki, FORMAT_URIS['umf'], - FORMAT_URIS['cff']) - # create the collections, then call lines on each one - # for thread safety during lines and encode - fccf = FieldcodeCFMappings(maps) - stcf = StashCFNameMappings(maps) - stcfhcon = StashCFHeightConstraintMappings(maps) - fh.writelines(fccf.lines(fuseki)) - fh.writelines(stcf.lines(fuseki)) - fh.writelines(stcfhcon.lines(fuseki)) - - # Encode the relevant CF to UM translations. - maps = _retrieve_mappings(fuseki, FORMAT_URIS['cff'], - FORMAT_URIS['umf']) - # create the collections, then call lines on each one - # for thread safety during lines and encode - cffc = CFFieldcodeMappings(maps) - fh.writelines(cffc.lines(fuseki)) - - -def build_grib_cf_map(fuseki, now, git_sha, base_dir): - """ - Encode the GRIB/CF phenomenon translation mappings - within the specified file. - - Args: - * fuseki: - The :class:`metarelate.fuseki.FusekiServer` instance. - * now: - Time stamp to write into the file - * git_sha: - The git SHA1 of the metarelate commit - * base_dir: - The root directory of the Iris source. - - """ - filename = os.path.join(base_dir, 'lib', 'iris', 'fileformats', - 'grib', '_grib_cf_map.py') - if not os.path.exists(os.path.dirname(filename)): - os.makedirs(os.path.dirname(filename)) - - # Create the file to contain GRIB/CF translations. - with open(filename, 'w') as fh: - fh.write(HEADER.format(year=YEAR, doc_string=DOC_STRING_GRIB, - datestamp=now, git_sha=git_sha, - name='iris-grib')) - fh.write(HEADER_GRIB) - fh.write('\n') - - # Encode the relevant GRIB to CF translations. - maps = _retrieve_mappings(fuseki, FORMAT_URIS['gribm'], - FORMAT_URIS['cff']) - # create the collections, then call lines on each one - # for thread safety during lines and encode - g1cfc = GRIB1LocalParamCFConstrainedMappings(maps) - g1c = GRIB1LocalParamCFMappings(maps) - g2c = GRIB2ParamCFMappings(maps) - fh.writelines(g1cfc.lines(fuseki)) - fh.writelines(g1c.lines(fuseki)) - fh.writelines(g2c.lines(fuseki)) - - # Encode the relevant CF to GRIB translations. - maps = _retrieve_mappings(fuseki, FORMAT_URIS['cff'], - FORMAT_URIS['gribm']) - # create the collections, then call lines on each one - # for thread safety during lines and encode - cfcg1 = CFConstrainedGRIB1LocalParamMappings(maps) - cg1 = CFGRIB1LocalParamMappings(maps) - cg2 = CFGRIB2ParamMappings(maps) - fh.writelines(cfcg1.lines(fuseki)) - fh.writelines(cg1.lines(fuseki)) - fh.writelines(cg2.lines(fuseki)) - - -def main(): - # Protect metarelate resource from 1.0 emergent bug - if not float(metarelate.__version__) >= 1.1: - raise ValueError("Please ensure that Metarelate Version is >= 1.1") - now = datetime.utcnow().strftime('%d %B %Y %H:%m') - git_sha = requests.get('http://www.metarelate.net/metOcean/latest_sha').text - gen_path = os.path.abspath(sys.modules['__main__'].__file__) - iris_path = os.path.dirname(os.path.dirname(gen_path)) - with FusekiServer() as fuseki: - build_um_cf_map(fuseki, now, git_sha, iris_path) - build_grib_cf_map(fuseki, now, git_sha, iris_path) - - if (git_sha != - requests.get('http://www.metarelate.net/metOcean/latest_sha').text): - raise ValueError('The metarelate translation store has altered during' - 'your retrieval, the results may not be stable.\n' - 'Please rerun your retrieval.') - -if __name__ == '__main__': - main() diff --git a/tools/generate_std_names.py b/tools/generate_std_names.py index 95dcce8171..08bacbe1e0 100644 --- a/tools/generate_std_names.py +++ b/tools/generate_std_names.py @@ -13,7 +13,9 @@ By default, Iris will use the source XML file: etc/cf-standard-name-table.xml as obtained from: - http://cf-pcmdi.llnl.gov/documents/cf-standard-names + http://cfconventions.org/standard-names.html + E.G. http://cfconventions.org/Data/cf-standard-names/78/src/cf-standard-name-table.xml + - N.B. no fixed 'latest' url is provided. """ diff --git a/tools/translator/__init__.py b/tools/translator/__init__.py deleted file mode 100644 index a83fee4edd..0000000000 --- a/tools/translator/__init__.py +++ /dev/null @@ -1,1116 +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. -""" -Provides the framework to support the encoding of metarelate mapping -translations. - -""" - -from abc import ABCMeta, abstractmethod -from collections import deque, namedtuple -import copy -from queue import Queue -import re -from threading import Thread -import warnings - -from metarelate.fuseki import FusekiServer, WorkerThread, MAXTHREADS -import metarelate - -# known format identifier URIs -FORMAT_URIS = {'cff': '', - 'gribm': '', - 'umf': ''} - -CFName = namedtuple('CFName', 'standard_name long_name units') -DimensionCoordinate = namedtuple('DimensionCoordinate', - 'standard_name units points') -G1LocalParam = namedtuple('G1LocalParam', 'edition t2version centre iParam') -G2Param = namedtuple('G2Param', 'edition discipline category number') - - -class MappingEncodeWorker(WorkerThread): - """Worker thread class for handling EncodableMap instances""" - def dowork(self, resource): - resource.encode(self.fuseki_process) - - -class EncodableMap: - """ - A metarelate mapping able to encode itself as a string for use in Iris, - as defined by a translator Mappings subclass - - """ - def __init__(self, mapping, sourcemsg, targetmsg, sourceid, targetid): - """ - Args: - * mapping: - A :class:`metarelate.Mapping` instance representing a translation. - * sourcemsg: - The code snippet message for the source of the translation for - formatting - * targetmsg: - The code snippet message for the target of the translation for - formatting - * sourceid: - A dictionary of required key:value pairs required by the sourcemsg - * targetid: - A dictionary of required key:value pairs required by the targetmsg - - """ - self.mapping = mapping - self.sourcemsg = sourcemsg - self.targetmsg = targetmsg - self.sourceid = sourceid - self.targetid = targetid - self.encoding = None - - def encode(self, fuseki_process): - """ - Return a string of the Python source code required to represent an - entry in a dictionary mapping source to target. - - Args: - * fuseki_process: - A :class:`metarelate.fuseki.FusekiServer` instance. - - """ - sids, tids = self.mapping.get_identifiers(fuseki_process) - self.sourceid.update(sids) - self.targetid.update(tids) - self.encoding = '{}: {}'.format(self.sourcemsg.format(**self.sourceid), - self.targetmsg.format(**self.targetid)) - - -class Mappings(metaclass=ABCMeta): - """ - Abstract base class to support the encoding of specific metarelate - mapping translations. - - """ - - def __init__(self, mappings): - """ - Filter the given sequence of mappings for those member - :class:`metarelate.Mapping` translations containing a source - :class:`metarelate.Component` with a matching - :attribute:`Mapping.source_scheme` and a target - :class:`metarelate.Component` with a matching - :attribute:`Mapping.target_scheme`. - - Also see :method:`Mapping.valid_mapping` for further matching - criterion for candidate metarelate mapping translations. - - Args: - * mappings: - Iterator of :class:`metarelate.Mapping` instances. - - """ - temp = [] - # Filter the mappings for the required type of translations. - for mapping in mappings: - source = mapping.source - target = mapping.target - sourcemsg, targetmsg = self.msg_strings() - sourceid, targetid = self.get_initial_id_nones() - if source.com_type == self.source_scheme and \ - target.com_type == self.target_scheme and \ - self.valid_mapping(mapping): - temp.append(EncodableMap(mapping, sourcemsg, targetmsg, - sourceid, targetid)) - self.mappings = temp - if len(self) == 0: - msg = '{!r} contains no mappings.' - warnings.warn(msg.format(self.__class__.__name__)) - - def _sort_lines(self, payload): - """ - Return the payload, unsorted. - - """ - return payload - - def lines(self, fuseki_process): - """ - Provides an iterator generating the encoded string representation - of each member of this metarelate mapping translation. - - Returns: - An iterator of string. - - """ - msg = '\tGenerating phenomenon translation {!r}.' - print(msg.format(self.mapping_name)) - lines = ['\n%s = {\n' % self.mapping_name] - # Retrieve encodings for the collection of mapping instances. - # Retrieval is threaded as it is heavily bound by resource resolution - # over http. - # Queue for metarelate mapping instances - mapenc_queue = Queue() - for mapping in self.mappings: - mapenc_queue.put(mapping) - # deque to contain the results of the jobs processed from the queue - mapencs = deque() - # run worker threads - for i in range(MAXTHREADS): - MappingEncodeWorker(mapenc_queue, mapencs, fuseki_process).start() - # block progress until the queue is empty - mapenc_queue.join() - # end of threaded retrieval process. - - # now sort the payload - payload = [mapenc.encoding for mapenc in mapencs] - payload.sort(key=self._key) - lines.extend(payload) - lines.append(' }\n') - return iter(lines) - - def __len__(self): - return len(self.mappings) - - def _key(self, line): - """Method to provide the sort key of the mappings order.""" - return line - - @property - @abstractmethod - def mapping_name(self): - """ - Abstract property that specifies the name of the dictionary - to contain the encoding of this metarelate mapping translation. - - """ - - @property - @abstractmethod - def source_scheme(self): - """ - Abstract property that specifies the name of the scheme for - the source :class:`metarelate.Component` defining this metarelate - mapping translation. - - """ - - @property - @abstractmethod - def target_scheme(self): - """ - Abstract property that specifies the name of the scheme for - the target :class:`metarelate.Component` defining this metarelate - mapping translation. - - """ - - @abstractmethod - def valid_mapping(self, mapping): - """ - Abstract method that determines whether the provided - :class:`metarelate.Mapping` is a translation from the required - source :class:`metarelate.Component` to the required target - :class:`metarelate.Component`. - - """ - - def get_initial_id_nones(self): - """ - Return the identifier items which may not exist, in the translation - database, and are needed for a msg_string. These must exist, even - even if not written from the database. - - Returns two dictionaries to use as the start point for - population from the database. - - """ - sourceid = {} - targetid = {} - return sourceid, targetid - - def is_cf(self, comp): - """ - Determines whether the provided component from a mapping - represents a simple CF component of the given kind. - - Args: - * component: - A :class:`metarelate.Component` or - :class:`metarelate.Component` instance. - - Returns: - Boolean. - - """ - kind = FORMAT_URIS['cff'] - result = False - result = hasattr(comp, 'com_type') and \ - comp.com_type == kind and \ - hasattr(comp, 'units') and \ - len(comp) in [1, 2] - return result - - def is_cf_constrained(self, comp): - """ - Determines whether the provided component from a mapping - represents a compound CF component for a phenomenon and - one, single valued dimension coordinate. - - Args: - * component: - A :class:`metarelate.Component` instance. - - Returns: - Boolean. - - """ - ftype = FORMAT_URIS['cff'] - result = False - cffield = hasattr(comp, 'com_type') and comp.com_type == ftype and \ - hasattr(comp, 'units') and (hasattr(comp, 'standard_name') or - hasattr(comp, 'long_name')) - dimcoord = hasattr(comp, 'dim_coord') and \ - isinstance(comp.dim_coord, metarelate.ComponentProperty) and \ - comp.dim_coord.component.com_type.notation == 'DimCoord' - result = cffield and dimcoord - return result - - def is_cf_height_constrained(self, comp): - item_sn = metarelate.Item((''), - 'standard_name') - item_h = metarelate.Item((''), - 'height') - snprop = metarelate.StatementProperty(item_sn, item_h) - item_u = metarelate.Item((''), - 'units') - uprop = metarelate.StatementProperty(item_u, - metarelate.Item('"m"', 'm')) - pts_pred = metarelate.Item((''), - 'points') - result = False - if self.is_cf_constrained(comp): - props = comp.dim_coord.component.properties - if len(props) == 3: - if snprop in props and uprop in props: - preds = [prop.predicate for prop in props] - if pts_pred in preds: - result = True - return result - - def is_fieldcode(self, component): - """ - Determines whether the provided concept from a mapping - represents a simple UM concept for a field-code. - - Args: - * concept: - A :class:`metarelate.Component` instance. - - Returns: - Boolean. - - """ - result = False - result = hasattr(component, 'lbfc') and len(component) == 1 - return result - - def is_grib1_local_param(self, component): - """ - Determines whether the provided component from a mapping - represents a simple GRIB edition 1 component for a local - parameter. - - Args: - * component: - A :class:`metarelate.Component` instance. - - Returns: - Boolean. - - """ - result = len(component) == 1 and hasattr(component, 'grib1_parameter') - return result - - def is_grib2_param(self, component): - """ - Determines whether the provided component from a mapping - represents a simple GRIB edition 2 component for a parameter. - - Args: - * component: - A :class:`metarelate.Component` instance. - - Returns: - Boolean. - - """ - - result = len(component) == 1 and hasattr(component, 'grib2_parameter') - return result - - def is_stash(self, component): - """ - Determines whether the provided concept for a mapping - represents a simple UM concept for a stash-code. - - Args: - * concept: - A :class:`metarelate.Component` instance. - - Returns: - Boolean. - - """ - result = False - result = hasattr(component, 'stash') and len(component) == 1 - return result - - -def _cfn(line): - """ - Helper function to parse dictionary lines using the CFName named tuple. - Matches to the line ' CFName({standard_name}, {long_name}, {units}:*) - giving access to these named parts - - """ - match = re.match('^ CFName\((.+), (.+), (.+)\):.+,', line) - if match is None: - raise ValueError('encoding not sortable') - standard_name, long_name, units = match.groups() - if standard_name == 'None': - standard_name = None - if long_name == 'None': - long_name = None - return [standard_name, long_name, units] - - -class CFFieldcodeMappings(Mappings): - """ - Represents a container for CF phenomenon to UM field-code metarelate - mapping translations. - - Encoding support is provided to generate the Python dictionary source - code representation of these mappings from CF standard name, long name, - and units to UM field-code. - - """ - def _key(self, line): - """Provides the sort key of the mappings order.""" - return _cfn(line) - - def msg_strings(self): - return (' CFName({standard_name!r}, {long_name!r}, ' - '{units!r})', - '{lbfc},\n') - - def get_initial_id_nones(self): - sourceid = {'standard_name': None, 'long_name': None} - targetid = {} - return sourceid, targetid - - @property - def mapping_name(self): - """ - Property that specifies the name of the dictionary to contain the - encoding of this metarelate mapping translation. - - """ - return 'CF_TO_LBFC' - - @property - def source_scheme(self): - """ - Property that specifies the name of the scheme for the source - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['cff'] - - @property - def target_scheme(self): - """ - Property that specifies the name of the scheme for the target - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['umf'] - - def valid_mapping(self, mapping): - """ - Determine whether the provided :class:`metarelate.Mapping` represents a - CF to UM field-code translation. - - Args: - * mapping: - A :class:`metarelate.Mapping` instance. - - Returns: - Boolean. - - """ - return self.is_cf(mapping.source) and self.is_fieldcode(mapping.target) - - -class FieldcodeCFMappings(Mappings): - """ - Represents a container for UM field-code to CF phenomenon metarelate - mapping translations. - - Encoding support is provided to generate the Python dictionary source - code representation of these mappings from UM field-code to - CF standard name, long name, and units. - - """ - def _key(self, line): - """Provides the sort key of the mappings order.""" - return int(line.split(':')[0].strip()) - - def msg_strings(self): - return (' {lbfc}', - 'CFName({standard_name!r}, {long_name!r}, {units!r}),\n') - - def get_initial_id_nones(self): - sourceid = {} - targetid = {'standard_name': None, 'long_name': None} - return sourceid, targetid - - @property - def mapping_name(self): - """ - Property that specifies the name of the dictionary to contain the - encoding of this metarelate mapping translation. - - """ - return 'LBFC_TO_CF' - - @property - def source_scheme(self): - """ - Property that specifies the name of the scheme for the source - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['umf'] - - @property - def target_scheme(self): - """ - Property that specifies the name of the scheme for the target - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['cff'] - - def valid_mapping(self, mapping): - """ - Determine whether the provided :class:`metarelate.Mapping` represents a - UM field-code to CF translation. - - Args: - * mapping: - A :class:`metarelate.Mapping` instance. - - Returns: - Boolean. - - """ - return self.is_fieldcode(mapping.source) and self.is_cf(mapping.target) - - -class StashCFNameMappings(Mappings): - """ - Represents a container for UM stash-code to CF phenomenon metarelate - mapping translations. - - Encoding support is provided to generate the Python dictionary source - code representation of these mappings from UM stash-code to CF - standard name, long name, and units. - - """ - def _key(self, line): - """Provides the sort key of the mappings order.""" - return line.split(':')[0].strip() - - def msg_strings(self): - return(' {stash!r}', - 'CFName({standard_name!r}, ' - '{long_name!r}, {units!r}),\n') - - def get_initial_id_nones(self): - sourceid = {} - targetid = {'standard_name': None, 'long_name': None} - return sourceid, targetid - - @property - def mapping_name(self): - """ - Property that specifies the name of the dictionary to contain the - encoding of this metarelate mapping translation. - - """ - return 'STASH_TO_CF' - - @property - def source_scheme(self): - """ - Property that specifies the name of the scheme for the source - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['umf'] - - @property - def target_scheme(self): - """ - Property that specifies the name of the scheme for the target - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['cff'] - - def valid_mapping(self, mapping): - """ - Determine whether the provided :class:`metarelate.Mapping` represents a - UM stash-code to CF translation. - - Args: - * mapping: - A :class:`metarelate.Mapping` instance. - - Returns: - Boolean. - - """ - return (self.is_stash(mapping.source) and - (self.is_cf(mapping.target) or - self.is_cf_constrained(mapping.target))) - - -class StashCFHeightConstraintMappings(Mappings): - """ - Represents a container for UM stash-code to CF phenomenon metarelate - mapping translations where a singular height constraint is defined by - the STASH code. - - Encoding support is provided to generate the Python dictionary source - code representation of these mappings from UM stash-code to CF - standard name, long name, and units. - - """ - def _key(self, line): - """Provides the sort key of the mappings order.""" - return line.split(':')[0].strip() - - def msg_strings(self): - return(' {stash!r}', - '{dim_coord[points]},\n') - - def get_initial_id_nones(self): - sourceid = {} - targetid = {} - return sourceid, targetid - - @property - def mapping_name(self): - """ - Property that specifies the name of the dictionary to contain the - encoding of this metarelate mapping translation. - - """ - return 'STASHCODE_IMPLIED_HEIGHTS' - - @property - def source_scheme(self): - """ - Property that specifies the name of the scheme for the source - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['umf'] - - @property - def target_scheme(self): - """ - Property that specifies the name of the scheme for the target - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['cff'] - - def valid_mapping(self, mapping): - """ - Determine whether the provided :class:`metarelate.Mapping` represents a - UM stash-code to CF translation. - - Args: - * mapping: - A :class:`metarelate.Mapping` instance. - - Returns: - Boolean. - - """ - return (self.is_stash(mapping.source) and - self.is_cf_height_constrained(mapping.target)) - - -class GRIB1LocalParamCFMappings(Mappings): - """ - Represents a container for GRIB (edition 1) local parameter to - CF phenomenon metarelate mapping translations. - - Encoding support is provided to generate the Python dictionary source - code representation of these mappings from GRIB1 edition, table II version, - centre and indicator of parameter to CF standard name, long name and units. - - """ - def _key(self, line): - """Provides the sort key of the mappings order.""" - matchstr = ('^ G1LocalParam\(([0-9]+), ([0-9]+), ' - '([0-9]+), ([0-9]+)\):.*') - match = re.match(matchstr, line) - if match is None: - raise ValueError('encoding not sortable') - return [int(i) for i in match.groups()] - - def msg_strings(self): - return (' G1LocalParam({editionNumber}, {table2version}, ' - '{centre}, {indicatorOfParameter})', - 'CFName({standard_name!r}, ' - '{long_name!r}, {units!r}),\n') - - def get_initial_id_nones(self): - sourceid = {} - targetid = {'standard_name': None, 'long_name': None} - return sourceid, targetid - - @property - def mapping_name(self): - """ - Property that specifies the name of the dictionary to contain the - encoding of this metarelate mapping translation. - - """ - return 'GRIB1_LOCAL_TO_CF' - - @property - def source_scheme(self): - """ - Property that specifies the name of the scheme for the source - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['gribm'] - - @property - def target_scheme(self): - """ - Property that specifies the name of the scheme for the target - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['cff'] - - def valid_mapping(self, mapping): - """ - Determine whether the provided :class:`metarelate.Mapping` represents a - GRIB1 local parameter to CF phenomenon translation. - - Args: - * mapping: - A :class:`metarelate.Mapping` instance. - - Returns: - Boolean. - - """ - return self.is_grib1_local_param(mapping.source) and \ - self.is_cf(mapping.target) - - -class CFGRIB1LocalParamMappings(Mappings): - """ - Represents a container for CF phenomenon to GRIB (edition 1) local - parameter metarelate mapping translations. - - Encoding support is provided to generate the Python dictionary source - code representation of these mappings from CF standard name, long name - and units to GRIB1 edition, table II version, centre and indicator of - parameter. - - """ - def _key(self, line): - """Provides the sort key of the mappings order.""" - return _cfn(line) - - def msg_strings(self): - return (' CFName({standard_name!r}, {long_name!r}, ' - '{units!r})', - 'G1LocalParam({editionNumber}, {table2version}, ' - '{centre}, {indicatorOfParameter}),\n') - - def get_initial_id_nones(self): - sourceid = {'standard_name': None, 'long_name': None} - targetid = {} - return sourceid, targetid - - @property - def mapping_name(self): - """ - Property that specifies the name of the dictionary to contain the - encoding of this metarelate mapping translation. - - """ - return 'CF_TO_GRIB1_LOCAL' - - @property - def source_scheme(self): - """ - Property that specifies the name of the scheme for the source - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['cff'] - - @property - def target_scheme(self): - """ - Property that specifies the name of the scheme for the target - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['gribm'] - - def valid_mapping(self, mapping): - """ - Determine whether the provided :class:`metarelate.Mapping` represents a - CF phenomenon to GRIB1 local parameter translation. - - Args: - * mapping: - A :class:`metarelate.Mapping` instance. - - Returns: - Boolean. - - """ - return self.is_cf(mapping.source) and \ - self.is_grib1_local_param(mapping.target) - - -class GRIB1LocalParamCFConstrainedMappings(Mappings): - """ - Represents a container for GRIB (edition 1) local parameter to - CF phenomenon and dimension coordinate constraint metarelate mapping - translations. - - Encoding support is provided to generate the Python dictionary source - code representation of these mappings from GRIB1 edition, table II version, - centre and indicator of parameter to CF phenomenon standard name, long name - and units, and CF dimension coordinate standard name, units and points. - - """ - def _key(self, line): - """Provides the sort key of the mappings order.""" - return line.split(':')[0].strip() - - def msg_strings(self): - return (' G1LocalParam({editionNumber}, {table2version}, ' - '{centre}, {indicatorOfParameter})', - '(CFName({standard_name!r}, ' - '{long_name!r}, {units!r}), ' - 'DimensionCoordinate({dim_coord[standard_name]!r}, ' - '{dim_coord[units]!r}, {dim_coord[points]})),\n') - - def get_initial_id_nones(self): - sourceid = {} - targetid = {'standard_name': None, 'long_name': None} - return sourceid, targetid - - @property - def mapping_name(self): - """ - Property that specifies the name of the dictionary to contain the - encoding of this metarelate mapping translation. - - """ - return 'GRIB1_LOCAL_TO_CF_CONSTRAINED' - - @property - def source_scheme(self): - """ - Property that specifies the name of the scheme for the source - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['gribm'] - - @property - def target_scheme(self): - """ - Property that specifies the name of the scheme for the target - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['cff'] - - def valid_mapping(self, mapping): - """ - Determine whether the provided :class:`metarelate.Mapping` represents a - GRIB1 local parameter to CF phenomenon and dimension coordinate - translation. - - Args: - * mapping: - A :class:`metarelate.Mapping` instance. - - Returns: - Boolean. - - """ - return self.is_grib1_local_param(mapping.source) and \ - self.is_cf_constrained(mapping.target) - - -class CFConstrainedGRIB1LocalParamMappings(Mappings): - """ - Represents a container for CF phenomenon and dimension coordinate - constraint to GRIB (edition 1) local parameter metarelate mapping - translations. - - Encoding support is provided to generate the Python dictionary source - code representation of these mappings from CF phenomenon standard name, - long name and units, and CF dimension coordinate standard name, units and - points to GRIB1 edition, table II version, centre and indicator of - parameter. - - """ - def _key(self, line): - """Provides the sort key of the mappings order.""" - return line.split(':')[0].strip() - - def msg_strings(self): - return (' (CFName({standard_name!r}, ' - '{long_name!r}, {units!r}), ' - 'DimensionCoordinate({dim_coord[standard_name]!r}, ' - '{dim_coord[units]!r}, {dim_coord[points]}))', - 'G1LocalParam({editionNumber}, {table2version}, ' - '{centre}, {indicatorOfParameter}),\n') - - def get_initial_id_nones(self): - sourceid = {'standard_name': None, 'long_name': None} - targetid = {} - return sourceid, targetid - - @property - def mapping_name(self): - """ - Property that specifies the name of the dictionary to contain the - encoding of this metarelate mapping translation. - - """ - return 'CF_CONSTRAINED_TO_GRIB1_LOCAL' - - @property - def source_scheme(self): - """ - Property that specifies the name of the scheme for the source - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['cff'] - - @property - def target_scheme(self): - """ - Property that specifies the name of the scheme for the target - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['gribm'] - - def valid_mapping(self, mapping): - """ - Determine whether the provided :class:`metarelate.Mapping` represents a - CF phenomenon and dimension coordinate to GRIB1 local parameter - translation. - - Args: - * mapping: - A :class:`metarelate.Mapping` instance. - - Returns: - Boolean. - - """ - return self.is_cf_constrained(mapping.source) and \ - self.is_grib1_local_param(mapping.target) - - -class GRIB2ParamCFMappings(Mappings): - """ - Represents a container for GRIB (edition 2) parameter to CF phenomenon - metarelate mapping translations. - - Encoding support is provided to generate the Python dictionary source - code representation of these mappings from GRIB2 edition, discipline, - parameter category and indicator of parameter to CF standard name, - long name and units. - - """ - def _key(self, line): - """Provides the sort key of the mappings order.""" - matchstr = ('^ G2Param\(([0-9]+), ([0-9]+), ([0-9]+), ' - '([0-9]+)\):.*') - match = re.match(matchstr, line) - if match is None: - raise ValueError('encoding not sortable') - return [int(i) for i in match.groups()] - - def msg_strings(self): - return (' G2Param({editionNumber}, {discipline}, ' - '{parameterCategory}, {parameterNumber})', - 'CFName({standard_name!r}, {long_name!r}, ' - '{units!r}),\n') - - def get_initial_id_nones(self): - sourceid = {} - targetid = {'standard_name': None, 'long_name': None} - return sourceid, targetid - - @property - def mapping_name(self): - """ - Property that specifies the name of the dictionary to contain the - encoding of this metarelate mapping translation. - - """ - return 'GRIB2_TO_CF' - - @property - def source_scheme(self): - """ - Property that specifies the name of the scheme for the source - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['gribm'] - - @property - def target_scheme(self): - """ - Property that specifies the name of the scheme for the target - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['cff'] - - def valid_mapping(self, mapping): - """ - Determine whether the provided :class:`metarelate.Mapping` represents a - GRIB2 parameter to CF phenomenon translation. - - Args: - * mapping: - A :class:`metarelate.Mapping` instance. - - Returns: - Boolean. - - """ - return self.is_grib2_param(mapping.source) and \ - self.is_cf(mapping.target) - - -class CFGRIB2ParamMappings(Mappings): - """ - Represents a container for CF phenomenon to GRIB (edition 2) parameter - metarelate mapping translations. - - Encoding support is provided to generate the Python dictionary source - code representation of these mappings from CF standard name, long name - and units to GRIB2 edition, discipline, parameter category and indicator - of parameter. - - """ - def _key(self, line): - """Provides the sort key of the mappings order.""" - return _cfn(line) - - def msg_strings(self): - return (' CFName({standard_name!r}, {long_name!r}, ' - '{units!r})', - 'G2Param({editionNumber}, {discipline}, ' - '{parameterCategory}, {parameterNumber}),\n') - - def get_initial_id_nones(self): - sourceid = {'standard_name': None, 'long_name': None} - targetid = {} - return sourceid, targetid - - @property - def mapping_name(self): - """ - Property that specifies the name of the dictionary to contain the - encoding of this metarelate mapping translation. - - """ - return 'CF_TO_GRIB2' - - @property - def source_scheme(self): - """ - Property that specifies the name of the scheme for the source - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['cff'] - - @property - def target_scheme(self): - """ - Property that specifies the name of the scheme for the target - :class:`metarelate.Component` defining this metarelate mapping - translation. - - """ - return FORMAT_URIS['gribm'] - - def valid_mapping(self, mapping): - """ - Determine whether the provided :class:`metarelate.Mapping` represents a - CF phenomenon to GRIB2 parameter translation. - - Args: - * mapping: - A :class:`metarelate.Mapping` instance. - - Returns: - Boolean. - - """ - return self.is_cf(mapping.source) and \ - self.is_grib2_param(mapping.target) From 585f2669d474bb2d22a478693a41a436e24d1081 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 10 Feb 2022 16:42:08 +0000 Subject: [PATCH 16/22] Revert "[pre-commit.ci] pre-commit autoupdate (#4560)" This reverts commit d503ce62c41e9d827b91121e4f6395d1fcb6b644. --- .pre-commit-config.yaml | 4 +- benchmarks/benchmarks/plot.py | 2 +- .../meteorology/plot_wind_barbs.py | 4 +- .../meteorology/plot_wind_speed.py | 2 +- docs/src/whatsnew/{3.2.rst => dev.rst} | 0 docs/src/whatsnew/dev.rst.template | 2 +- docs/src/whatsnew/index.rst | 2 +- docs/src/whatsnew/latest.rst | 2 +- lib/iris/analysis/__init__.py | 2 +- lib/iris/analysis/_grid_angles.py | 2 +- lib/iris/analysis/_scipy_interpolate.py | 2 +- lib/iris/analysis/calculus.py | 12 ++++-- lib/iris/analysis/cartography.py | 6 +-- lib/iris/analysis/maths.py | 2 +- lib/iris/analysis/stats.py | 4 +- lib/iris/fileformats/netcdf.py | 4 +- lib/iris/fileformats/pp.py | 4 +- lib/iris/tests/integration/test_netcdf.py | 6 +-- lib/iris/tests/test_basic_maths.py | 38 +++++++++---------- .../analysis/cartography/test_rotate_winds.py | 8 ++-- .../regrid/test_RectilinearRegridder.py | 2 +- .../test_add_categorised_coord.py | 2 +- ...__collapse_degenerate_points_and_bounds.py | 2 +- 23 files changed, 59 insertions(+), 55 deletions(-) rename docs/src/whatsnew/{3.2.rst => dev.rst} (100%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 228970bee2..97dff666cf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: no-commit-to-branch - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 21.12b0 hooks: - id: black pass_filenames: false @@ -50,7 +50,7 @@ repos: args: [--filter-files] - repo: https://github.com/asottile/blacken-docs - rev: v1.12.1 + rev: v1.12.0 hooks: - id: blacken-docs types: [file, rst] diff --git a/benchmarks/benchmarks/plot.py b/benchmarks/benchmarks/plot.py index 24899776dc..45905abd2f 100644 --- a/benchmarks/benchmarks/plot.py +++ b/benchmarks/benchmarks/plot.py @@ -22,7 +22,7 @@ def setup(self): # Should generate 10 distinct contours, regardless of dim size. dim_size = int(ARTIFICIAL_DIM_SIZE / 5) repeat_number = int(dim_size / 10) - repeat_range = range(int((dim_size**2) / repeat_number)) + repeat_range = range(int((dim_size ** 2) / repeat_number)) data = np.repeat(repeat_range, repeat_number) data = data.reshape((dim_size,) * 2) diff --git a/docs/gallery_code/meteorology/plot_wind_barbs.py b/docs/gallery_code/meteorology/plot_wind_barbs.py index b09040c64e..c3c056eb4a 100644 --- a/docs/gallery_code/meteorology/plot_wind_barbs.py +++ b/docs/gallery_code/meteorology/plot_wind_barbs.py @@ -30,7 +30,7 @@ def main(): # To illustrate the full range of barbs, scale the wind speed up to pretend # that a storm is passing over - magnitude = (uwind**2 + vwind**2) ** 0.5 + magnitude = (uwind ** 2 + vwind ** 2) ** 0.5 magnitude.convert_units("knot") max_speed = magnitude.collapsed( ("latitude", "longitude"), iris.analysis.MAX @@ -41,7 +41,7 @@ def main(): vwind = vwind / max_speed * max_desired # Create a cube containing the wind speed - windspeed = (uwind**2 + vwind**2) ** 0.5 + windspeed = (uwind ** 2 + vwind ** 2) ** 0.5 windspeed.rename("windspeed") windspeed.convert_units("knot") diff --git a/docs/gallery_code/meteorology/plot_wind_speed.py b/docs/gallery_code/meteorology/plot_wind_speed.py index 40d9d0da00..fd03f54205 100644 --- a/docs/gallery_code/meteorology/plot_wind_speed.py +++ b/docs/gallery_code/meteorology/plot_wind_speed.py @@ -27,7 +27,7 @@ def main(): vwind = iris.load_cube(infile, "y_wind") # Create a cube containing the wind speed. - windspeed = (uwind**2 + vwind**2) ** 0.5 + windspeed = (uwind ** 2 + vwind ** 2) ** 0.5 windspeed.rename("windspeed") # Plot the wind speed as a contour plot. diff --git a/docs/src/whatsnew/3.2.rst b/docs/src/whatsnew/dev.rst similarity index 100% rename from docs/src/whatsnew/3.2.rst rename to docs/src/whatsnew/dev.rst diff --git a/docs/src/whatsnew/dev.rst.template b/docs/src/whatsnew/dev.rst.template index 1b36d3f0b0..7c10e9ba29 100644 --- a/docs/src/whatsnew/dev.rst.template +++ b/docs/src/whatsnew/dev.rst.template @@ -42,7 +42,7 @@ v3.X.X (DD MMM YYYY) NOTE: section above is a template for bugfix patches ==================================================== (Please remove this section when creating an initial 'dev.rst') - + 📢 Announcements diff --git a/docs/src/whatsnew/index.rst b/docs/src/whatsnew/index.rst index f425e649b9..51f03e8d8f 100644 --- a/docs/src/whatsnew/index.rst +++ b/docs/src/whatsnew/index.rst @@ -10,7 +10,7 @@ Iris versions. .. toctree:: :maxdepth: 1 - 3.2.rst + dev.rst 3.1.rst 3.0.rst 2.4.rst diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 2bdbea5d85..56aebe92dd 120000 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -1 +1 @@ -3.2.rst \ No newline at end of file +dev.rst \ No newline at end of file diff --git a/lib/iris/analysis/__init__.py b/lib/iris/analysis/__init__.py index b1a9e1d259..465a521065 100644 --- a/lib/iris/analysis/__init__.py +++ b/lib/iris/analysis/__init__.py @@ -1394,7 +1394,7 @@ def _lazy_rms(array, axis, **kwargs): # all. Thus trying to use this aggregator with weights will currently # raise an error in dask due to the unexpected keyword `weights`, # rather than silently returning the wrong answer. - return da.sqrt(da.mean(array**2, axis=axis, **kwargs)) + return da.sqrt(da.mean(array ** 2, axis=axis, **kwargs)) @_build_dask_mdtol_function diff --git a/lib/iris/analysis/_grid_angles.py b/lib/iris/analysis/_grid_angles.py index 0b52f54568..127aec7c1e 100644 --- a/lib/iris/analysis/_grid_angles.py +++ b/lib/iris/analysis/_grid_angles.py @@ -120,7 +120,7 @@ def _angle(p, q, r): mid_lons = np.deg2rad(q[0]) pr = _3d_xyz_from_latlon(r[0], r[1]) - _3d_xyz_from_latlon(p[0], p[1]) - pr_norm = np.sqrt(np.sum(pr**2, axis=0)) + pr_norm = np.sqrt(np.sum(pr ** 2, axis=0)) pr_top = pr[1] * np.cos(mid_lons) - pr[0] * np.sin(mid_lons) index = pr_norm == 0 diff --git a/lib/iris/analysis/_scipy_interpolate.py b/lib/iris/analysis/_scipy_interpolate.py index fc64249729..c6b33c56a4 100644 --- a/lib/iris/analysis/_scipy_interpolate.py +++ b/lib/iris/analysis/_scipy_interpolate.py @@ -229,7 +229,7 @@ def compute_interp_weights(self, xi, method=None): xi_shape, method, indices, norm_distances, out_of_bounds = prepared # Allocate arrays for describing the sparse matrix. - n_src_values_per_result_value = 2**ndim + n_src_values_per_result_value = 2 ** ndim n_result_values = len(indices[0]) n_non_zero = n_result_values * n_src_values_per_result_value weights = np.ones(n_non_zero, dtype=norm_distances[0].dtype) diff --git a/lib/iris/analysis/calculus.py b/lib/iris/analysis/calculus.py index 4630f47967..409782f256 100644 --- a/lib/iris/analysis/calculus.py +++ b/lib/iris/analysis/calculus.py @@ -629,10 +629,14 @@ def curl(i_cube, j_cube, k_cube=None): # (d/dtheta (i_cube * sin(lat)) - d_j_cube_dphi) # phi_cmpt = 1/r * ( d/dr (r * j_cube) - d_k_cube_dtheta) # theta_cmpt = 1/r * ( 1/cos(lat) * d_k_cube_dphi - d/dr (r * i_cube) - if y_coord.name() not in [ - "latitude", - "grid_latitude", - ] or x_coord.name() not in ["longitude", "grid_longitude"]: + if ( + y_coord.name() + not in [ + "latitude", + "grid_latitude", + ] + or x_coord.name() not in ["longitude", "grid_longitude"] + ): raise ValueError( "Expecting latitude as the y coord and " "longitude as the x coord for spherical curl." diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index f704468e33..373487af53 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -335,7 +335,7 @@ def _quadrant_area(radian_lat_bounds, radian_lon_bounds, radius_of_earth): raise ValueError("Bounds must be [n,2] array") # fill in a new array of areas - radius_sqr = radius_of_earth**2 + radius_sqr = radius_of_earth ** 2 radian_lat_64 = radian_lat_bounds.astype(np.float64) radian_lon_64 = radian_lon_bounds.astype(np.float64) @@ -1010,8 +1010,8 @@ def _transform_distance_vectors_tolerance_mask( # Squared magnitudes should be equal to one within acceptable tolerance. # A value of atol=2e-3 is used, which corresponds to a change in magnitude # of approximately 0.1%. - sqmag_1_0 = u_one_t**2 + v_zero_t**2 - sqmag_0_1 = u_zero_t**2 + v_one_t**2 + sqmag_1_0 = u_one_t ** 2 + v_zero_t ** 2 + sqmag_0_1 = u_zero_t ** 2 + v_one_t ** 2 mask = np.logical_not( np.logical_and( np.isclose(sqmag_1_0, ones, atol=2e-3), diff --git a/lib/iris/analysis/maths.py b/lib/iris/analysis/maths.py index 1cbc90cc60..107d964ed4 100644 --- a/lib/iris/analysis/maths.py +++ b/lib/iris/analysis/maths.py @@ -540,7 +540,7 @@ def power(data, out=None): return _math_op_common( cube, power, - cube.units**exponent, + cube.units ** exponent, new_dtype=new_dtype, in_place=in_place, ) diff --git a/lib/iris/analysis/stats.py b/lib/iris/analysis/stats.py index 711e3c5bfb..89dde1818b 100644 --- a/lib/iris/analysis/stats.py +++ b/lib/iris/analysis/stats.py @@ -168,10 +168,10 @@ def _ones_like(cube): covar = (s1 * s2).collapsed( corr_coords, iris.analysis.SUM, weights=weights_1, mdtol=mdtol ) - var_1 = (s1**2).collapsed( + var_1 = (s1 ** 2).collapsed( corr_coords, iris.analysis.SUM, weights=weights_1 ) - var_2 = (s2**2).collapsed( + var_2 = (s2 ** 2).collapsed( corr_coords, iris.analysis.SUM, weights=weights_2 ) diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 73a137b4af..100ab29daa 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -2738,9 +2738,9 @@ def _create_cf_data_variable( cmin, cmax = _co_realise_lazy_arrays([cmin, cmax]) n = dtype.itemsize * 8 if masked: - scale_factor = (cmax - cmin) / (2**n - 2) + scale_factor = (cmax - cmin) / (2 ** n - 2) else: - scale_factor = (cmax - cmin) / (2**n - 1) + scale_factor = (cmax - cmin) / (2 ** n - 1) if dtype.kind == "u": add_offset = cmin elif dtype.kind == "i": diff --git a/lib/iris/fileformats/pp.py b/lib/iris/fileformats/pp.py index 9bda98bf61..9f213ec4db 100644 --- a/lib/iris/fileformats/pp.py +++ b/lib/iris/fileformats/pp.py @@ -403,7 +403,7 @@ def _calculate_str_value_from_value(self): def _calculate_value_from_str_value(self): self._value = np.sum( - [10**i * val for i, val in enumerate(self._strvalue)] + [10 ** i * val for i, val in enumerate(self._strvalue)] ) def __len__(self): @@ -418,7 +418,7 @@ def __getitem__(self, key): # if the key returns a list of values, then combine them together # to an integer if isinstance(val, list): - val = sum([10**i * val for i, val in enumerate(val)]) + val = sum([10 ** i * val for i, val in enumerate(val)]) return val diff --git a/lib/iris/tests/integration/test_netcdf.py b/lib/iris/tests/integration/test_netcdf.py index 2a45561e17..f7aaa1d05c 100644 --- a/lib/iris/tests/integration/test_netcdf.py +++ b/lib/iris/tests/integration/test_netcdf.py @@ -416,7 +416,7 @@ def setUp(self): levels.units = "centimeters" levels.positive = "down" levels.axis = "Z" - levels[:] = np.linspace(0, 10**5, 3) + levels[:] = np.linspace(0, 10 ** 5, 3) volcello.id = "volcello" volcello.out_name = "volcello" @@ -507,9 +507,9 @@ def _get_scale_factor_add_offset(cube, datatype): else: masked = False if masked: - scale_factor = (cmax - cmin) / (2**n - 2) + scale_factor = (cmax - cmin) / (2 ** n - 2) else: - scale_factor = (cmax - cmin) / (2**n - 1) + scale_factor = (cmax - cmin) / (2 ** n - 1) if dt.kind == "u": add_offset = cmin elif dt.kind == "i": diff --git a/lib/iris/tests/test_basic_maths.py b/lib/iris/tests/test_basic_maths.py index 24f2b89442..e753adbae8 100644 --- a/lib/iris/tests/test_basic_maths.py +++ b/lib/iris/tests/test_basic_maths.py @@ -249,7 +249,7 @@ def test_apply_ufunc(self): np.square, a, new_name="squared temperature", - new_unit=a.units**2, + new_unit=a.units ** 2, in_place=False, ) self.assertCMLApproxData(a, ("analysis", "apply_ufunc_original.cml")) @@ -259,14 +259,14 @@ def test_apply_ufunc(self): np.square, a, new_name="squared temperature", - new_unit=a.units**2, + new_unit=a.units ** 2, in_place=True, ) self.assertCMLApproxData(b, ("analysis", "apply_ufunc.cml")) self.assertCMLApproxData(a, ("analysis", "apply_ufunc.cml")) def vec_mag(u, v): - return math.sqrt(u**2 + v**2) + return math.sqrt(u ** 2 + v ** 2) c = a.copy() + 2 @@ -295,7 +295,7 @@ def test_apply_ufunc_fail(self): def test_ifunc(self): a = self.cube - my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units**2) + my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units ** 2) b = my_ifunc(a, new_name="squared temperature", in_place=False) self.assertCMLApproxData(a, ("analysis", "apply_ifunc_original.cml")) @@ -307,7 +307,7 @@ def test_ifunc(self): self.assertCMLApproxData(a, ("analysis", "apply_ifunc.cml")) def vec_mag(u, v): - return math.sqrt(u**2 + v**2) + return math.sqrt(u ** 2 + v ** 2) c = a.copy() + 2 @@ -347,7 +347,7 @@ def test_ifunc_init_fail(self): def test_ifunc_call_fail(self): a = self.cube - my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units**2) + my_ifunc = iris.analysis.maths.IFunc(np.square, lambda a: a.units ** 2) # should now NOT fail because giving 2 arguments to an ifunc that # expects only one will now ignore the surplus argument and raise @@ -367,7 +367,7 @@ def test_ifunc_call_fail(self): my_ifunc(a) my_ifunc = iris.analysis.maths.IFunc( - lambda a: (a, a**2.0), lambda cube: cf_units.Unit("1") + lambda a: (a, a ** 2.0), lambda cube: cf_units.Unit("1") ) # should fail because data function returns a tuple @@ -553,9 +553,9 @@ def test_square_root(self): a.data = abs(a.data) a.units **= 2 - e = a**0.5 + e = a ** 0.5 - self.assertArrayAllClose(e.data, a.data**0.5) + self.assertArrayAllClose(e.data, a.data ** 0.5) self.assertCML(e, ("analysis", "sqrt.cml"), checksum=False) self.assertRaises(ValueError, iris.analysis.maths.exponentiate, a, 0.3) @@ -585,26 +585,26 @@ def test_apply_ufunc(self): np.square, a, new_name="more_thingness", - new_unit=a.units**2, + new_unit=a.units ** 2, in_place=False, ) - ans = a.data**2 + ans = a.data ** 2 self.assertArrayEqual(b.data, ans) self.assertEqual(b.name(), "more_thingness") self.assertEqual(b.units, cf_units.Unit("m^2")) def vec_mag(u, v): - return math.sqrt(u**2 + v**2) + return math.sqrt(u ** 2 + v ** 2) c = a.copy() + 2 vec_mag_ufunc = np.frompyfunc(vec_mag, 2, 1) b = iris.analysis.maths.apply_ufunc(vec_mag_ufunc, a, c) - ans = a.data**2 + c.data**2 - b2 = b**2 + ans = a.data ** 2 + c.data ** 2 + b2 = b ** 2 self.assertArrayAlmostEqual(b2.data, ans) @@ -617,17 +617,17 @@ def test_ifunc(self): a = self.cube a.units = cf_units.Unit("meters") - my_ifunc = iris.analysis.maths.IFunc(np.square, lambda x: x.units**2) + my_ifunc = iris.analysis.maths.IFunc(np.square, lambda x: x.units ** 2) b = my_ifunc(a, new_name="more_thingness", in_place=False) - ans = a.data**2 + ans = a.data ** 2 self.assertArrayEqual(b.data, ans) self.assertEqual(b.name(), "more_thingness") self.assertEqual(b.units, cf_units.Unit("m^2")) def vec_mag(u, v): - return math.sqrt(u**2 + v**2) + return math.sqrt(u ** 2 + v ** 2) c = a.copy() + 2 @@ -637,12 +637,12 @@ def vec_mag(u, v): ) b = my_ifunc(a, c) - ans = (a.data**2 + c.data**2) ** 0.5 + ans = (a.data ** 2 + c.data ** 2) ** 0.5 self.assertArrayAlmostEqual(b.data, ans) def vec_mag_data_func(u_data, v_data): - return np.sqrt(u_data**2 + v_data**2) + return np.sqrt(u_data ** 2 + v_data ** 2) vec_mag_ifunc = iris.analysis.maths.IFunc( vec_mag_data_func, lambda a, b: (a + b).units diff --git a/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py b/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py index eafaa20ec8..9e3af90603 100644 --- a/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py +++ b/lib/iris/tests/unit/analysis/cartography/test_rotate_winds.py @@ -343,8 +343,8 @@ def test_orig_coords(self): def test_magnitude_preservation(self): u, v = self._uv_cubes_limited_extent() ut, vt = rotate_winds(u, v, iris.coord_systems.OSGB()) - orig_sq_mag = u.data**2 + v.data**2 - res_sq_mag = ut.data**2 + vt.data**2 + orig_sq_mag = u.data ** 2 + v.data ** 2 + res_sq_mag = ut.data ** 2 + vt.data ** 2 self.assertArrayAllClose(orig_sq_mag, res_sq_mag, rtol=5e-4) def test_data_values(self): @@ -437,9 +437,9 @@ def test_rotated_to_osgb(self): self.assertArrayEqual(expected_mask, vt.data.mask) # Check unmasked values have sufficiently small error in mag. - expected_mag = np.sqrt(u.data**2 + v.data**2) + expected_mag = np.sqrt(u.data ** 2 + v.data ** 2) # Use underlying data to ignore mask in calculation. - res_mag = np.sqrt(ut.data.data**2 + vt.data.data**2) + res_mag = np.sqrt(ut.data.data ** 2 + vt.data.data ** 2) # Calculate percentage error (note there are no zero magnitudes # so we can divide safely). anom = 100.0 * np.abs(res_mag - expected_mag) / expected_mag diff --git a/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py b/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py index a018507fb3..f0dba83748 100644 --- a/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py +++ b/lib/iris/tests/unit/analysis/regrid/test_RectilinearRegridder.py @@ -33,7 +33,7 @@ def setUp(self): self.xs, self.ys = np.meshgrid(self.x.points, self.y.points) def transformation(x, y): - return x + y**2 + return x + y ** 2 # Construct a function which adds dimensions to the 2D data array # so that we can test higher dimensional functionality. diff --git a/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py b/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py index 0c20f16f5a..b7c59ff566 100644 --- a/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py +++ b/lib/iris/tests/unit/coord_categorisation/test_add_categorised_coord.py @@ -36,7 +36,7 @@ def test_vectorise_call(self): # The reason we use numpy.vectorize is to support multi-dimensional # coordinate points. def fn(coord, v): - return v**2 + return v ** 2 with mock.patch( "numpy.vectorize", return_value=self.vectorised diff --git a/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py b/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py index c9c4821e0a..0f2a8a2d4b 100644 --- a/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py +++ b/lib/iris/tests/unit/fileformats/pp_load_rules/test__collapse_degenerate_points_and_bounds.py @@ -65,7 +65,7 @@ def test_3d(self): def test_multiple_odd_dims(self): # Test to ensure multiple collapsed dimensions don't interfere. # make a 5-D array where dimensions 0, 2 and 3 are degenerate. - array = np.arange(3**5).reshape([3] * 5) + array = np.arange(3 ** 5).reshape([3] * 5) array[1:] = array[0:1] array[:, :, 1:] = array[:, :, 0:1] array[:, :, :, 1:] = array[:, :, :, 0:1] From d9cef30618a2241f573dc7d00232d0258b668068 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 10 Feb 2022 16:43:09 +0000 Subject: [PATCH 17/22] Revert "[pre-commit.ci] auto fixes from pre-commit.com hooks" This reverts commit ca169af6028891ee446150eb1c4d7be44bfeea15. --- benchmarks/benchmarks/generate_data/um_files.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/benchmarks/generate_data/um_files.py b/benchmarks/benchmarks/generate_data/um_files.py index fe8bf6a246..8ad6777808 100644 --- a/benchmarks/benchmarks/generate_data/um_files.py +++ b/benchmarks/benchmarks/generate_data/um_files.py @@ -107,9 +107,9 @@ def add_field(level_: int, time_step_: int) -> None: six_rec = 20 / 3 three_rec = six_rec / 2 - new_field.blev = level_1**2 * six_rec - six_rec + new_field.blev = level_1 ** 2 * six_rec - six_rec new_field.brsvd1 = ( - level_1**2 * six_rec + (six_rec * level_1) - three_rec + level_1 ** 2 * six_rec + (six_rec * level_1) - three_rec ) brsvd2_simulated = np.linspace(0.995, 0, len_z) From 39f086c9fd4f926a2eea7140647e8c2251b2818d Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Thu, 10 Feb 2022 16:46:56 +0000 Subject: [PATCH 18/22] Fix inconsistency with upstream in dev.rst.template. --- docs/src/whatsnew/dev.rst.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/whatsnew/dev.rst.template b/docs/src/whatsnew/dev.rst.template index 7c10e9ba29..1b36d3f0b0 100644 --- a/docs/src/whatsnew/dev.rst.template +++ b/docs/src/whatsnew/dev.rst.template @@ -42,7 +42,7 @@ v3.X.X (DD MMM YYYY) NOTE: section above is a template for bugfix patches ==================================================== (Please remove this section when creating an initial 'dev.rst') - + 📢 Announcements From 9fbcb336393e19c6d2ed3146b14509eb5808e374 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Fri, 11 Feb 2022 15:53:37 +0000 Subject: [PATCH 19/22] Loading benchmark review clarifications. --- benchmarks/benchmarks/generate_data/__init__.py | 3 +++ benchmarks/benchmarks/generate_data/um_files.py | 4 ++-- noxfile.py | 6 +++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/benchmarks/benchmarks/generate_data/__init__.py b/benchmarks/benchmarks/generate_data/__init__.py index a635d3c103..a56f2e4623 100644 --- a/benchmarks/benchmarks/generate_data/__init__.py +++ b/benchmarks/benchmarks/generate_data/__init__.py @@ -37,7 +37,9 @@ ) raise ValueError(error) +# The default location of data files used in benchmarks. Used by CI. default_data_dir = (Path(__file__).parents[2] / ".data").resolve() +# Optionally override the default data location with environment variable. BENCHMARK_DATA = Path(environ.get("BENCHMARK_DATA", default_data_dir)) if BENCHMARK_DATA == default_data_dir: BENCHMARK_DATA.mkdir(exist_ok=True) @@ -46,6 +48,7 @@ raise ValueError(message) # Manual flag to allow the rebuilding of synthetic data. +# False forces a benchmark run to re-make all the data files. REUSE_DATA = True diff --git a/benchmarks/benchmarks/generate_data/um_files.py b/benchmarks/benchmarks/generate_data/um_files.py index 8ad6777808..5f9789d031 100644 --- a/benchmarks/benchmarks/generate_data/um_files.py +++ b/benchmarks/benchmarks/generate_data/um_files.py @@ -184,8 +184,8 @@ def create_um_files( """ Generate FF-based FF / PP / NetCDF files with specified shape and compression. - Saved to a directory for all files that shape. A dictionary of the saved - paths is returned. + All files representing a given shape are saved in a dedicated directory. A + dictionary of the saved paths is returned. """ # Self contained imports to avoid linting confusion with _create_um_files(). from . import BENCHMARK_DATA, REUSE_DATA, run_function_elsewhere diff --git a/noxfile.py b/noxfile.py index 820959060a..0600540c5b 100755 --- a/noxfile.py +++ b/noxfile.py @@ -321,20 +321,24 @@ def benchmarks(session: nox.sessions.Session, ci_mode: bool): print("Using existing data generation environment.") else: print("Setting up the data generation environment...") + # Get Nox to build an environment for the `tests` session, but don't + # run the session. Will re-use a cached environment if appropriate. session.run_always( "nox", "--session=tests", "--install-only", f"--python={session.python}", ) + # Find the environment built above, set it to be the data generation + # environment. data_gen_python = next( Path(".nox").rglob(f"tests*/bin/python{session.python}") ).resolve() session.env[data_gen_var] = data_gen_python - print("Installing Mule into data generation environment...") mule_dir = data_gen_python.parents[1] / "resources" / "mule" if not mule_dir.is_dir(): + print("Installing Mule into data generation environment...") session.run_always( "git", "clone", From 79a822867219acbfdcdd84ffc827d811b1d6918b Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Fri, 11 Feb 2022 16:37:24 +0000 Subject: [PATCH 20/22] ManyVars benchmark use setup_cache. --- benchmarks/benchmarks/loading.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/benchmarks/benchmarks/loading.py b/benchmarks/benchmarks/loading.py index 41320bc6bf..f59a2fcff7 100644 --- a/benchmarks/benchmarks/loading.py +++ b/benchmarks/benchmarks/loading.py @@ -116,6 +116,8 @@ def time_time_constraint(self, _, __, ___) -> None: class ManyVars: + FILE_PATH = BENCHMARK_DATA / "many_var_file.nc" + @staticmethod def _create_file(save_path: str) -> None: """Is run externally - everything must be self-contained.""" @@ -135,17 +137,16 @@ def _create_file(save_path: str) -> None: cube.add_aux_coord(coord, 0) save(cube, save_path) - def setup(self) -> None: - self.file_path = BENCHMARK_DATA / "many_var_file.nc" - if not REUSE_DATA or not self.file_path.is_file(): + def setup_cache(self) -> None: + if not REUSE_DATA or not self.FILE_PATH.is_file(): # See :mod:`benchmarks.generate_data` docstring for full explanation. _ = run_function_elsewhere( self._create_file, - str(self.file_path), + str(self.FILE_PATH), ) def time_many_var_load(self) -> None: - _ = load(str(self.file_path)) + _ = load(str(self.FILE_PATH)) class StructuredFF: From b69a388437fa6e136b709d01a5c15eeb8f8bded0 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Fri, 11 Feb 2022 16:39:40 +0000 Subject: [PATCH 21/22] Clarify file re-use in benchmarks um_files generator. --- benchmarks/benchmarks/generate_data/um_files.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/benchmarks/benchmarks/generate_data/um_files.py b/benchmarks/benchmarks/generate_data/um_files.py index 5f9789d031..8792fcc48b 100644 --- a/benchmarks/benchmarks/generate_data/um_files.py +++ b/benchmarks/benchmarks/generate_data/um_files.py @@ -186,6 +186,9 @@ def create_um_files( All files representing a given shape are saved in a dedicated directory. A dictionary of the saved paths is returned. + + If the required files exist, they are re-used, unless + :const:`benchmarks.REUSE_DATA` is ``False``. """ # Self contained imports to avoid linting confusion with _create_um_files(). from . import BENCHMARK_DATA, REUSE_DATA, run_function_elsewhere From f96480db26e38cafecb2bcdfbc5b5446c0842b11 Mon Sep 17 00:00:00 2001 From: Martin Yeo Date: Mon, 14 Feb 2022 10:42:28 +0000 Subject: [PATCH 22/22] Benchmarking better strategy for not permanently realising arrays. --- benchmarks/benchmarks/loading.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/benchmarks/benchmarks/loading.py b/benchmarks/benchmarks/loading.py index f59a2fcff7..4558c3b5cb 100644 --- a/benchmarks/benchmarks/loading.py +++ b/benchmarks/benchmarks/loading.py @@ -60,12 +60,10 @@ def time_load(self, _, __, ___, ____) -> None: _ = self.load() def time_realise(self, _, __, ___, ____) -> None: - # Cache the original data object and re-apply after realisation, which - # restores original state for the next repeat. The cache/apply steps - # add negligible time to the benchmark result. - data_original = self.cube.core_data() - _ = self.cube.data - self.cube.data = data_original + # Don't touch cube.data - permanent realisation plays badly with ASV's + # re-run strategy. + assert self.cube.has_lazy_data() + self.cube.core_data().compute() class STASHConstraint: