From e7eced5f2abea2867c1baac5e8ef30cd01d8ca19 Mon Sep 17 00:00:00 2001 From: "Max H. Balsmeier" Date: Wed, 27 Jan 2021 11:26:41 +0100 Subject: [PATCH 1/9] Extended the installation description (#3958) * Extended the installation description As mentioned in Issue 3949, installation of Iris on Ubuntu was very difficult. I described an easy way to install Iris on Ubuntu without conda here. * Renamed a label. _installing_from_source_with_conda -> _installing_from_source because tests failed * Formatting of code improved. * update links (#3942) * update links * added s to http * Reset. * Simplified installation and modified latest.rst. * Included missing link in latest.rst. Co-authored-by: Ruth Comer --- docs/iris/src/installing.rst | 38 ++++++++++++++++++++++++-- docs/iris/src/userguide/citation.rst | 2 +- docs/iris/src/userguide/cube_maths.rst | 2 +- docs/iris/src/whatsnew/latest.rst | 3 ++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/docs/iris/src/installing.rst b/docs/iris/src/installing.rst index 762fe60e4d..5a81e59c88 100644 --- a/docs/iris/src/installing.rst +++ b/docs/iris/src/installing.rst @@ -41,11 +41,45 @@ need the Iris sample data. This can also be installed using conda:: Further documentation on using conda and the features it provides can be found at https://conda.io/en/latest/index.html. +.. _installing_from_source_without_conda: + +Installing from source without conda on Debian-based Linux distros (devs) +------------------------------------------------------------------------- + +Iris can also be installed without a conda environment. The instructions in +this section are valid for Debian-based Linux distributions (Debian, Ubuntu, +Kubuntu, etc.). + +Iris and its dependencies need some shared libraries in order to work properly. +These can be installed +with apt:: + + sudo apt-get install python3-pip python3-tk libudunits2-dev libproj-dev proj-bin libgeos-dev libcunit1-dev + +Consider executing:: + + sudo apt-get update + +before and after installation of Debian packages. + +The rest can be done with pip. Begin with numpy:: + + pip3 install numpy + +Finally, Iris and its Python dependencies can be installed with the following +command:: + + pip3 install setuptools cftime==1.2.1 cf-units scitools-pyke scitools-iris + +This procedure was tested on a Ubuntu 20.04 system on the +27th of January, 2021. +Be aware that through updates of the involved Debian and/or Python packages, +dependency conflicts might arise or the procedure might have to modified. .. _installing_from_source: -Installing from source (devs) ------------------------------ +Installing from source with conda (devs) +---------------------------------------- The latest Iris source release is available from https://github.com/SciTools/iris. diff --git a/docs/iris/src/userguide/citation.rst b/docs/iris/src/userguide/citation.rst index 56eab0a4eb..f91bc670f0 100644 --- a/docs/iris/src/userguide/citation.rst +++ b/docs/iris/src/userguide/citation.rst @@ -48,7 +48,7 @@ For example:: Iris. Met Office. git@github.com:SciTools/iris.git 06-03-2013 -.. _How to cite and describe software: http://software.ac.uk/so-exactly-what-software-did-you-use +.. _How to cite and describe software: https://software.ac.uk/how-cite-software Reference: [Jackson]_. diff --git a/docs/iris/src/userguide/cube_maths.rst b/docs/iris/src/userguide/cube_maths.rst index eebff53e62..1b1b2dbe66 100644 --- a/docs/iris/src/userguide/cube_maths.rst +++ b/docs/iris/src/userguide/cube_maths.rst @@ -243,7 +243,7 @@ unit (if ``a`` had units ``'m2'`` then ``a ** 0.5`` would result in a cube with units ``'m'``). Iris inherits units from `cf_units `_ -which in turn inherits from `UDUNITS `_. +which in turn inherits from `UDUNITS `_. As well as the units UDUNITS provides, cf units also provides the units ``'no-unit'`` and ``'unknown'``. A unit of ``'no-unit'`` means that the associated data is not suitable for describing with a unit, cf units diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 302cab7817..6205dc6bfa 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -53,6 +53,8 @@ This document explains the changes made to Iris for this release * `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. (:pull:`3933`) +* `@MHBalsmeier`_ Described non-conda installation on Debian-based distros. + (:pull:`3958`) 💼 Internal @@ -66,3 +68,4 @@ This document explains the changes made to Iris for this release .. _@trexfeathers: https://github.com/trexfeathers .. _@gcaria: https://github.com/gcaria .. _@rcomer: https://github.com/rcomer +.. _@MHBalsmeier: https://github.com/MHBalsmeier From 02777db967fc1dba1aa47611af1705ddb0e5cdb4 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 27 Jan 2021 11:11:20 +0000 Subject: [PATCH 2/9] Merge back v3p0p0 (#3960) * Add release highlights and pin rc version (#3898) * Add release highlights and pin rc version * review actions * reorder release highlights (#3899) Tweak release highlights * Add whatsnew announcement (#3900) * Fix spelling (#3903) * Fix unit label handling (#3902) * Add failing test of plotting * Implement fix to pass test * Update idiff to ignore irrelevant hyphens in path * Update imagerepo (following docs) * Update after review by @trexfeathers * Add whatsnew entries * Move whatsnew entries into correct file * Release Docs Improvements (#3895) * Minor phrasing change in 'Release candidate'. * Before release deprecations. * Whatsnew highlights section. * Relax setup.py setup requirements (#3909) * Updated CF saver version in User Guide and docstring (#3925) * Updated CF saver version in User Guide and docstring * Remove references to CF version of the loader in docstrings * Added whatsnew * Pin cftime<1.3.0 * Migrate to cirrus-ci (#3928) * migrate from travis-ci to cirrus-ci * added whatsnew entries * ignore url for doc link check (#3929) * whatsnew for coord default units (#3924) * Cube._summary_coord_extra: efficiency and bugfix (#3922) * Add Documentation Title Case Capitalization (#3940) * Use Title Case Capitalisation for Documentation * add whatsnew enter * CI requirements drop pip packages (#3939) * requirements pip to conda * use pip install over develop * default PY_VER to python versions * update links (#3942) * update links * added s to http * Add support for 1-d weights in collapse. (#3943) * Remove warning for convert_units on lazy data (#3951) * drop stickler references in docs (#3953) * drop stickler references in docs * remove sticker from common links * update docs for travis-ci to cirrus-ci (#3954) * update docs for travis-ci to cirrus-ci * add 'travis-ci' reference locally to whatsnew * update whatsnew comment * docs for nox (#3955) * docs for nox * add titles, notices and additional detail * review actions * Resolve test coverage (#3947) * test coverage for __init__ and __call__ * test coverage for metadata resolve and coverage * partial test coverage for metadata mapping * python 3.6 workaround for deepcopy of mock.sentinel * test coverage for Resolve._free_mapping * test coverage for Resolve convenience methods * add test stub for Resolve._metadata_mapping * fix Test__tgt_cube_position * test coverage for shape * test coverage for _as_compatible_cubes * test coverage for Resolve._metadata_mapping * test coverage for Resolve._prepare_common_dim_payload * test coverage for Resolve._prepare_common_aux_payload * test coverage for Resolve._prepare_points_and_bounds * test coverage for Resolve._create_prepared_item * test coverage for Resolve._prepare_local_payload_dim * test coverage for Resolve._prepare_local_payload_aux * test coverage for Resolve._prepare_local_payload_scalar + docs URL skip * test coverage for Resolve._prepare_local_payload * test coverage for Resolve._metadata_prepare * added docs URL linkcheck skip * test coverage for Resolve._prepare_factory_payload * test coverage for Resolve._get_prepared_item * review actions * test coverage for Resolve.cube * pin v3.0.0 version and whatnew date (#3956) * update github ci checks image (#3957) Co-authored-by: tkknight <2108488+tkknight@users.noreply.github.com> Co-authored-by: Zeb Nicholls Co-authored-by: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> Co-authored-by: Jon Seddon <17068361+jonseddon@users.noreply.github.com> Co-authored-by: Ruth Comer Co-authored-by: Patrick Peglar --- .../general/plot_SOI_filtering.py | 2 +- .../general/plot_anomaly_log_colouring.py | 2 +- .../gallery_code/general/plot_coriolis.py | 2 +- .../general/plot_cross_section.py | 2 +- .../general/plot_custom_aggregation.py | 2 +- .../general/plot_custom_file_loading.py | 2 +- .../gallery_code/general/plot_global_map.py | 2 +- .../general/plot_lineplot_with_legend.py | 2 +- .../gallery_code/general/plot_polar_stereo.py | 2 +- .../general/plot_polynomial_fit.py | 2 +- .../plot_projections_and_annotations.py | 2 +- .../general/plot_rotated_pole_mapping.py | 2 +- .../gallery_code/meteorology/plot_COP_1d.py | 2 +- .../gallery_code/meteorology/plot_COP_maps.py | 2 +- .../iris/gallery_code/meteorology/plot_TEC.py | 2 +- .../meteorology/plot_hovmoller.py | 2 +- .../meteorology/plot_lagged_ensemble.py | 2 +- .../meteorology/plot_wind_speed.py | 4 +- .../oceanography/plot_atlantic_profiles.py | 2 +- .../oceanography/plot_load_nemo.py | 2 +- docs/iris/src/common_links.inc | 5 +- docs/iris/src/conf.py | 8 +- docs/iris/src/copyright.rst | 6 +- docs/iris/src/developers_guide/ci_checks.png | Bin 24457 -> 203990 bytes .../developers_guide/contributing_changes.rst | 2 +- .../contributing_ci_tests.rst | 27 +- .../contributing_code_formatting.rst | 2 +- .../contributing_codebase_index.rst | 2 +- .../contributing_deprecations.rst | 12 +- .../contributing_documentation.rst | 10 +- .../contributing_getting_involved.rst | 2 +- .../contributing_graphics_tests.rst | 14 +- .../contributing_pull_request_checklist.rst | 6 +- .../contributing_running_tests.rst | 98 +- .../developers_guide/contributing_testing.rst | 8 +- .../documenting/docstrings.rst | 10 +- .../documenting/rest_guide.rst | 4 +- .../documenting/whats_new_contributions.rst | 12 +- .../gitwash/configure_git.rst | 6 +- .../gitwash/development_workflow.rst | 28 +- .../src/developers_guide/gitwash/forking.rst | 6 +- .../src/developers_guide/gitwash/index.rst | 2 +- .../developers_guide/gitwash/set_up_fork.rst | 8 +- docs/iris/src/developers_guide/release.rst | 20 +- docs/iris/src/further_topics/index.rst | 2 +- .../iris/src/further_topics/lenient_maths.rst | 12 +- .../src/further_topics/lenient_metadata.rst | 16 +- docs/iris/src/further_topics/metadata.rst | 40 +- docs/iris/src/index.rst | 4 +- docs/iris/src/installing.rst | 10 +- .../iris/src/techpapers/change_management.rst | 14 +- docs/iris/src/techpapers/index.rst | 2 +- .../src/techpapers/missing_data_handling.rst | 4 +- docs/iris/src/techpapers/um_files_loading.rst | 14 +- docs/iris/src/userguide/citation.rst | 6 +- docs/iris/src/userguide/code_maintenance.rst | 6 +- docs/iris/src/userguide/cube_maths.rst | 10 +- docs/iris/src/userguide/cube_statistics.rst | 10 +- .../interpolation_and_regridding.rst | 12 +- docs/iris/src/userguide/iris_cubes.rst | 8 +- .../iris/src/userguide/loading_iris_cubes.rst | 12 +- docs/iris/src/userguide/merge_and_concat.rst | 8 +- docs/iris/src/userguide/navigating_a_cube.rst | 12 +- docs/iris/src/userguide/plotting_a_cube.rst | 34 +- .../iris/src/userguide/real_and_lazy_data.rst | 10 +- docs/iris/src/userguide/saving_iris_cubes.rst | 18 +- docs/iris/src/userguide/subsetting_a_cube.rst | 8 +- docs/iris/src/whatsnew/1.0.rst | 20 +- docs/iris/src/whatsnew/1.1.rst | 6 +- docs/iris/src/whatsnew/1.10.rst | 6 +- docs/iris/src/whatsnew/1.11.rst | 2 +- docs/iris/src/whatsnew/1.13.rst | 4 +- docs/iris/src/whatsnew/1.2.rst | 4 +- docs/iris/src/whatsnew/1.3.rst | 10 +- docs/iris/src/whatsnew/1.4.rst | 28 +- docs/iris/src/whatsnew/1.5.rst | 2 +- docs/iris/src/whatsnew/1.6.rst | 20 +- docs/iris/src/whatsnew/1.7.rst | 6 +- docs/iris/src/whatsnew/1.8.rst | 4 +- docs/iris/src/whatsnew/1.9.rst | 6 +- docs/iris/src/whatsnew/2.0.rst | 4 +- docs/iris/src/whatsnew/2.1.rst | 6 +- docs/iris/src/whatsnew/2.2.rst | 2 +- docs/iris/src/whatsnew/2.3.rst | 2 +- docs/iris/src/whatsnew/2.4.rst | 2 +- docs/iris/src/whatsnew/3.0.rst | 25 +- docs/iris/src/whatsnew/index.rst | 2 +- lib/iris/__init__.py | 2 +- lib/iris/common/resolve.py | 714 ++- lib/iris/cube.py | 36 +- .../tests/unit/common/resolve/__init__.py | 6 + .../tests/unit/common/resolve/test_Resolve.py | 4795 +++++++++++++++++ lib/iris/tests/unit/cube/test_Cube.py | 112 + noxfile.py | 30 +- requirements/ci/py36.yml | 7 +- requirements/ci/py37.yml | 6 +- 96 files changed, 6030 insertions(+), 451 deletions(-) mode change 100644 => 100755 docs/iris/src/developers_guide/ci_checks.png create mode 100644 lib/iris/tests/unit/common/resolve/__init__.py create mode 100644 lib/iris/tests/unit/common/resolve/test_Resolve.py diff --git a/docs/iris/gallery_code/general/plot_SOI_filtering.py b/docs/iris/gallery_code/general/plot_SOI_filtering.py index 116e819af7..d7948ac965 100644 --- a/docs/iris/gallery_code/general/plot_SOI_filtering.py +++ b/docs/iris/gallery_code/general/plot_SOI_filtering.py @@ -1,5 +1,5 @@ """ -Applying a filter to a time-series +Applying a Filter to a Time-Series ================================== This example demonstrates low pass filtering a time-series by applying a diff --git a/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py b/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py index b0cee818de..778f92db1b 100644 --- a/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py +++ b/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py @@ -1,5 +1,5 @@ """ -Colouring anomaly data with logarithmic scaling +Colouring Anomaly Data With Logarithmic Scaling =============================================== In this example, we need to plot anomaly data where the values have a diff --git a/docs/iris/gallery_code/general/plot_coriolis.py b/docs/iris/gallery_code/general/plot_coriolis.py index cc67d1267c..77066d362a 100644 --- a/docs/iris/gallery_code/general/plot_coriolis.py +++ b/docs/iris/gallery_code/general/plot_coriolis.py @@ -1,5 +1,5 @@ """ -Deriving the Coriolis frequency over the globe +Deriving the Coriolis Frequency Over the Globe ============================================== This code computes the Coriolis frequency and stores it in a cube with diff --git a/docs/iris/gallery_code/general/plot_cross_section.py b/docs/iris/gallery_code/general/plot_cross_section.py index a4bc918fc7..12f4bdb0dc 100644 --- a/docs/iris/gallery_code/general/plot_cross_section.py +++ b/docs/iris/gallery_code/general/plot_cross_section.py @@ -1,5 +1,5 @@ """ -Cross section plots +Cross Section Plots =================== This example demonstrates contour plots of a cross-sectioned multi-dimensional diff --git a/docs/iris/gallery_code/general/plot_custom_aggregation.py b/docs/iris/gallery_code/general/plot_custom_aggregation.py index 9c847be779..5fba3669b6 100644 --- a/docs/iris/gallery_code/general/plot_custom_aggregation.py +++ b/docs/iris/gallery_code/general/plot_custom_aggregation.py @@ -1,5 +1,5 @@ """ -Calculating a custom statistic +Calculating a Custom Statistic ============================== This example shows how to define and use a custom diff --git a/docs/iris/gallery_code/general/plot_custom_file_loading.py b/docs/iris/gallery_code/general/plot_custom_file_loading.py index b96e152bf8..6890650704 100644 --- a/docs/iris/gallery_code/general/plot_custom_file_loading.py +++ b/docs/iris/gallery_code/general/plot_custom_file_loading.py @@ -1,5 +1,5 @@ """ -Loading a cube from a custom file format +Loading a Cube From a Custom File Format ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This example shows how a custom text file can be loaded using the standard Iris diff --git a/docs/iris/gallery_code/general/plot_global_map.py b/docs/iris/gallery_code/general/plot_global_map.py index 41fd226921..8d2bdee174 100644 --- a/docs/iris/gallery_code/general/plot_global_map.py +++ b/docs/iris/gallery_code/general/plot_global_map.py @@ -1,5 +1,5 @@ """ -Quickplot of a 2d cube on a map +Quickplot of a 2D Cube on a Map =============================== This example demonstrates a contour plot of global air temperature. The plot diff --git a/docs/iris/gallery_code/general/plot_lineplot_with_legend.py b/docs/iris/gallery_code/general/plot_lineplot_with_legend.py index 5641b9c4d0..78401817ba 100644 --- a/docs/iris/gallery_code/general/plot_lineplot_with_legend.py +++ b/docs/iris/gallery_code/general/plot_lineplot_with_legend.py @@ -1,5 +1,5 @@ """ -Multi-line temperature profile plot +Multi-Line Temperature Profile Plot ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ """ diff --git a/docs/iris/gallery_code/general/plot_polar_stereo.py b/docs/iris/gallery_code/general/plot_polar_stereo.py index bd4a11923d..71c0f3b00e 100644 --- a/docs/iris/gallery_code/general/plot_polar_stereo.py +++ b/docs/iris/gallery_code/general/plot_polar_stereo.py @@ -1,5 +1,5 @@ """ -Example of a polar stereographic plot +Example of a Polar Stereographic Plot ===================================== Demonstrates plotting data that are defined on a polar stereographic diff --git a/docs/iris/gallery_code/general/plot_polynomial_fit.py b/docs/iris/gallery_code/general/plot_polynomial_fit.py index 237f4044b6..5da5d50571 100644 --- a/docs/iris/gallery_code/general/plot_polynomial_fit.py +++ b/docs/iris/gallery_code/general/plot_polynomial_fit.py @@ -1,5 +1,5 @@ """ -Fitting a polynomial +Fitting a Polynomial ==================== This example demonstrates computing a polynomial fit to 1D data from an Iris diff --git a/docs/iris/gallery_code/general/plot_projections_and_annotations.py b/docs/iris/gallery_code/general/plot_projections_and_annotations.py index e59bb236d7..f93ac3714f 100644 --- a/docs/iris/gallery_code/general/plot_projections_and_annotations.py +++ b/docs/iris/gallery_code/general/plot_projections_and_annotations.py @@ -1,5 +1,5 @@ """ -Plotting in different projections +Plotting in Different Projections ================================= This example shows how to overlay data and graphics in different projections, diff --git a/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py b/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py index 063fe93674..8a0c80c707 100644 --- a/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py +++ b/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py @@ -1,5 +1,5 @@ """ -Rotated pole mapping +Rotated Pole Mapping ===================== This example uses several visualisation methods to achieve an array of diff --git a/docs/iris/gallery_code/meteorology/plot_COP_1d.py b/docs/iris/gallery_code/meteorology/plot_COP_1d.py index 2f93627b77..bebbad4224 100644 --- a/docs/iris/gallery_code/meteorology/plot_COP_1d.py +++ b/docs/iris/gallery_code/meteorology/plot_COP_1d.py @@ -1,5 +1,5 @@ """ -Global average annual temperature plot +Global Average Annual Temperature Plot ====================================== Produces a time-series plot of North American temperature forecasts for 2 diff --git a/docs/iris/gallery_code/meteorology/plot_COP_maps.py b/docs/iris/gallery_code/meteorology/plot_COP_maps.py index a8e6055a77..5555a0b85c 100644 --- a/docs/iris/gallery_code/meteorology/plot_COP_maps.py +++ b/docs/iris/gallery_code/meteorology/plot_COP_maps.py @@ -1,5 +1,5 @@ """ -Global average annual temperature maps +Global Average Annual Temperature Maps ====================================== Produces maps of global temperature forecasts from the A1B and E1 scenarios. diff --git a/docs/iris/gallery_code/meteorology/plot_TEC.py b/docs/iris/gallery_code/meteorology/plot_TEC.py index df2e29ef19..71a743a161 100644 --- a/docs/iris/gallery_code/meteorology/plot_TEC.py +++ b/docs/iris/gallery_code/meteorology/plot_TEC.py @@ -1,5 +1,5 @@ """ -Ionosphere space weather +Ionosphere Space Weather ======================== This space weather example plots a filled contour of rotated pole point diff --git a/docs/iris/gallery_code/meteorology/plot_hovmoller.py b/docs/iris/gallery_code/meteorology/plot_hovmoller.py index 9f18b8021e..e9f8207a94 100644 --- a/docs/iris/gallery_code/meteorology/plot_hovmoller.py +++ b/docs/iris/gallery_code/meteorology/plot_hovmoller.py @@ -1,5 +1,5 @@ """ -Hovmoller diagram of monthly surface temperature +Hovmoller Diagram of Monthly Surface Temperature ================================================ This example demonstrates the creation of a Hovmoller diagram with fine control diff --git a/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py b/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py index cdd39028c8..5cd2752f39 100644 --- a/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py +++ b/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py @@ -1,5 +1,5 @@ """ -Seasonal ensemble model plots +Seasonal Ensemble Model Plots ============================= This example demonstrates the loading of a lagged ensemble dataset from the diff --git a/docs/iris/gallery_code/meteorology/plot_wind_speed.py b/docs/iris/gallery_code/meteorology/plot_wind_speed.py index 6844d3874c..79be64ddd7 100644 --- a/docs/iris/gallery_code/meteorology/plot_wind_speed.py +++ b/docs/iris/gallery_code/meteorology/plot_wind_speed.py @@ -1,6 +1,6 @@ """ -Plotting wind direction using quiver -=========================================================== +Plotting Wind Direction Using Quiver +==================================== This example demonstrates using quiver to plot wind speed contours and wind direction arrows from wind vector component input data. The vector components diff --git a/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py b/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py index 89d99c80b4..dc038ecffe 100644 --- a/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py +++ b/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py @@ -1,5 +1,5 @@ """ -Oceanographic profiles and T-S diagrams +Oceanographic Profiles and T-S Diagrams ======================================= This example demonstrates how to plot vertical profiles of different diff --git a/docs/iris/gallery_code/oceanography/plot_load_nemo.py b/docs/iris/gallery_code/oceanography/plot_load_nemo.py index 5f2b72c956..c7ad5aaee4 100644 --- a/docs/iris/gallery_code/oceanography/plot_load_nemo.py +++ b/docs/iris/gallery_code/oceanography/plot_load_nemo.py @@ -1,5 +1,5 @@ """ -Load a time series of data from the NEMO model +Load a Time Series of Data From the NEMO Model ============================================== This example demonstrates how to load multiple files containing data output by diff --git a/docs/iris/src/common_links.inc b/docs/iris/src/common_links.inc index 0bc8ca60e6..3941bfaff2 100644 --- a/docs/iris/src/common_links.inc +++ b/docs/iris/src/common_links.inc @@ -12,9 +12,8 @@ .. _iris-sample-data: https://github.com/SciTools/iris-sample-data .. _test-iris-imagehash: https://github.com/SciTools/test-iris-imagehash .. _readthedocs.yml: https://github.com/SciTools/iris/blob/master/requirements/ci/readthedocs.yml -.. _travis-ci: https://travis-ci.org/github/SciTools/iris -.. _.travis.yml: https://github.com/SciTools/iris/blob/master/.travis.yml -.. _.stickler.yml: https://github.com/SciTools/iris/blob/master/.stickler.yml +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris +.. _.cirrus.yml: https://github.com/SciTools/iris/blob/master/.cirrus.yml .. _.flake8.yml: https://github.com/SciTools/iris/blob/master/.flake8 .. _GitHub Help Documentation: https://docs.github.com/en/github .. _using git: https://docs.github.com/en/github/using-git diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index ab7689479a..7232d7c40e 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -230,7 +230,7 @@ def autolog(message): "menu_links_name": "Support", "menu_links": [ ( - ' Source code', + ' Source Code', "https://github.com/SciTools/iris", ), ( @@ -242,11 +242,11 @@ def autolog(message): "https://groups.google.com/forum/#!forum/scitools-iris-dev", ), ( - ' StackOverflow for "How do I?"', + ' StackOverflow for "How Do I?"', "https://stackoverflow.com/questions/tagged/python-iris", ), ( - ' Legacy documentation', + ' Legacy Documentation', "https://scitools.org.uk/iris/docs/v2.4.0/index.html", ), ], @@ -269,6 +269,8 @@ def autolog(message): "http://schacon.github.com/git", "http://scitools.github.com/cartopy", "http://www.wmo.int/pages/prog/www/DPFS/documents/485_Vol_I_en_colour.pdf", + "https://software.ac.uk/how-cite-software", + "http://www.esrl.noaa.gov/psd/data/gridded/conventions/cdc_netcdf_standard.shtml", ] # list of sources to exclude from the build. diff --git a/docs/iris/src/copyright.rst b/docs/iris/src/copyright.rst index 08a40e5a1e..16ac07acb3 100644 --- a/docs/iris/src/copyright.rst +++ b/docs/iris/src/copyright.rst @@ -1,8 +1,8 @@ -Iris copyright, licensing and contributors +Iris Copyright, Licensing and Contributors ========================================== -Iris code +Iris Code --------- All Iris source code, unless explicitly stated, is ``Copyright Iris @@ -20,7 +20,7 @@ You should find all source files with the following header: licensing details. -Iris documentation and examples +Iris Documentation and Examples ------------------------------- All documentation, examples and sample data found on this website and in source repository diff --git a/docs/iris/src/developers_guide/ci_checks.png b/docs/iris/src/developers_guide/ci_checks.png old mode 100644 new mode 100755 index cf93239dea4ef9ee6c265d5f74359993ca822e15..e088e03a6657cf5719468a850e4279c162723c58 GIT binary patch literal 203990 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

-F3$~3Eu?g0z()hSyh`PNw(fJxbD5Rcm-jaN2 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 literal 24457 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. -Adding a deprecation +Adding a Deprecation ==================== .. _removing-a-public-api: -Removing a public API +Removing a Public API --------------------- The simplest form of deprecation occurs when you need to remove a public @@ -49,7 +49,7 @@ Under these circumstances the following points apply: - You should check the documentation for references to the deprecated API and update them as appropriate. -Changing a default +Changing a Default ------------------ When you need to change the default behaviour of a public API the @@ -74,7 +74,7 @@ API: deprecation warning and corresponding Sphinx deprecation directive. -Removing a deprecation +Removing a Deprecation ====================== When the time comes to make a new major release you should locate any @@ -83,7 +83,7 @@ minimum period described previously. Locating deprecated APIs can easily be done by searching for the Sphinx deprecation directives and/or deprecation warnings. -Removing a public API +Removing a Public API --------------------- The deprecated API should be removed and any corresponding documentation @@ -91,7 +91,7 @@ and/or example code should be removed/updated as appropriate. .. _iris_developer_future: -Changing a default +Changing a Default ------------------ - You should update the initial state of the relevant boolean attribute diff --git a/docs/iris/src/developers_guide/contributing_documentation.rst b/docs/iris/src/developers_guide/contributing_documentation.rst index 9674289568..56d2257a55 100644 --- a/docs/iris/src/developers_guide/contributing_documentation.rst +++ b/docs/iris/src/developers_guide/contributing_documentation.rst @@ -1,7 +1,7 @@ .. _contributing.documentation: -Contributing to the documentation +Contributing to the Documentation --------------------------------- Documentation is important and we encourage any improvements that can be made. @@ -28,7 +28,7 @@ The build can be run from the documentation directory ``iris/docs/iris/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 `travis-ci`_ build. +*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. @@ -50,7 +50,7 @@ This is useful for a final test before committing your changes. have been promoted to be **errors** to ensure they are addressed. This **only** applies when ``make html`` is run. -.. _travis-ci: https://travis-ci.org/github/SciTools/iris +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris .. _contributing.documentation.testing: @@ -99,7 +99,7 @@ or ignore the url. ``spelling_word_list_filename``. -.. note:: In addition to the automated `travis-ci`_ build of all the +.. 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 @@ -112,7 +112,7 @@ or ignore the url. .. _contributing.documentation.api: -Generating API documentation +Generating API Documentation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to auto generate the API documentation based upon the docstrings a diff --git a/docs/iris/src/developers_guide/contributing_getting_involved.rst b/docs/iris/src/developers_guide/contributing_getting_involved.rst index edcbbaf726..0fd873517f 100644 --- a/docs/iris/src/developers_guide/contributing_getting_involved.rst +++ b/docs/iris/src/developers_guide/contributing_getting_involved.rst @@ -2,7 +2,7 @@ .. _development_where_to_start: -Getting involved +Getting Involved ---------------- Iris_ is an Open Source project hosted on Github and as such anyone with a diff --git a/docs/iris/src/developers_guide/contributing_graphics_tests.rst b/docs/iris/src/developers_guide/contributing_graphics_tests.rst index a276f520d6..8d8189c69b 100644 --- a/docs/iris/src/developers_guide/contributing_graphics_tests.rst +++ b/docs/iris/src/developers_guide/contributing_graphics_tests.rst @@ -2,7 +2,7 @@ .. _testing.graphics: -Graphics tests +Graphics Tests ************** Iris may be used to create various forms of graphical output; to ensure @@ -31,10 +31,10 @@ known acceptable output may fail. The failure may also not be visually perceived as it may be a simple pixel shift. -Testing strategy +Testing Strategy ================ -The `Iris Travis matrix`_ defines multiple test runs that use +The `Iris Cirrus-CI matrix`_ defines multiple test runs that use different versions of Python to ensure Iris is working as expected. To make this manageable, the ``iris.tests.IrisTest_nometa.check_graphic`` test @@ -64,7 +64,7 @@ This consists of: against the existing accepted reference images, for each failing test. -Reviewing failing tests +Reviewing Failing Tests ======================= When you find that a graphics test in the Iris testing suite has failed, @@ -122,7 +122,7 @@ you should follow: happens, simply repeat the check-and-accept process until all tests pass. -Add your changes to Iris +Add Your Changes to Iris ======================== To add your changes to Iris, you need to make two pull requests (PR). @@ -155,7 +155,7 @@ To add your changes to Iris, you need to make two pull requests (PR). .. important:: - The Iris pull-request will not test successfully in Travis until the + The Iris pull-request will not test successfully in Cirrus-CI until the ``test-iris-imagehash`` pull request has been merged. This is because there is an Iris_ test which ensures the existence of the reference images (uris) for all the targets in the image results database. It will also fail @@ -163,4 +163,4 @@ To add your changes to Iris, you need to make two pull requests (PR). image-listing file in ``test-iris-imagehash``. -.. _Iris travis matrix: https://github.com/scitools/iris/blob/master/.travis.yml#L15 +.. _Iris Cirrus-CI matrix: https://github.com/scitools/iris/blob/master/.cirrus.yml diff --git a/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst b/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst index b01f370ea2..3e7a9f1ae3 100644 --- a/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst +++ b/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst @@ -2,8 +2,8 @@ .. _pr_check: -Pull request check list -======================= +Pull Request Checklist +====================== All pull request will be reviewed by a core developer who will manage the process of merging. It is the responsibility of a developer submitting a @@ -38,7 +38,7 @@ is merged. Before submitting a pull request please consider this list. #. **Check the documentation builds without warnings or errors**. See :ref:`contributing.documentation.building` -#. **Check for any new dependencies in the** `.travis.yml`_ **config file.** +#. **Check for any new dependencies in the** `.cirrus.yml`_ **config file.** #. **Check for any new dependencies in the** `readthedocs.yml`_ **file**. This file is used to build the documentation that is served from diff --git a/docs/iris/src/developers_guide/contributing_running_tests.rst b/docs/iris/src/developers_guide/contributing_running_tests.rst index cadf3710db..99ea4e831c 100644 --- a/docs/iris/src/developers_guide/contributing_running_tests.rst +++ b/docs/iris/src/developers_guide/contributing_running_tests.rst @@ -2,9 +2,14 @@ .. _developer_running_tests: -Running the tests +Running the Tests ***************** +Using setuptools for Testing Iris +================================= + +.. warning:: The `setuptools`_ ``test`` command was deprecated in `v41.5.0`_. See :ref:`using nox`. + A prerequisite of running the tests is to have the Python environment setup. For more information on this see :ref:`installing_from_source`. @@ -90,4 +95,93 @@ due to an experimental dependency not being present. All Python decorators that skip tests will be defined in ``lib/iris/tests/__init__.py`` with a function name with a prefix of - ``skip_``. \ No newline at end of file + ``skip_``. + + +.. _using nox: + +Using Nox for Testing Iris +========================== + +Iris has adopted the use of the `nox`_ tool for automated testing on `cirrus-ci`_ +and also locally on the command-line for developers. + +`nox`_ is similar to `tox`_, but instead leverages the expressiveness and power of a Python +configuration file rather than an `.ini` style file. As with `tox`_, `nox`_ can use `virtualenv`_ +to create isolated Python environments, but in addition also supports `conda`_ as a testing +environment backend. + + +Where is Nox Used? +------------------ + +Iris uses `nox`_ as a convenience to fully automate the process of executing the Iris tests, but also +automates the process of: + +* building the documentation and executing the doc-tests +* building the documentation gallery +* running the documentation URL link check +* linting the code-base +* ensuring the code-base style conforms to the `black`_ standard + + +You can perform all of these tasks manually yourself, however the onus is on you to first ensure +that all of the required package dependencies are installed and available in the testing environment. + +`Nox`_ has been configured to automatically do this for you, and provides a means to easily replicate +the remote testing behaviour of `cirrus-ci`_ locally for the developer. + + +Installing Nox +-------------- + +We recommend installing `nox`_ using `conda`_. To install `nox`_ in a separate `conda`_ environment:: + + conda create -n nox -c conda-forge nox + conda activate nox + +To install `nox`_ in an existing active `conda`_ environment:: + + conda install -c conda-forge nox + +The `nox`_ package is also available on PyPI, however `nox`_ has been configured to use the `conda`_ +backend for Iris, so an installation of `conda`_ must always be available. + + +Testing with Nox +---------------- + +The `nox`_ configuration file `noxfile.py` is available in the root ``iris`` project directory, and +defines all the `nox`_ sessions (i.e., tasks) that may be performed. `nox`_ must always be executed +from the ``iris`` root directory. + +To list the configured `nox`_ sessions for Iris:: + + nox --list + +To run the Iris tests for all configured versions of Python:: + + nox --session tests + +To build the Iris documentation specifically for Python 3.7:: + + nox --session doctest-3.7 + +To run all the Iris `nox`_ sessions:: + + nox + +For further `nox`_ command-line options:: + + nox --help + +.. note:: `nox`_ will cache its testing environments in the `.nox` root ``iris`` project directory. + + +.. _black: https://black.readthedocs.io/en/stable/ +.. _nox: https://nox.thea.codes/en/latest/ +.. _setuptools: https://setuptools.readthedocs.io/en/latest/ +.. _tox: https://tox.readthedocs.io/en/latest/ +.. _virtualenv: https://virtualenv.pypa.io/en/latest/ +.. _PyPI: https://pypi.org/project/nox/ +.. _v41.5.0: https://setuptools.readthedocs.io/en/latest/history.html#v41-5-0 diff --git a/docs/iris/src/developers_guide/contributing_testing.rst b/docs/iris/src/developers_guide/contributing_testing.rst index 375ad57003..486af706d3 100644 --- a/docs/iris/src/developers_guide/contributing_testing.rst +++ b/docs/iris/src/developers_guide/contributing_testing.rst @@ -3,7 +3,7 @@ .. _developer_test_categories: -Test categories +Test Categories *************** There are two main categories of tests within Iris: @@ -20,7 +20,7 @@ feel free to submit a pull-request in any state and ask for assistance. .. _testing.unit_test: -Unit tests +Unit Tests ========== Code changes should be accompanied by enough unit tests to give a @@ -128,7 +128,7 @@ Within that file the tests might look something like: .. _testing.integration: -Integration tests +Integration Tests ================= Some code changes may require tests which exercise several units in @@ -141,4 +141,4 @@ tests. But folders and files must be created as required to help developers locate relevant tests. It is recommended they are named according to the capabilities under test, e.g. ``metadata/test_pp_preservation.py``, and not named according to the -module(s) under test. \ No newline at end of file +module(s) under test. diff --git a/docs/iris/src/developers_guide/documenting/docstrings.rst b/docs/iris/src/developers_guide/documenting/docstrings.rst index 34ec790d03..8a06024ee2 100644 --- a/docs/iris/src/developers_guide/documenting/docstrings.rst +++ b/docs/iris/src/developers_guide/documenting/docstrings.rst @@ -27,7 +27,7 @@ There are two forms of docstrings: **single-line** and **multi-line** docstrings. -Single-line docstrings +Single-Line Docstrings ====================== The single line docstring of an object must state the **purpose** of that @@ -35,7 +35,7 @@ object, known as the **purpose section**. This terse overview must be on one line and ideally no longer than 80 characters. -Multi-line docstrings +Multi-Line Docstrings ===================== Multi-line docstrings must consist of at least a purpose section akin to the @@ -53,7 +53,7 @@ not to document *argument* and *keyword argument* details. Such information should be documented in the following *arguments and keywords section*. -Sample multi-line docstring +Sample Multi-Line Docstring --------------------------- Here is a simple example of a standard docstring: @@ -75,7 +75,7 @@ Additionally, a summary can be extracted automatically, which would result in: documenting.docstrings_sample_routine.sample_routine -Documenting classes +Documenting Classes =================== The class constructor should be documented in the docstring for its @@ -90,7 +90,7 @@ superclass method and does not call the superclass method; use the verb (in addition to its own behaviour). -Attribute and property docstrings +Attribute and Property Docstrings --------------------------------- Here is a simple example of a class containing an attribute docstring and a diff --git a/docs/iris/src/developers_guide/documenting/rest_guide.rst b/docs/iris/src/developers_guide/documenting/rest_guide.rst index bc34d16cd8..4845132b15 100644 --- a/docs/iris/src/developers_guide/documenting/rest_guide.rst +++ b/docs/iris/src/developers_guide/documenting/rest_guide.rst @@ -3,7 +3,7 @@ .. _reST_quick_start: ================ -reST quick start +reST Quick Start ================ `reST`_ is used to create the documentation for Iris_. It is used to author @@ -19,7 +19,7 @@ reST markup syntaxes, for the basics of reST the following links may be useful: Reference documentation for reST can be found at http://docutils.sourceforge.net/rst.html. -Creating links +Creating Links -------------- Basic links can be created with ```Text of the link `_`` which will look like `Text of the link `_ diff --git a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst index 856d9af0a9..d6f805c511 100644 --- a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst +++ b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst @@ -1,7 +1,7 @@ .. _whats_new_contributions: ================================= -Contributing a "What's New" entry +Contributing a "What's New" Entry ================================= Iris uses a file named ``latest.rst`` to keep a draft of upcoming changes @@ -38,7 +38,7 @@ situation is thought likely (large PR, high repo activity etc.): * PR author: create the "What's New" pull request * PR reviewer: once the "What's New" PR is created, **merge the main PR**. - (this will fix any `travis-ci`_ linkcheck errors where the links in the + (this will fix any `cirrus-ci`_ linkcheck errors where the links in the "What's New" PR reference new features introduced in the main PR) * PR reviewer: review the "What's New" PR, merge once acceptable @@ -48,7 +48,7 @@ for the minimum time, minimising conflicts and minimising the need to rebase or merge from trunk. -Writing a contribution +Writing a Contribution ====================== As introduced above, a contribution is the description of a change to Iris @@ -96,14 +96,14 @@ examine past what's :ref:`iris_whatsnew` entries. .. note:: The reStructuredText syntax will be checked as part of building the documentation. Any warnings should be corrected. - `travis-ci`_ will automatically build the documentation when + `cirrus-ci`_ will automatically build the documentation when creating a pull request, however you can also manually :ref:`build ` the documentation. -.. _travis-ci: https://travis-ci.org/github/SciTools/iris +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris -Contribution categories +Contribution Categories ======================= The structure of the what's new release note should be easy to read by diff --git a/docs/iris/src/developers_guide/gitwash/configure_git.rst b/docs/iris/src/developers_guide/gitwash/configure_git.rst index b958a683ee..6fc288daf9 100644 --- a/docs/iris/src/developers_guide/gitwash/configure_git.rst +++ b/docs/iris/src/developers_guide/gitwash/configure_git.rst @@ -3,7 +3,7 @@ .. _configure-git: ============= -Configure git +Configure Git ============= .. _git-config-basic: @@ -51,7 +51,7 @@ command:: To set up on another computer, you can copy your ``~/.gitconfig`` file, or run the commands above. -In detail +In Detail ========= user.name and user.email @@ -124,7 +124,7 @@ Or from the command line:: .. _fancy-log: -Fancy log output +Fancy Log Output ---------------- This is a very nice alias to get a fancy log output; it should go in the diff --git a/docs/iris/src/developers_guide/gitwash/development_workflow.rst b/docs/iris/src/developers_guide/gitwash/development_workflow.rst index b67885e6bd..f6144a05e9 100644 --- a/docs/iris/src/developers_guide/gitwash/development_workflow.rst +++ b/docs/iris/src/developers_guide/gitwash/development_workflow.rst @@ -1,14 +1,14 @@ .. _development-workflow: #################### -Development workflow +Development Workflow #################### You already have your own forked copy of the `iris`_ repository, by following :ref:`forking`. You have :ref:`set-up-fork`. You have configured git by following :ref:`configure-git`. Now you are ready for some real work. -Workflow summary +Workflow Summary ================ In what follows we'll refer to the upstream iris ``master`` branch, as @@ -34,7 +34,7 @@ what you've done, and why you did it. See `linux git workflow`_ for some explanation. -Consider deleting your master branch +Consider Deleting Your Master Branch ==================================== It may sound strange, but deleting your own ``master`` branch can help reduce @@ -43,7 +43,7 @@ details. .. _update-mirror-trunk: -Update the mirror of trunk +Update the Mirror of Trunk ========================== First make sure you have done :ref:`linking-to-upstream`. @@ -59,7 +59,7 @@ you last checked, ``upstream/master`` will change after you do the fetch. .. _make-feature-branch: -Make a new feature branch +Make a New Feature Branch ========================= When you are ready to make some changes to the code, you should start a new @@ -99,7 +99,7 @@ From now on git will know that ``my-new-feature`` is related to the .. _edit-flow: -The editing workflow +The Editing Workflow ==================== Overview @@ -112,7 +112,7 @@ Overview git commit -am 'NF - some message' git push -In more detail +In More Detail -------------- #. Make some changes @@ -144,14 +144,14 @@ In more detail push`` (see `git push`_). -Testing your changes +Testing Your Changes ==================== Once you are happy with your changes, work thorough the :ref:`pr_check` and make sure your branch passes all the relevant tests. -Ask for your changes to be reviewed or merged +Ask for Your Changes to be Reviewed or Merged ============================================= When you are ready to ask for someone to review your code and consider a merge: @@ -175,10 +175,10 @@ When you are ready to ask for someone to review your code and consider a merge: pull request message. This is still a good way of getting some preliminary code review. -Some other things you might want to do +Some Other Things you Might Want to do ====================================== -Delete a branch on github +Delete a Branch on Github ------------------------- :: @@ -193,7 +193,7 @@ Note the colon ``:`` before ``test-branch``. See also: http://github.com/guides/remove-a-remote-branch -Several people sharing a single repository +Several People Sharing a Single Repository ------------------------------------------ If you want to work on some stuff with other people, where you are all @@ -225,7 +225,7 @@ usual:: git commit -am 'ENH - much better code' git push origin master # pushes directly into your repo -Explore your repository +Explore Your Repository ----------------------- To see a graphical representation of the repository branches and @@ -243,7 +243,7 @@ graph of the repository. .. _rebase-on-trunk: -Rebasing on trunk +Rebasing on Trunk ----------------- For more information please see the diff --git a/docs/iris/src/developers_guide/gitwash/forking.rst b/docs/iris/src/developers_guide/gitwash/forking.rst index e10b8f84ca..161847ed79 100644 --- a/docs/iris/src/developers_guide/gitwash/forking.rst +++ b/docs/iris/src/developers_guide/gitwash/forking.rst @@ -3,7 +3,7 @@ .. _forking: =================================== -Making your own copy (fork) of Iris +Making Your own Copy (fork) of Iris =================================== You need to do this only once. The instructions here are very similar @@ -12,7 +12,7 @@ that page for more detail. We're repeating some of it here just to give the specifics for the `Iris`_ project, and to suggest some default names. -Set up and configure a github account +Set up and Configure a Github Account ===================================== If you don't have a github account, go to the github page, and make one. @@ -21,7 +21,7 @@ You then need to configure your account to allow write access, see the `generating sss keys for GitHub`_ help on `github help`_. -Create your own forked copy of Iris +Create Your own Forked Copy of Iris =================================== #. Log into your github account. diff --git a/docs/iris/src/developers_guide/gitwash/index.rst b/docs/iris/src/developers_guide/gitwash/index.rst index d0e70597f1..3cde622583 100644 --- a/docs/iris/src/developers_guide/gitwash/index.rst +++ b/docs/iris/src/developers_guide/gitwash/index.rst @@ -1,6 +1,6 @@ .. _using-git: -Working with Iris source code +Working With Iris Source Code ============================= .. toctree:: diff --git a/docs/iris/src/developers_guide/gitwash/set_up_fork.rst b/docs/iris/src/developers_guide/gitwash/set_up_fork.rst index 9dc6618c64..70d602c97c 100644 --- a/docs/iris/src/developers_guide/gitwash/set_up_fork.rst +++ b/docs/iris/src/developers_guide/gitwash/set_up_fork.rst @@ -3,7 +3,7 @@ .. _set-up-fork: ================ -Set up your fork +Set up Your Fork ================ First you follow the instructions for :ref:`forking`. @@ -17,10 +17,10 @@ Overview cd iris git remote add upstream git://github.com/SciTools/iris.git -In detail +In Detail ========= -Clone your fork +Clone Your Fork --------------- #. Clone your fork to the local computer with ``git clone @@ -42,7 +42,7 @@ Clone your fork .. _linking-to-upstream: -Linking your repository to the upstream repo +Linking Your Repository to the Upstream Repo -------------------------------------------- :: diff --git a/docs/iris/src/developers_guide/release.rst b/docs/iris/src/developers_guide/release.rst index 2ec787a780..6ac3af5c75 100644 --- a/docs/iris/src/developers_guide/release.rst +++ b/docs/iris/src/developers_guide/release.rst @@ -10,7 +10,7 @@ The summary below is of the main areas that constitute the release. The final section details the :ref:`iris_development_releases_steps` to take. -Before release +Before Release -------------- Deprecations @@ -21,7 +21,7 @@ previous releases is now finally changed. More detail, including the correct number of releases, is in :ref:`iris_development_deprecations`. -Release branch +Release Branch -------------- Once the features intended for the release are on master, a release branch @@ -37,7 +37,7 @@ This branch shall be used to finalise the release details in preparation for the release candidate. -Release candidate +Release Candidate ----------------- Prior to a release, a release candidate tag may be created, marked as a @@ -67,7 +67,7 @@ This content should be reviewed and adapted as required. Steps to achieve this can be found in the :ref:`iris_development_releases_steps`. -The release +The Release ----------- The final steps are to change the version string in the source of @@ -78,7 +78,7 @@ Once all checks are complete, the release is cut by the creation of a new tag in the SciTools Iris repository. -Conda recipe +Conda Recipe ------------ Once a release is cut, the `Iris feedstock`_ for the conda recipe must be @@ -88,7 +88,7 @@ updated to build the latest release of Iris and push this artefact to .. _Iris feedstock: https://github.com/conda-forge/iris-feedstock/tree/master/recipe .. _conda forge: https://anaconda.org/conda-forge/iris -Merge back +Merge Back ---------- After the release is cut, the changes shall be merged back onto the @@ -101,7 +101,7 @@ pull request to master. This work flow ensures that the commit identifiers are consistent between the :literal:`.x` branch and :literal:`master`. -Point releases +Point Releases -------------- Bug fixes may be implemented and targeted as the :literal:`.x` branch. These @@ -118,12 +118,12 @@ release process is to be followed, including the merge back of changes into .. _iris_development_releases_steps: -Maintainer steps +Maintainer Steps ---------------- These steps assume a release for ``v1.9`` is to be created -Release steps +Release Steps ~~~~~~~~~~~~~ #. Create the branch ``1.9.x`` on the main repo, not in a forked repo, for the @@ -156,7 +156,7 @@ Release steps `Iris release page `_ -Post release steps +Post Release Steps ~~~~~~~~~~~~~~~~~~ #. Check the documentation has built on `Read The Docs`_. The build is diff --git a/docs/iris/src/further_topics/index.rst b/docs/iris/src/further_topics/index.rst index 8a4d95b6cd..dc162d6a1e 100644 --- a/docs/iris/src/further_topics/index.rst +++ b/docs/iris/src/further_topics/index.rst @@ -5,7 +5,7 @@ Introduction Some specific areas of Iris may require further explanation or a deep dive into additional detail above and beyond that offered by the -:ref:`User guide `. +:ref:`User Guide `. This section provides a collection of additional material on focused topics that may be of interest to the more advanced or curious user. diff --git a/docs/iris/src/further_topics/lenient_maths.rst b/docs/iris/src/further_topics/lenient_maths.rst index 6f139fd9bf..4aad721780 100644 --- a/docs/iris/src/further_topics/lenient_maths.rst +++ b/docs/iris/src/further_topics/lenient_maths.rst @@ -1,6 +1,6 @@ .. _lenient maths: -Lenient cube maths +Lenient Cube Maths ****************** This section provides an overview of lenient cube maths. In particular, it explains @@ -46,7 +46,7 @@ a practical worked example, which we'll explore together next. .. _lenient example: -Lenient example +Lenient Example =============== .. testsetup:: lenient-example @@ -154,7 +154,7 @@ Now let's compare and contrast this lenient result with the strict alternative. But before we do so, let's first clarify how to control the behaviour of cube maths. -Control the behaviour +Control the Behaviour ===================== As stated earlier, lenient cube maths is the default behaviour from Iris ``3.0.0``. @@ -191,7 +191,7 @@ scope of the ``LENIENT`` `context manager`_, Lenient(maths=True) -Strict example +Strict Example ============== Now that we know how to control the underlying behaviour of cube maths, @@ -229,7 +229,7 @@ This is because strict cube maths, in general, will only return common metadata and common coordinates that are :ref:`strictly equivalent `. -Finer detail +Finer Detail ============ In general, if you want to preserve as much metadata and coordinate information as @@ -278,4 +278,4 @@ resultant :class:`~iris.cube.Cube`, .. _atmosphere hybrid height parametric vertical coordinate: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#atmosphere-hybrid-height-coordinate -.. _context manager: https://docs.python.org/3/library/contextlib.html \ No newline at end of file +.. _context manager: https://docs.python.org/3/library/contextlib.html diff --git a/docs/iris/src/further_topics/lenient_metadata.rst b/docs/iris/src/further_topics/lenient_metadata.rst index ada7049786..b68ed501ba 100644 --- a/docs/iris/src/further_topics/lenient_metadata.rst +++ b/docs/iris/src/further_topics/lenient_metadata.rst @@ -1,6 +1,6 @@ .. _lenient metadata: -Lenient metadata +Lenient Metadata **************** This section discusses lenient metadata; what it is, what it means, and how you @@ -27,7 +27,7 @@ methods that provide this rich metadata behaviour, all of which are explored more fully in :ref:`metadata`. -Strict behaviour +Strict Behaviour ================ .. testsetup:: strict-behaviour @@ -137,7 +137,7 @@ practical behaviour is available. .. _lenient behaviour: -Lenient behaviour +Lenient Behaviour ================= .. testsetup:: lenient-behaviour @@ -210,7 +210,7 @@ lenient behaviour for each of the metadata classes. .. _lenient equality: -Lenient equality +Lenient Equality ---------------- Lenient equality is enabled using the ``lenient`` keyword argument, therefore @@ -273,7 +273,7 @@ forgiving and practical alternative to strict behaviour. .. _lenient difference: -Lenient difference +Lenient Difference ------------------ Similar to :ref:`lenient equality`, the lenient ``difference`` method @@ -330,7 +330,7 @@ highlights the change in how such dissimilar metadata is treated gracefully, .. _lenient combination: -Lenient combination +Lenient Combination ------------------- The behaviour of the lenient ``combine`` metadata class method is outlined @@ -380,7 +380,7 @@ for more inclusive, richer metadata, .. _lenient members: -Lenient members +Lenient Members --------------- :ref:`lenient behaviour` is not applied regardlessly across all metadata members @@ -429,7 +429,7 @@ strict behaviour, regardlessly. .. _special lenient name: -Special lenient name behaviour +Special Lenient Name Behaviour ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``standard_name``, ``long_name`` and ``var_name`` have a closer association diff --git a/docs/iris/src/further_topics/metadata.rst b/docs/iris/src/further_topics/metadata.rst index 3536c87a2b..e6d6ebc57a 100644 --- a/docs/iris/src/further_topics/metadata.rst +++ b/docs/iris/src/further_topics/metadata.rst @@ -42,7 +42,7 @@ Collectively, the aforementioned classes will be known here as the Iris `SciTools/iris`_ -Common metadata +Common Metadata =============== Each of the Iris `CF Conventions`_ classes use **metadata** to define them and @@ -69,7 +69,7 @@ actual `data attribute`_ names of the metadata members on the Iris class. :align: center =================== ======================================= ============================== ========================================== ================================= ======================== ============================== =================== - Metadata members :class:`~iris.coords.AncillaryVariable` :class:`~iris.coords.AuxCoord` :class:`~iris.aux_factory.AuxCoordFactory` :class:`~iris.coords.CellMeasure` :class:`~iris.cube.Cube` :class:`~iris.coords.DimCoord` Metadata members + Metadata Members :class:`~iris.coords.AncillaryVariable` :class:`~iris.coords.AuxCoord` :class:`~iris.aux_factory.AuxCoordFactory` :class:`~iris.coords.CellMeasure` :class:`~iris.cube.Cube` :class:`~iris.coords.DimCoord` Metadata Members =================== ======================================= ============================== ========================================== ================================= ======================== ============================== =================== ``standard_name`` ✔ ✔ ✔ ✔ ✔ ✔ ``standard_name`` ``long_name`` ✔ ✔ ✔ ✔ ✔ ✔ ``long_name`` @@ -90,7 +90,7 @@ actual `data attribute`_ names of the metadata members on the Iris class. terms. -Common metadata API +Common Metadata API =================== .. testsetup:: @@ -149,7 +149,7 @@ a **common** and **consistent** approach to managing your metadata, which we'll now explore a little more fully. -Metadata classes +Metadata Classes ---------------- The ``metadata`` property will return an appropriate `namedtuple`_ metadata class @@ -162,7 +162,7 @@ each container class is shown in :numref:`metadata classes table` below, :align: center ========================================== ======================================================== - Container class Metadata class + Container Class Metadata Class ========================================== ======================================================== :class:`~iris.coords.AncillaryVariable` :class:`~iris.common.metadata.AncillaryVariableMetadata` :class:`~iris.coords.AuxCoord` :class:`~iris.common.metadata.CoordMetadata` @@ -232,7 +232,7 @@ discussion on options how to **set** and **get** metadata on the instance of an Iris `CF Conventions`_ container class (:numref:`metadata classes table`). -Metadata class behaviour +Metadata Class Behaviour ------------------------ As mentioned previously, the metadata classes in :numref:`metadata classes table` @@ -301,7 +301,7 @@ which we explore next. .. _richer metadata: -Richer metadata behaviour +Richer Metadata Behaviour ------------------------- .. testsetup:: richer-metadata @@ -320,7 +320,7 @@ allows you to easily **compare**, **combine**, **convert** and understand the .. _metadata equality: -Metadata equality +Metadata Equality ^^^^^^^^^^^^^^^^^ The metadata classes support both **equality** (``__eq__``) and **inequality** @@ -357,7 +357,7 @@ a means to enable **lenient** equality, as discussed in :ref:`lenient equality`. .. _strict equality: -Strict equality +Strict Equality """"""""""""""" By default, metadata class equality will perform a **strict** comparison between @@ -426,7 +426,7 @@ However, metadata class equality is rich enough to handle this eventuality, .. _compare like: -Comparing like with like +Comparing Like With Like """""""""""""""""""""""" So far in our journey through metadata class equality, we have only considered @@ -446,7 +446,7 @@ metadata class contains **different** members, as shown in .. _exception rule: -Exception to the rule +Exception to the Rule ~~~~~~~~~~~~~~~~~~~~~ In general, **different** metadata classes cannot be compared, however support @@ -502,7 +502,7 @@ methods of metadata classes. .. _metadata difference: -Metadata difference +Metadata Difference ^^^^^^^^^^^^^^^^^^^ Being able to compare metadata is valuable, especially when we have the @@ -605,7 +605,7 @@ Now, let's compare the two above instances and see what ``attributes`` member di .. _diff like: -Diffing like with like +Diffing Like With Like """""""""""""""""""""" As discussed in :ref:`compare like`, it only makes sense to determine the @@ -655,7 +655,7 @@ In general, however, comparing **different** metadata classes will result in a .. _metadata combine: -Metadata combination +Metadata Combination ^^^^^^^^^^^^^^^^^^^^ .. testsetup:: metadata-combine @@ -740,7 +740,7 @@ metadata class. This is explored in a little further detail next. .. _combine like: -Combine like with like +Combine Like With Like """""""""""""""""""""" Akin to the :ref:`equal ` and @@ -788,7 +788,7 @@ However, note that commutativity in this case cannot be honoured, for obvious re .. _metadata conversion: -Metadata conversion +Metadata Conversion ^^^^^^^^^^^^^^^^^^^ .. testsetup:: metadata-convert @@ -853,7 +853,7 @@ class instance, .. _metadata assignment: -Metadata assignment +Metadata Assignment ^^^^^^^^^^^^^^^^^^^ .. testsetup:: metadata-assign @@ -888,7 +888,7 @@ coordinate, DimCoordMetadata(standard_name='latitude', long_name=None, var_name='latitude', units=Unit('degrees'), attributes={}, coord_system=GeogCS(6371229.0), climatological=False, circular=False) -Assign by iterable +Assign by Iterable """""""""""""""""" It is also possible to assign to the ``metadata`` property of an Iris @@ -903,7 +903,7 @@ number** of associated member values, e.g., DimCoordMetadata(standard_name='latitude', long_name=None, var_name='latitude', units=Unit('degrees'), attributes={}, coord_system=GeogCS(6371229.0), climatological=False, circular=False) -Assign by namedtuple +Assign by Namedtuple """""""""""""""""""" A `namedtuple`_ may also be used to assign to the ``metadata`` property of an @@ -933,7 +933,7 @@ of the ``longitude`` coordinate, DimCoordMetadata(standard_name='latitude', long_name=None, var_name='latitude', units=Unit('degrees'), attributes={}, coord_system=GeogCS(6371229.0), climatological=False, circular=False) -Assign by mapping +Assign by Mapping """"""""""""""""" It is also possible to assign to the ``metadata`` property using a `mapping`_, diff --git a/docs/iris/src/index.rst b/docs/iris/src/index.rst index f230e36f75..80aa696ba1 100644 --- a/docs/iris/src/index.rst +++ b/docs/iris/src/index.rst @@ -46,7 +46,7 @@ For **Iris 2.4** and earlier documentation please see the :container: container-lg pb-3 :column: col-lg-4 col-md-4 col-sm-6 col-xs-12 p-2 - Install Iris to use or for development. + Install Iris as a user or developer. +++ .. link-button:: installing_iris :type: ref @@ -91,7 +91,7 @@ For **Iris 2.4** and earlier documentation please see the .. toctree:: :maxdepth: 1 - :caption: Getting started + :caption: Getting Started :hidden: installing diff --git a/docs/iris/src/installing.rst b/docs/iris/src/installing.rst index 5a81e59c88..cd6a648516 100644 --- a/docs/iris/src/installing.rst +++ b/docs/iris/src/installing.rst @@ -22,7 +22,7 @@ any WSL_ distributions. .. _installing_using_conda: -Installing using conda (users) +Installing Using Conda (Users) ------------------------------ To install Iris using conda, you must first download and install conda, @@ -78,8 +78,8 @@ dependency conflicts might arise or the procedure might have to modified. .. _installing_from_source: -Installing from source with conda (devs) ----------------------------------------- +Installing from Source with Conda (Developers) +---------------------------------------------- The latest Iris source release is available from https://github.com/SciTools/iris. @@ -115,7 +115,7 @@ to find your local Iris code:: python setup.py develop -Running the tests +Running the Tests ----------------- To ensure your setup is configured correctly you can run the test suite using @@ -126,7 +126,7 @@ the command:: For more information see :ref:`developer_running_tests`. -Custom site configuration +Custom Site Configuration ------------------------- The default site configuration values can be overridden by creating the file diff --git a/docs/iris/src/techpapers/change_management.rst b/docs/iris/src/techpapers/change_management.rst index ab45fe7926..f39d64f430 100644 --- a/docs/iris/src/techpapers/change_management.rst +++ b/docs/iris/src/techpapers/change_management.rst @@ -4,7 +4,7 @@ .. _change_management: -Change Management in Iris from the User's perspective +Change Management in Iris From the User's Perspective ***************************************************** As Iris changes, user code will need revising from time to time to keep it @@ -16,7 +16,7 @@ Here, we define ways to make this as easy as possible. .. include:: ../userguide/change_management_goals.txt -Key principles you can rely on +Key Principles you can Rely on ============================== Iris code editions are published as defined version releases, with a given @@ -42,7 +42,7 @@ If your code produces :ref:`deprecation warnings `, then it -User Actions : How you should respond to changes and releases +User Actions : How you Should Respond to Changes and Releases ============================================================= Checklist : @@ -96,7 +96,7 @@ Key concepts covered here: .. _iris_backward_compatibility: -Backwards compatibility +Backwards Compatibility ----------------------- "Backwards-compatible" changes are those that leave any existing valid API @@ -135,7 +135,7 @@ See :ref:`Usage of iris.FUTURE `, below. .. _iris_api: -Terminology : API, features, usages and behaviours +Terminology : API, Features, Usages and Behaviours ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The API is the components of the iris module and its submodules which are @@ -320,7 +320,7 @@ This is to warn users : * eventually to rewrite old code to use the newer or better alternatives -Deprecated features support through the Release cycle +Deprecated Features Support Through the Release Cycle ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The whole point of a deprecation is that the feature continues to work, but @@ -341,7 +341,7 @@ follows: .. _iris_future_usage: -Future options, `iris.FUTURE` +Future Options, `iris.FUTURE` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A special approach is needed where the replacement behaviour is not controlled diff --git a/docs/iris/src/techpapers/index.rst b/docs/iris/src/techpapers/index.rst index 3074569eae..773c8f7059 100644 --- a/docs/iris/src/techpapers/index.rst +++ b/docs/iris/src/techpapers/index.rst @@ -1,7 +1,7 @@ .. _techpapers_index: -Iris technical papers +Iris Technical Papers ===================== Extra information on specific technical issues. diff --git a/docs/iris/src/techpapers/missing_data_handling.rst b/docs/iris/src/techpapers/missing_data_handling.rst index 46279bc566..13b00d3424 100644 --- a/docs/iris/src/techpapers/missing_data_handling.rst +++ b/docs/iris/src/techpapers/missing_data_handling.rst @@ -1,5 +1,5 @@ ============================= -Missing data handling in Iris +Missing Data Handling in Iris ============================= This document provides a brief overview of how Iris handles missing data values @@ -73,7 +73,7 @@ all have the same fill-value. If the components have differing fill-values, a default fill-value will be used instead. -Other operations +Other Operations ---------------- Other operations, such as :class:`~iris.cube.Cube` arithmetic operations, diff --git a/docs/iris/src/techpapers/um_files_loading.rst b/docs/iris/src/techpapers/um_files_loading.rst index d8c796b31f..72d34962ce 100644 --- a/docs/iris/src/techpapers/um_files_loading.rst +++ b/docs/iris/src/techpapers/um_files_loading.rst @@ -14,7 +14,7 @@ =================================== -Iris handling of PP and Fieldsfiles +Iris Handling of PP and Fieldsfiles =================================== This document provides a basic account of how PP and Fieldsfiles data is @@ -40,7 +40,7 @@ For details of Iris terms (cubes, coordinates, attributes), refer to For details of CF conventions, see http://cfconventions.org/. -Overview of loading process +Overview of Loading Process --------------------------- The basics of Iris loading are explained at :ref:`loading_iris_cubes`. @@ -165,7 +165,7 @@ For example: sections are written only if the actual values are unevenly spaced. -Phenomenon identification +Phenomenon Identification ------------------------- **UM Field elements** @@ -218,7 +218,7 @@ For example: LBUSER4 and LBUSER7 elements. -Vertical coordinates +Vertical Coordinates -------------------- **UM Field elements** @@ -319,7 +319,7 @@ See an example printout of a hybrid height cube, .. _um_time_metadata: -Time information +Time Information ---------------- **UM Field elements** @@ -391,7 +391,7 @@ See an example printout of a forecast data cube, 'forecast_reference_time' is a constant. -Statistical measures +Statistical Measures -------------------- **UM Field elements** @@ -438,7 +438,7 @@ For example: (CellMethod(method='mean', coord_names=('time',), intervals=('6 hour',), comments=()),) -Other metadata +Other Metadata -------------- LBRSVD4 diff --git a/docs/iris/src/userguide/citation.rst b/docs/iris/src/userguide/citation.rst index f91bc670f0..0a3a85fb89 100644 --- a/docs/iris/src/userguide/citation.rst +++ b/docs/iris/src/userguide/citation.rst @@ -8,7 +8,7 @@ If Iris played an important part in your research then please add us to your reference list by using one of the recommendations below. ************ -BibTeX entry +BibTeX Entry ************ For example:: @@ -24,7 +24,7 @@ For example:: ******************* -Downloaded software +Downloaded Software ******************* Suggested format:: @@ -37,7 +37,7 @@ For example:: ******************** -Checked out software +Checked Out Software ******************** Suggested format:: diff --git a/docs/iris/src/userguide/code_maintenance.rst b/docs/iris/src/userguide/code_maintenance.rst index d03808e18f..b2b498bc80 100644 --- a/docs/iris/src/userguide/code_maintenance.rst +++ b/docs/iris/src/userguide/code_maintenance.rst @@ -1,11 +1,11 @@ -Code maintenance +Code Maintenance ================ From a user point of view "code maintenance" means ensuring that your existing working code stays working, in the face of changes to Iris. -Stability and change +Stability and Change --------------------- In practice, as Iris develops, most users will want to periodically upgrade @@ -25,7 +25,7 @@ maintenance effort is probably still necessary: for some completely unconnected reason. -Principles of change management +Principles of Change Management ------------------------------- When you upgrade software to a new version, you often find that you need to diff --git a/docs/iris/src/userguide/cube_maths.rst b/docs/iris/src/userguide/cube_maths.rst index 1b1b2dbe66..d2d4d84b68 100644 --- a/docs/iris/src/userguide/cube_maths.rst +++ b/docs/iris/src/userguide/cube_maths.rst @@ -1,7 +1,7 @@ .. _cube maths: ========== -Cube maths +Cube Maths ========== @@ -29,7 +29,7 @@ In order to reduce the amount of metadata which becomes inconsistent, fundamental arithmetic operations such as addition, subtraction, division and multiplication can be applied directly to any cube. -Calculating the difference between two cubes +Calculating the Difference Between Two Cubes -------------------------------------------- Let's load some air temperature which runs from 1860 to 2100:: @@ -77,7 +77,7 @@ but with the data representing their difference: .. _cube-maths_anomaly: -Calculating a cube anomaly +Calculating a Cube Anomaly -------------------------- In section :doc:`cube_statistics` we discussed how the dimensionality of a cube @@ -165,7 +165,7 @@ broadcasting behaviour:: >>> print(result.summary(True)) unknown / (K) (time: 240; latitude: 37; longitude: 49) -Combining multiple phenomena to form a new one +Combining Multiple Phenomena to Form a New One ---------------------------------------------- Combining cubes of potential-temperature and pressure we can calculate @@ -223,7 +223,7 @@ The result could now be plotted using the guidance provided in the .. _cube_maths_combining_units: -Combining units +Combining Units --------------- It should be noted that when combining cubes by multiplication, division or diff --git a/docs/iris/src/userguide/cube_statistics.rst b/docs/iris/src/userguide/cube_statistics.rst index 310551c76f..4eb016078e 100644 --- a/docs/iris/src/userguide/cube_statistics.rst +++ b/docs/iris/src/userguide/cube_statistics.rst @@ -1,12 +1,12 @@ .. _cube-statistics: =============== -Cube statistics +Cube Statistics =============== .. _cube-statistics-collapsing: -Collapsing entire data dimensions +Collapsing Entire Data Dimensions --------------------------------- .. testsetup:: @@ -100,7 +100,7 @@ in the gallery takes a zonal mean of an ``XYT`` cube by using the .. _cube-statistics-collapsing-average: -Area averaging +Area Averaging ^^^^^^^^^^^^^^ Some operators support additional keywords to the ``cube.collapsed`` method. @@ -152,14 +152,14 @@ including an example on taking a :ref:`global area-weighted mean .. _cube-statistics-aggregated-by: -Partially reducing data dimensions +Partially Reducing Data Dimensions ---------------------------------- Instead of completely collapsing a dimension, other methods can be applied to reduce or filter the number of data points of a particular dimension. -Aggregation of grouped data +Aggregation of Grouped Data ^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :meth:`Cube.aggregated_by ` operation diff --git a/docs/iris/src/userguide/interpolation_and_regridding.rst b/docs/iris/src/userguide/interpolation_and_regridding.rst index ffed21a7f5..5a5a985ccb 100644 --- a/docs/iris/src/userguide/interpolation_and_regridding.rst +++ b/docs/iris/src/userguide/interpolation_and_regridding.rst @@ -8,7 +8,7 @@ warnings.simplefilter('ignore') ================================= -Cube interpolation and regridding +Cube Interpolation and Regridding ================================= Iris provides powerful cube-aware interpolation and regridding functionality, @@ -123,7 +123,7 @@ will be orthogonal: air_temperature / (K) (latitude: 13; longitude: 14) -Interpolating non-horizontal coordinates +Interpolating Non-Horizontal Coordinates ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Interpolation in Iris is not limited to horizontal-spatial coordinates - any @@ -195,7 +195,7 @@ For example, to mask values that lie beyond the range of the original data: .. _caching_an_interpolator: -Caching an interpolator +Caching an Interpolator ^^^^^^^^^^^^^^^^^^^^^^^ If you need to interpolate a cube on multiple sets of sample points you can @@ -305,7 +305,7 @@ cells have now become rectangular in a plate carrée (equirectangular) projectio The spatial grid of the resulting cube is really global, with a large proportion of the data being masked. -Area-weighted regridding +Area-Weighted Regridding ^^^^^^^^^^^^^^^^^^^^^^^^ It is often the case that a point-based regridding scheme (such as @@ -384,7 +384,7 @@ To visualise the above regrid, let's plot the original data, along with 3 distin .. _caching_a_regridder: -Caching a regridder +Caching a Regridder ^^^^^^^^^^^^^^^^^^^ If you need to regrid multiple cubes with a common source grid onto a common @@ -415,7 +415,7 @@ In each case ``result`` will be the input cube regridded to the grid defined by the target grid cube (in this case ``rotated_psl``) that we used to define the cached regridder. -Regridding lazy data +Regridding Lazy Data ^^^^^^^^^^^^^^^^^^^^ If you are working with large cubes, especially when you are regridding to a diff --git a/docs/iris/src/userguide/iris_cubes.rst b/docs/iris/src/userguide/iris_cubes.rst index 5929c402f2..de206486d3 100644 --- a/docs/iris/src/userguide/iris_cubes.rst +++ b/docs/iris/src/userguide/iris_cubes.rst @@ -1,7 +1,7 @@ .. _iris_data_structures: ==================== -Iris data structures +Iris Data Structures ==================== The top level object in Iris is called a cube. A cube contains data and metadata about a phenomenon. @@ -71,11 +71,11 @@ A cube consists of: * a list of coordinate "factories" used for deriving coordinates from the values of other coordinates in the cube -Cubes in practice +Cubes in Practice ----------------- -A simple cube example +A Simple Cube Example ===================== Suppose we have some gridded data which has 24 air temperature readings (in Kelvin) which is located at @@ -137,7 +137,7 @@ For example, it is possible to attach any of the following: a collection of "ensembles" (i.e. multiple model runs). -Printing a cube +Printing a Cube =============== Every Iris cube can be printed to screen as you will see later in the user guide. It is worth familiarising yourself with the diff --git a/docs/iris/src/userguide/loading_iris_cubes.rst b/docs/iris/src/userguide/loading_iris_cubes.rst index 006a919408..659c28420a 100644 --- a/docs/iris/src/userguide/loading_iris_cubes.rst +++ b/docs/iris/src/userguide/loading_iris_cubes.rst @@ -1,7 +1,7 @@ .. _loading_iris_cubes: =================== -Loading Iris cubes +Loading Iris Cubes =================== To load a single file into a **list** of Iris cubes @@ -116,7 +116,7 @@ This was the output discussed at the end of the :doc:`iris_cubes` section. appropriate column for each cube data dimension that they describe. -Loading multiple files +Loading Multiple Files ----------------------- To load more than one file into a list of cubes, a list of filenames can be @@ -142,7 +142,7 @@ star wildcards can be used:: The cubes returned will not necessarily be in the same order as the order of the filenames. -Lazy loading +Lazy Loading ------------ In fact when Iris loads data from most file types, it normally only reads the @@ -155,7 +155,7 @@ For more on the benefits, handling and uses of lazy data, see :doc:`Real and Laz .. _constrained-loading: -Constrained loading +Constrained Loading ----------------------- Given a large dataset, it is possible to restrict or constrain the load to match specific Iris cube metadata. @@ -261,7 +261,7 @@ then specific STASH codes can be filtered:: :class:`iris.Constraint` reference documentation. -Constraining a circular coordinate across its boundary +Constraining a Circular Coordinate Across its Boundary ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Occasionally you may need to constrain your cube with a region that crosses the @@ -403,7 +403,7 @@ Notice how the dates printed are between the range specified in the ``st_swithun and that they span multiple years. -Strict loading +Strict Loading -------------- The :py:func:`iris.load_cube` and :py:func:`iris.load_cubes` functions are diff --git a/docs/iris/src/userguide/merge_and_concat.rst b/docs/iris/src/userguide/merge_and_concat.rst index 0d844ac403..ffa36ccdeb 100644 --- a/docs/iris/src/userguide/merge_and_concat.rst +++ b/docs/iris/src/userguide/merge_and_concat.rst @@ -1,7 +1,7 @@ .. _merge_and_concat: ===================== -Merge and concatenate +Merge and Concatenate ===================== We saw in the :doc:`loading_iris_cubes` chapter that Iris tries to load as few cubes as @@ -203,7 +203,7 @@ single cube. An example of fixing an issue like this can be found in the :ref:`merge_concat_common_issues` section. -Merge in Iris load +Merge in Iris Load ================== The CubeList's :meth:`~iris.cube.CubeList.merge` method is used internally @@ -365,7 +365,7 @@ single cube. An example of fixing an issue like this can be found in the .. _merge_concat_common_issues: -Common issues with merge and concatenate +Common Issues With Merge and Concatenate ---------------------------------------- The Iris algorithms that drive :meth:`~iris.cube.CubeList.merge` and @@ -529,7 +529,7 @@ Trying to merge the input cubes with duplicate cubes not allowed raises an error highlighting the presence of the duplicate cube. -**Single value coordinates** +**Single Value Coordinates** Coordinates containing only a single value can cause confusion when combining input cubes. Remember: diff --git a/docs/iris/src/userguide/navigating_a_cube.rst b/docs/iris/src/userguide/navigating_a_cube.rst index a7b7717ae3..df18c032c1 100644 --- a/docs/iris/src/userguide/navigating_a_cube.rst +++ b/docs/iris/src/userguide/navigating_a_cube.rst @@ -1,5 +1,5 @@ ================= -Navigating a cube +Navigating a Cube ================= .. testsetup:: @@ -15,7 +15,7 @@ Navigating a cube After loading any cube, you will want to investigate precisely what it contains. This section is all about accessing and manipulating the metadata contained within a cube. -Cube string representations +Cube String Representations --------------------------- We have already seen a basic string representation of a cube when printing: @@ -52,7 +52,7 @@ variable. In most cases it is reasonable to ignore anything starting with a "``_ dir(cube) help(cube) -Working with cubes +Working With Cubes ------------------ Every cube has a standard name, long name and units which are accessed with @@ -111,7 +111,7 @@ cube with the :attr:`Cube.cell_methods ` attribute: print(cube.cell_methods) -Accessing coordinates on the cube +Accessing Coordinates on the Cube --------------------------------- A cube's coordinates can be retrieved via :meth:`Cube.coords `. @@ -148,7 +148,7 @@ numpy array. If the coordinate has no bounds ``None`` will be returned:: print(type(coord.bounds)) -Adding metadata to a cube +Adding Metadata to a Cube ------------------------- We can add and remove coordinates via :func:`Cube.add_dim_coord`, @@ -177,7 +177,7 @@ We can add and remove coordinates via :func:`Cube.add_dim_coord`_ package in order to generate @@ -13,7 +13,7 @@ been extended within Iris to facilitate easy visualisation of a cube's data. *************************** -Matplotlib's pyplot basics +Matplotlib's Pyplot Basics *************************** A simple line plot can be created using the @@ -35,7 +35,7 @@ There are two modes of rendering within Matplotlib; **interactive** and **non-interactive**. -Interactive plot rendering +Interactive Plot Rendering ========================== The previous example was *non-interactive* as the figure is only rendered *after* the call to :py:func:`plt.show() `. @@ -84,7 +84,7 @@ so ensure that interactive mode is turned off with:: plt.interactive(False) -Saving a plot +Saving a Plot ============= The :py:func:`matplotlib.pyplot.savefig` function is similar to **plt.show()** @@ -113,7 +113,7 @@ Some of the formats which are supported by **plt.savefig**: ====== ====== ====================================================================== ****************** -Iris cube plotting +Iris Cube Plotting ****************** The Iris modules :py:mod:`iris.quickplot` and :py:mod:`iris.plot` extend the @@ -149,7 +149,7 @@ where appropriate. import iris.quickplot as qplt -Plotting 1-dimensional cubes +Plotting 1-Dimensional Cubes ============================ The simplest 1D plot is achieved with the :py:func:`iris.plot.plot` function. @@ -181,7 +181,7 @@ For example, the previous plot can be improved quickly by replacing -Multi-line plot +Multi-Line Plot --------------- A multi-lined (or over-plotted) plot, with a legend, can be achieved easily by @@ -212,10 +212,10 @@ the temperature at some latitude cross-sections. and run it using ``python my_file.py``. -Plotting 2-dimensional cubes +Plotting 2-Dimensional Cubes ============================ -Creating maps +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 @@ -230,7 +230,7 @@ things. :meth:`cartopy's coastlines() `. -Cube contour +Cube Contour ------------ A simple contour plot of a cube can be created with either the :func:`iris.plot.contour` or :func:`iris.quickplot.contour` functions: @@ -239,7 +239,7 @@ A simple contour plot of a cube can be created with either the :include-source: -Cube filled contour +Cube Filled Contour ------------------- Similarly a filled contour plot of a cube can be created with the :func:`iris.plot.contourf` or :func:`iris.quickplot.contourf` functions: @@ -248,7 +248,7 @@ Similarly a filled contour plot of a cube can be created with the :include-source: -Cube block plot +Cube Block Plot --------------- In some situations the underlying coordinates are better represented with a continuous bounded coordinate, in which case a "block" plot may be more @@ -268,7 +268,7 @@ or :func:`iris.quickplot.pcolormesh`. .. _brewer-info: *********************** -Brewer colour palettes +Brewer Colour Palettes *********************** Iris includes colour specifications and designs developed by @@ -303,7 +303,7 @@ The following subset of Brewer palettes found at .. plot:: userguide/plotting_examples/brewer.py -Plotting with Brewer +Plotting With Brewer ==================== To plot a cube using a Brewer colour palette, simply select one of the Iris @@ -316,7 +316,7 @@ become available once :mod:`iris.plot` or :mod:`iris.quickplot` are imported. .. _brewer-cite: -Adding a citation +Adding a Citation ================= Citations can be easily added to a plot using the diff --git a/docs/iris/src/userguide/real_and_lazy_data.rst b/docs/iris/src/userguide/real_and_lazy_data.rst index 574ca4e1a0..0bc1846457 100644 --- a/docs/iris/src/userguide/real_and_lazy_data.rst +++ b/docs/iris/src/userguide/real_and_lazy_data.rst @@ -10,7 +10,7 @@ ================== -Real and lazy data +Real and Lazy Data ================== We have seen in the :doc:`iris_cubes` section of the user guide that @@ -21,7 +21,7 @@ In this section of the user guide we will look specifically at the concepts of real and lazy data as they apply to the cube and other data structures in Iris. -What is real and lazy data? +What is Real and Lazy Data? --------------------------- In Iris, we use the term **real data** to describe data arrays that are loaded @@ -97,7 +97,7 @@ In such cases, a required portion can be extracted and realised without calculat .. _when_real_data: -When does my data become real? +When Does My Data Become Real? ------------------------------ Certain operations, such as cube indexing and statistics, can be @@ -134,7 +134,7 @@ You can also realise (and so load into memory) your cube's lazy data if you 'tou To 'touch' the data means directly accessing the data by calling ``cube.data``, as in the previous example. -Core data +Core Data ^^^^^^^^^ Cubes have the concept of "core data". This returns the cube's data in its @@ -225,7 +225,7 @@ coordinates' lazy points and bounds: Printing a lazy :class:`~iris.coords.AuxCoord` will realise its points and bounds arrays! -Dask processing options +Dask Processing Options ----------------------- Iris uses dask to provide lazy data arrays for both Iris cubes and coordinates, diff --git a/docs/iris/src/userguide/saving_iris_cubes.rst b/docs/iris/src/userguide/saving_iris_cubes.rst index 3a30321979..237ceb18b6 100644 --- a/docs/iris/src/userguide/saving_iris_cubes.rst +++ b/docs/iris/src/userguide/saving_iris_cubes.rst @@ -1,7 +1,7 @@ .. _saving_iris_cubes: ================== -Saving Iris cubes +Saving Iris Cubes ================== Iris supports the saving of cubes and cube lists to: @@ -39,8 +39,8 @@ and the keyword argument `saver` is not required. attempting to overwrite an existing file. -Controlling the save process ------------------------------ +Controlling the Save Process +---------------------------- The :py:func:`iris.save` function passes all other keywords through to the saver function defined, or automatically set from the file extension. This enables saver specific functionality to be called. @@ -73,8 +73,8 @@ See for more details on supported arguments for the individual savers. -Customising the save process ------------------------------ +Customising the Save Process +---------------------------- When saving to GRIB or PP, the save process may be intercepted between the translation step and the file writing. This enables customisation of the output messages, based on Cube metadata if required, over and above the translations supplied by Iris. @@ -103,14 +103,14 @@ Similarly a PP field may need to be written out with a specific value for LBEXP. iris.fileformats.pp.save_fields(tweaked_fields(cubes[0]), '/tmp/app.pp') -netCDF -^^^^^^^ +NetCDF +^^^^^^ NetCDF is a flexible container for metadata and cube metadata is closely related to the CF for netCDF semantics. This means that cube metadata is well represented in netCDF files, closely resembling the in memory metadata representation. Thus there is no provision for similar save customisation functionality for netCDF saving, all customisations should be applied to the cube prior to saving to netCDF. -Bespoke saver --------------- +Bespoke Saver +------------- A bespoke saver may be written to support an alternative file format. This can be provided to the :py:func:`iris.save` function, enabling Iris to write to a different file format. Such a custom saver will need be written to meet the needs of the file format and to handle the metadata translation from cube metadata effectively. diff --git a/docs/iris/src/userguide/subsetting_a_cube.rst b/docs/iris/src/userguide/subsetting_a_cube.rst index 5d9a560be9..02cf1645a1 100644 --- a/docs/iris/src/userguide/subsetting_a_cube.rst +++ b/docs/iris/src/userguide/subsetting_a_cube.rst @@ -1,7 +1,7 @@ .. _subsetting_a_cube: ================= -Subsetting a cube +Subsetting a Cube ================= The :doc:`loading_iris_cubes` section of the user guide showed how to load data into multidimensional Iris cubes. @@ -11,7 +11,7 @@ Iris provides several ways of reducing both the amount of data and/or the number In all cases **the subset of a valid cube is itself a valid cube**. -Cube extraction +Cube Extraction ^^^^^^^^^^^^^^^^ A subset of a cube can be "extracted" from a multi-dimensional cube in order to reduce its dimensionality: @@ -101,7 +101,7 @@ same way as loading with constraints: um_version: 7.3 -Cube iteration +Cube Iteration ^^^^^^^^^^^^^^^ It is not possible to directly iterate over an Iris cube. That is, you cannot use code such as ``for x in cube:``. However, you can iterate over cube slices, as this section details. @@ -152,7 +152,7 @@ slicing the 3 dimensional cube (15, 100, 100) by longitude (i starts at 0 and 15 cube using the slices method. -Cube indexing +Cube Indexing ^^^^^^^^^^^^^ In the same way that you would expect a numeric multidimensional array to be **indexed** to take a subset of your original array, you can **index** a Cube for the same purpose. diff --git a/docs/iris/src/whatsnew/1.0.rst b/docs/iris/src/whatsnew/1.0.rst index 11d29320b6..b226dc609b 100644 --- a/docs/iris/src/whatsnew/1.0.rst +++ b/docs/iris/src/whatsnew/1.0.rst @@ -10,7 +10,7 @@ work. Following this release we plan to deliver significant performance improvements and additional features. -The role of 1.x +The Role of 1.x =============== The 1.x series of releases is intended to provide a relatively stable, @@ -58,7 +58,7 @@ A summary of the main features added with version 1.0: contain bounds. -CF-netCDF coordinate systems +CF-NetCDF Coordinate Systems ---------------------------- The coordinate systems in Iris are now defined by the CF-netCDF @@ -73,7 +73,7 @@ The coordinate systems available in Iris 1.0 and their corresponding Iris classes are: ================================================================================================================= ========================================= -CF name Iris class +CF Name Iris Class ================================================================================================================= ========================================= `Latitude-longitude `_ :class:`~iris.coord_systems.GeogCS` `Rotated pole `_ :class:`~iris.coord_systems.RotatedGeogCS` @@ -88,7 +88,7 @@ coordinate system used by the British .. _whats-new-cartopy: -Using Cartopy for mapping in matplotlib +Using Cartopy for Mapping in Matplotlib --------------------------------------- The underlying map drawing package has now been updated to use @@ -135,7 +135,7 @@ For more examples of what can be done with Cartopy, see the Iris gallery and `Cartopy's documentation `_. -Hybrid-pressure +Hybrid-Pressure --------------- With the introduction of the :class:`~iris.aux_factory.HybridPressureFactory` @@ -181,7 +181,7 @@ dealing with large numbers of netCDF files, or in long running processes. -Brewer colour palettes +Brewer Colour Palettes ---------------------- Iris includes a selection of carefully designed colour palettes produced @@ -207,7 +207,7 @@ To include a reference in a journal article or report please refer to in the citation guidance provided by Cynthia Brewer. -Metadata attributes +Metadata Attributes ------------------- Iris now stores "source" and "history" metadata in Cube attributes. @@ -241,7 +241,7 @@ Where previously it would have appeared as:: cube.add_aux_coord(src_coord) -New loading functions +New Loading Functions --------------------- The main functions for loading cubes are now: @@ -264,7 +264,7 @@ now use the :func:`iris.load_cube()` and :func:`iris.load_cubes()` functions instead. -Cube projection +Cube Projection --------------- Iris now has the ability to project a cube into a number of map projections. @@ -302,7 +302,7 @@ preserved. This function currently assumes global data and will if necessary extrapolate beyond the geographical extent of the source cube. -Incompatible changes +Incompatible Changes ==================== * The "source" and "history" metadata are now represented as Cube diff --git a/docs/iris/src/whatsnew/1.1.rst b/docs/iris/src/whatsnew/1.1.rst index f2b0995fa0..86f0bb16fa 100644 --- a/docs/iris/src/whatsnew/1.1.rst +++ b/docs/iris/src/whatsnew/1.1.rst @@ -44,7 +44,7 @@ some notable improvements to netCDF/PP import. with product template 4.9. -Coordinate categorisation +Coordinate Categorisation ------------------------- An :func:`~iris.coord_categorisation.add_day_of_year` categorisation @@ -52,7 +52,7 @@ function has been added to the existing suite in :mod:`iris.coord_categorisation`. -Custom seasons +Custom Seasons ~~~~~~~~~~~~~~ The conventional seasonal categorisation functions have been @@ -87,7 +87,7 @@ This function adds a coordinate containing True/False values determined by membership of a single custom season. -Bugs fixed +Bugs Fixed ========== * PP export no longer attempts to set/overwrite the STASH code based on diff --git a/docs/iris/src/whatsnew/1.10.rst b/docs/iris/src/whatsnew/1.10.rst index 3f51287fa1..92822087dd 100644 --- a/docs/iris/src/whatsnew/1.10.rst +++ b/docs/iris/src/whatsnew/1.10.rst @@ -1,5 +1,5 @@ v1.10 (05 Sep 2016) -********************* +******************* This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -137,7 +137,7 @@ Features attributes is now allowed. -Bugs fixed +Bugs Fixed ========== * Altered Cell Methods to display coordinate's standard_name rather than @@ -215,7 +215,7 @@ Bugs fixed thrown while trying to subset over a non-dimensional scalar coordinate. -Incompatible changes +Incompatible Changes ==================== * The source and target for diff --git a/docs/iris/src/whatsnew/1.11.rst b/docs/iris/src/whatsnew/1.11.rst index e0d46d0f09..356e6ec85b 100644 --- a/docs/iris/src/whatsnew/1.11.rst +++ b/docs/iris/src/whatsnew/1.11.rst @@ -16,7 +16,7 @@ Features * The coordinate system :class:`iris.coord_systems.LambertAzimuthalEqualArea` has been added with NetCDF saving support. -Bugs fixed +Bugs Fixed ========== * Fixed a floating point tolerance bug in diff --git a/docs/iris/src/whatsnew/1.13.rst b/docs/iris/src/whatsnew/1.13.rst index 2d3b3ffce5..028c298505 100644 --- a/docs/iris/src/whatsnew/1.13.rst +++ b/docs/iris/src/whatsnew/1.13.rst @@ -1,5 +1,5 @@ v1.13 (17 May 2017) -************************* +******************* This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -17,7 +17,7 @@ Features :meth:`iris.cube.share_data` flag. -Bug fixes +Bug Fixes ========= * The bounds are now set correctly on the longitude coordinate if a zonal mean diff --git a/docs/iris/src/whatsnew/1.2.rst b/docs/iris/src/whatsnew/1.2.rst index d4bb863a3b..dce0b6dc04 100644 --- a/docs/iris/src/whatsnew/1.2.rst +++ b/docs/iris/src/whatsnew/1.2.rst @@ -44,7 +44,7 @@ Features :class:`~iris.cube.Cube`. -Bugs fixed +Bugs Fixed ========== * The GRIB hindcast interpretation of negative forecast times can be enabled @@ -54,7 +54,7 @@ Bugs fixed coordinates. -Incompatible changes +Incompatible Changes ==================== * The deprecated :attr:`iris.cube.Cube.unit` and :attr:`iris.coords.Coord.unit` diff --git a/docs/iris/src/whatsnew/1.3.rst b/docs/iris/src/whatsnew/1.3.rst index 9a2ac2eba1..beaa594ab5 100644 --- a/docs/iris/src/whatsnew/1.3.rst +++ b/docs/iris/src/whatsnew/1.3.rst @@ -30,7 +30,7 @@ Features .. _whats-new-abf: -Loading ABF/ABL files +Loading ABF/ABL Files --------------------- Support for the ABF and ABL file formats (as @@ -51,7 +51,7 @@ For example:: .. _whats-new-cf-profile: -Customised CF profiles +Customised CF Profiles ---------------------- Iris now provides hooks in the CF-netCDF export process to allow @@ -74,7 +74,7 @@ For further implementation details see ``iris/fileformats/netcdf.py``. .. _whats-new-concat: -Cube concatenation +Cube Concatenation ------------------ Iris now provides initial support for concatenating Cubes along one or @@ -101,7 +101,7 @@ combine these into a single Cube as follows:: As this is an experimental feature, your feedback is especially welcome. -Bugs fixed +Bugs Fixed ========== * Printing a Cube now supports Unicode attribute values. @@ -123,7 +123,7 @@ Deprecations naming conventions. ====================================== =========================================== - Deprecated property/method New method + Deprecated Property/Method New Method ====================================== =========================================== :meth:`~iris.unit.Unit.convertible()` :meth:`~iris.unit.Unit.is_convertible()` :attr:`~iris.unit.Unit.dimensionless` :meth:`~iris.unit.Unit.is_dimensionless()` diff --git a/docs/iris/src/whatsnew/1.4.rst b/docs/iris/src/whatsnew/1.4.rst index 29f2079af8..858f985ec6 100644 --- a/docs/iris/src/whatsnew/1.4.rst +++ b/docs/iris/src/whatsnew/1.4.rst @@ -61,7 +61,7 @@ Features .. _OPeNDAP: http://www.opendap.org/about .. _exp-regrid: -Experimental regridding enhancements +Experimental Regridding Enhancements ------------------------------------ Bilinear, area-weighted and area-conservative regridding functions are now @@ -72,7 +72,7 @@ development. In the meantime: -Bilinear rectilinear regridding +Bilinear Rectilinear Regridding ------------------------------- :func:`~iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid` @@ -85,7 +85,7 @@ For example:: regridded_cube = regrid_bilinear_rectilinear_src_and_grid(source_cube, target_grid_cube) -Area-weighted regridding +Area-Weighted Regridding ------------------------ :func:`~iris.experimental.regrid.regrid_area_weighted_rectilinear_src_and_grid` @@ -98,7 +98,7 @@ For example:: regridded_cube = regrid_area_weighted(source_cube, target_grid_cube) -Area-conservative regridding +Area-Conservative Regridding ---------------------------- :func:`~iris.experimental.regrid_conservative.regrid_conservative_via_esmpy` @@ -113,7 +113,7 @@ For example:: .. _iris-pandas: -Iris-Pandas interoperability +Iris-Pandas Interoperability ---------------------------- Conversion to and from Pandas Series_ and DataFrames_ is now available. @@ -125,7 +125,7 @@ See :mod:`iris.pandas` for more details. .. _load-opendap: -Load cubes from the internet via OPeNDAP +Load Cubes From the Internet via OPeNDAP ---------------------------------------- Cubes can now be loaded directly from the internet, via OPeNDAP_. @@ -137,7 +137,7 @@ For example:: .. _geotiff_export: -GeoTiff export +GeoTiff Export -------------- With this experimental feature, two dimensional cubes can now be exported to @@ -155,7 +155,7 @@ For example:: .. _cube-merge-update: -Cube merge update +Cube Merge Update ----------------- Cube merging now favours numerical coordinates over string coordinates @@ -167,7 +167,7 @@ dimensions"*. .. _season-year-name: -Unambiguous season year naming +Unambiguous Season Year Naming ------------------------------ The default names of categorisation coordinates are now less ambiguous. @@ -178,7 +178,7 @@ For example, :func:`~iris.coord_categorisation.add_month_number` and .. _grib-novert: -Cubes with no vertical coord can now be exported to GRIB +Cubes With no Vertical Coord can now be Exported to GRIB -------------------------------------------------------- Iris can now export cubes with no vertical coord to GRIB. @@ -188,7 +188,7 @@ https://github.com/SciTools/iris/issues/519. .. _simple_cfg: -Simplified resource configuration +Simplified Resource Configuration --------------------------------- A new configuration variable called :data:`iris.config.TEST_DATA_DIR` @@ -202,7 +202,7 @@ be set by adding a ``test_data_dir`` entry to the ``Resources`` section of .. _grib_params: -Extended GRIB parameter translation +Extended GRIB Parameter Translation ----------------------------------- - More GRIB2 params are recognised on input. @@ -213,7 +213,7 @@ Extended GRIB parameter translation .. _one-d-linear: -One dimensional linear interpolation fix +One dimensional Linear Interpolation Fix ---------------------------------------- :func:`~iris.analysis.interpolate.linear` can now extrapolate from a single @@ -232,7 +232,7 @@ to cause the loss of coordinate metadata when calculating the curl or the derivative of a cube has been fixed. -Incompatible changes +Incompatible Changes ==================== * As part of simplifying the mechanism for accessing test data, diff --git a/docs/iris/src/whatsnew/1.5.rst b/docs/iris/src/whatsnew/1.5.rst index ea7965fe15..72bdbac480 100644 --- a/docs/iris/src/whatsnew/1.5.rst +++ b/docs/iris/src/whatsnew/1.5.rst @@ -125,7 +125,7 @@ Features systems and mapping 0 to 360 longitudes to the -180 to 180 range. -Bugs fixed +Bugs Fixed ========== * NetCDF error handling on save has been extended to capture file path and diff --git a/docs/iris/src/whatsnew/1.6.rst b/docs/iris/src/whatsnew/1.6.rst index 3855d71479..8b0205b86f 100644 --- a/docs/iris/src/whatsnew/1.6.rst +++ b/docs/iris/src/whatsnew/1.6.rst @@ -146,7 +146,7 @@ Features .. _caching: -A new utility function to assist with caching +A New Utility Function to Assist With Caching --------------------------------------------- To assist with management of caching results to file, the new utility function :func:`iris.util.file_is_newer_than` may be used to easily determine whether @@ -173,7 +173,7 @@ consuming processing, or to reap the benefit of fast-loading a pickled cube. .. _rms: -The RMS aggregator supports weights +The RMS Aggregator Supports Weights ----------------------------------- The :data:`iris.analysis.RMS` aggregator has been extended to allow the use of @@ -189,7 +189,7 @@ For example, an RMS weighted cube collapse is performed as follows: .. _equalise: -Equalise cube attributes +Equalise Cube Attributes ------------------------ To assist with :class:`iris.cube.Cube` merging, the new experimental in-place @@ -202,7 +202,7 @@ have the same attributes. .. _tolerance: -Masking a collapsed result by missing-data tolerance +Masking a Collapsed Result by Missing-Data Tolerance ---------------------------------------------------- The result from collapsing masked cube data may now be completely @@ -216,7 +216,7 @@ less than or equal to the provided tolerance. .. _promote: -Promote a scalar coordinate +Promote a Scalar Coordinate --------------------------- The new utility function :func:`iris.util.new_axis` creates a new cube with @@ -229,7 +229,7 @@ Note that, this function will load the data payload of the cube. .. _peak: -A new PEAK aggregator providing spline interpolation +A New PEAK Aggregator Providing Spline Interpolation ---------------------------------------------------- The new :data:`iris.analysis.PEAK` aggregator calculates the global peak @@ -244,7 +244,7 @@ For example, to calculate the peak time: collapsed_cube = cube.collapsed('time', PEAK) -Bugs fixed +Bugs Fixed ========== * :meth:`iris.cube.Cube.rolling_window` has been extended to support masked @@ -283,7 +283,7 @@ Bugs fixed * Exception no longer raised for any ellipsoid definition in nimrod loading. -Incompatible changes +Incompatible Changes ==================== * The experimental 'concatenate' function is now a method of a @@ -312,7 +312,7 @@ Incompatible changes been removed. ====================================== =========================================== - Removed property/method New method + Removed Property/Method New Method ====================================== =========================================== :meth:`~iris.unit.Unit.convertible()` :meth:`~iris.unit.Unit.is_convertible()` :attr:`~iris.unit.Unit.dimensionless` :meth:`~iris.unit.Unit.is_dimensionless()` @@ -335,7 +335,7 @@ Incompatible changes removed. =============================================================== ======================================================= - Removed function New function + Removed Function New Function =============================================================== ======================================================= :func:`~iris.coord_categorisation.add_custom_season` :func:`~iris.coord_categorisation.add_season` :func:`~iris.coord_categorisation.add_custom_season_number` :func:`~iris.coord_categorisation.add_season_number` diff --git a/docs/iris/src/whatsnew/1.7.rst b/docs/iris/src/whatsnew/1.7.rst index f6e818fedf..44ebe9ec60 100644 --- a/docs/iris/src/whatsnew/1.7.rst +++ b/docs/iris/src/whatsnew/1.7.rst @@ -1,5 +1,5 @@ v1.7 (04 Jul 2014) -******************** +****************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -196,7 +196,7 @@ Features * A speed improvement when loading PP or FF data and constraining on STASH code. -Bugs fixed +Bugs Fixed ========== * Data containing more than one reference cube for constructing hybrid height @@ -282,7 +282,7 @@ v1.7.4 (15 Apr 2015) create LambertConformal coordinate systems with Cartopy >= 0.12. -Incompatible changes +Incompatible Changes ==================== * Saving a cube with a STASH attribute to NetCDF now produces a variable diff --git a/docs/iris/src/whatsnew/1.8.rst b/docs/iris/src/whatsnew/1.8.rst index 579d4d20c5..0e327b4f5a 100644 --- a/docs/iris/src/whatsnew/1.8.rst +++ b/docs/iris/src/whatsnew/1.8.rst @@ -1,5 +1,5 @@ v1.8 (14 Apr 2015) -******************** +****************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -151,7 +151,7 @@ Features "iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid". -Bugs fixed +Bugs Fixed ========== * Fix in netCDF loader to correctly determine whether the longitude coordinate diff --git a/docs/iris/src/whatsnew/1.9.rst b/docs/iris/src/whatsnew/1.9.rst index c9d91bf33c..9829d8ff3b 100644 --- a/docs/iris/src/whatsnew/1.9.rst +++ b/docs/iris/src/whatsnew/1.9.rst @@ -1,5 +1,5 @@ v1.9 (10 Dec 2015) -******************** +****************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -93,7 +93,7 @@ Features read Fieldsfile data after the original :class:`iris.experimental.um.FieldsFileVariant` has been closed. -Bugs fixed +Bugs Fixed ========== * Fixed a bug in :meth:`iris.unit.Unit.convert` @@ -170,7 +170,7 @@ v1.9.2 (28 Jan 2016) * Fixed a bug regarding unsuccessful dot import. -Incompatible changes +Incompatible Changes ==================== * GRIB message/file reading and writing may not be available for Python 3 due diff --git a/docs/iris/src/whatsnew/2.0.rst b/docs/iris/src/whatsnew/2.0.rst index fbd012dd1f..400a395e90 100644 --- a/docs/iris/src/whatsnew/2.0.rst +++ b/docs/iris/src/whatsnew/2.0.rst @@ -60,7 +60,7 @@ Features respectively. -The :data:`iris.FUTURE` has arrived! +The :data:`iris.FUTURE` has Arrived! ------------------------------------ Throughout version 1 of Iris a set of toggles in @@ -111,7 +111,7 @@ all existing toggles in :attr:`iris.FUTURE` now default to :data:`True`. off is now deprecated. -Bugs fixed +Bugs Fixed ========== * Indexing or slicing an :class:`~iris.coords.AuxCoord` coordinate will return a coordinate with diff --git a/docs/iris/src/whatsnew/2.1.rst b/docs/iris/src/whatsnew/2.1.rst index ef03f023b2..18c562d3da 100644 --- a/docs/iris/src/whatsnew/2.1.rst +++ b/docs/iris/src/whatsnew/2.1.rst @@ -43,7 +43,7 @@ Features the ``standard_parallel`` keyword argument (:pull:`3041`). -Bugs fixed +Bugs Fixed ========== * All var names being written to NetCDF are now CF compliant. @@ -59,7 +59,7 @@ Bugs fixed ``axes`` keyword (:pull:`3010`). -Incompatible changes +Incompatible Changes ==================== * The deprecated :mod:`iris.experimental.um` was removed. @@ -94,4 +94,4 @@ Internal * Iris now requires version 2 of Matplotlib, and ``>=1.14`` of NumPy. Full requirements can be seen in the `requirements `_ - directory of the Iris' the source. \ No newline at end of file + directory of the Iris' the source. diff --git a/docs/iris/src/whatsnew/2.2.rst b/docs/iris/src/whatsnew/2.2.rst index 48280895fe..a1f48f962b 100644 --- a/docs/iris/src/whatsnew/2.2.rst +++ b/docs/iris/src/whatsnew/2.2.rst @@ -66,7 +66,7 @@ Features a NaN-tolerant array comparison. -Bugs fixed +Bugs Fixed ========== * The bug has been fixed that prevented printing time coordinates with bounds diff --git a/docs/iris/src/whatsnew/2.3.rst b/docs/iris/src/whatsnew/2.3.rst index 5997a7f4dc..2509242c05 100644 --- a/docs/iris/src/whatsnew/2.3.rst +++ b/docs/iris/src/whatsnew/2.3.rst @@ -147,7 +147,7 @@ Features `metarelate/metOcean commit 448f2ef, 2019-11-29 `_ -Bugs fixed +Bugs Fixed ========== * Cube equality of boolean data is now handled correctly. diff --git a/docs/iris/src/whatsnew/2.4.rst b/docs/iris/src/whatsnew/2.4.rst index c62e84c129..0e271389b5 100644 --- a/docs/iris/src/whatsnew/2.4.rst +++ b/docs/iris/src/whatsnew/2.4.rst @@ -47,7 +47,7 @@ Features ``STASH`` from the attributes dictionary of a :class:`~iris.cube.Cube`. -Bugs fixed +Bugs Fixed ========== * Fixed a problem which was causing file loads to fetch *all* field data diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 0a9dcd89b0..399325add5 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -1,6 +1,6 @@ .. include:: ../common_links.inc -v3.0 (02 Oct 2020) +v3.0 (25 Jan 2021) ****************** This document explains the changes made to Iris for this release @@ -113,6 +113,12 @@ This document explains the changes made to Iris for this release and preservation of common metadata and coordinates during cube math operations. Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) +* `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when + collapsing over a single dimension. + Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. + The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works + with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) + 🐛 Bugs Fixed ============= @@ -168,7 +174,11 @@ This document explains the changes made to Iris for this release Previously, the first tick label would occasionally be duplicated. This also removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) -* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) +* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check + ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) + +* `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's + coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) .. _whatsnew 3.0 changes: @@ -356,6 +366,11 @@ This document explains the changes made to Iris for this release * `@jonseddon`_ updated the CF version of the netCDF saver in the :ref:`saving_iris_cubes` section and in the equivalent function docstring. + (:pull:`3925`) + +* `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. + (:pull:`3940`) + 💼 Internal =========== @@ -427,7 +442,7 @@ This document explains the changes made to Iris for this release * `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). -* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_. (:pull:`3928`) +* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ support. (:pull:`3928`) * `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to @@ -463,6 +478,7 @@ This document explains the changes made to Iris for this release .. _@tkknight: https://github.com/tkknight .. _@lbdreyer: https://github.com/lbdreyer .. _@SimonPeatman: https://github.com/SimonPeatman +.. _@TomekTrzeciak: https://github.com/TomekTrzeciak .. _@rcomer: https://github.com/rcomer .. _@jvegasbsc: https://github.com/jvegasbsc .. _@zklaus: https://github.com/zklaus @@ -481,3 +497,6 @@ This document explains the changes made to Iris for this release .. _CF Conventions and Metadata: https://cfconventions.org/ .. _flake8: https://flake8.pycqa.org/en/stable/ .. _nox: https://nox.thea.codes/en/stable/ +.. _Title Case Capitalization: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case +.. _travis-ci: https://travis-ci.org/github/SciTools/iris +.. _stickler-ci: https://stickler-ci.com/ diff --git a/docs/iris/src/whatsnew/index.rst b/docs/iris/src/whatsnew/index.rst index 3fd5fe6070..c7fabc0479 100644 --- a/docs/iris/src/whatsnew/index.rst +++ b/docs/iris/src/whatsnew/index.rst @@ -1,6 +1,6 @@ .. _iris_whatsnew: -What's new in Iris +What's New in Iris ****************** These "What's new" pages describe the important changes between major diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index e31c7b58d7..a78d0a7682 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -106,7 +106,7 @@ def callback(cube, field, filename): # Iris revision. -__version__ = "3.1.dev0" +__version__ = "3.1.0dev0" # Restrict the names imported when using "from iris import *" __all__ = [ diff --git a/lib/iris/common/resolve.py b/lib/iris/common/resolve.py index ad37247809..e772eeefce 100644 --- a/lib/iris/common/resolve.py +++ b/lib/iris/common/resolve.py @@ -230,7 +230,7 @@ def __init__(self, lhs=None, rhs=None): """ #: The ``lhs`` operand to be resolved into the resultant :class:`~iris.cube.Cube`. - self.lhs_cube = None # set in _call__ + self.lhs_cube = None # set in __call__ #: The ``rhs`` operand to be resolved into the resultant :class:`~iris.cube.Cube`. self.rhs_cube = None # set in __call__ @@ -294,6 +294,25 @@ def __init__(self, lhs=None, rhs=None): self(lhs, rhs) def __call__(self, lhs, rhs): + """ + Resolve the ``lhs`` :class:`~iris.cube.Cube` operand and ``rhs`` + :class:`~iris.cube.Cube` operand metadata. + + Involves determining all the common coordinate metadata shared between + the operands, and the metadata that is local to each operand. Given + the common metadata, the broadcast shape of the resultant resolved + :class:`~iris.cube.Cube`, which may be auto-transposed, can be + determined. + + Args: + + * lhs: + The left-hand-side :class:`~iris.cube.Cube` operand. + + * rhs: + The right-hand-side :class:`~iris.cube.Cube` operand. + + """ from iris.cube import Cube emsg = ( @@ -338,11 +357,31 @@ def __call__(self, lhs, rhs): return self def _as_compatible_cubes(self): + """ + Determine whether the ``src`` and ``tgt`` :class:`~iris.cube.Cube` can + be transposed and/or broadcast successfully together. + + If compatible, the ``_broadcast_shape`` of the resultant resolved cube is + calculated, and the ``_src_cube_resolved`` (transposed/broadcast ``src`` + cube) and ``_tgt_cube_resolved`` (same as the ``tgt`` cube) are + calculated. + + An exception will be raised if the ``src`` and ``tgt`` cannot be + broadcast, even after a suitable transpose has been performed. + + .. note:: + + Requires that **all** ``src`` cube dimensions have been mapped + successfully to an appropriate ``tgt`` cube dimension. + + """ from iris.cube import Cube src_cube = self._src_cube tgt_cube = self._tgt_cube + assert src_cube.ndim == len(self.mapping) + # Use the mapping to calculate the new src cube shape. new_src_shape = [1] * tgt_cube.ndim for src_dim, tgt_dim in self.mapping.items(): @@ -430,6 +469,40 @@ def _aux_coverage( common_aux_metadata, common_scalar_metadata, ): + """ + Determine the dimensions covered by each of the local and common + auxiliary coordinates of the provided :class:`~iris.cube.Cube`. + + The cube dimensions not covered by any of the auxiliary coordinates is + also determined; these are known as `free` dimensions. + + The scalar coordinates local to the cube are also determined. + + Args: + + * cube: + The :class:`~iris.cube.Cube` to be analysed for coverage. + + * cube_items_aux: + The list of associated :class:`~iris.common.resolve._Item` metadata + for each auxiliary coordinate owned by the cube. + + * cube_items_scalar: + The list of associated :class:`~iris.common.resolve._Item` metadata + for each scalar coordinate owned by the cube. + + * common_aux_metadata: + The list of common auxiliary coordinate metadata shared by both + the LHS and RHS cube operands being resolved. + + * common_scalar_metadata: + The list of common scalar coordinate metadata shared by both + the LHS and RHS cube operands being resolved. + + Returns: + :class:`~iris.common.resolve._AuxCoverage` + + """ common_items_aux = [] common_items_scalar = [] local_items_aux = [] @@ -465,7 +538,33 @@ def _aux_coverage( dims_free=sorted(dims_free), ) - def _aux_mapping(self, src_coverage, tgt_coverage): + @staticmethod + def _aux_mapping(src_coverage, tgt_coverage): + """ + Establish the mapping of dimensions from the ``src`` to ``tgt`` + :class:`~iris.cube.Cube` using the auxiliary coordinate metadata + common between each of the operands. + + The ``src`` to ``tgt`` common auxiliary coordinate mapping is held by + the :attr:`~iris.common.resolve.Resolve.mapping`. + + Args: + + * src_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``src`` + :class:`~iris.cube.Cube` i.e., map from the common ``src`` + dimensions. + + * tgt_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``tgt`` + :class:`~iris.cube.Cube` i.e., map to the common ``tgt`` + dimensions. + + Returns: + Dictionary of ``src`` to ``tgt`` dimension mapping. + + """ + mapping = {} for tgt_item in tgt_coverage.common_items_aux: # Search for a src aux metadata match. tgt_metadata = tgt_item.metadata @@ -484,7 +583,7 @@ def _aux_mapping(self, src_coverage, tgt_coverage): tgt_dims = tgt_item.dims if len(src_dims) == len(tgt_dims): for src_dim, tgt_dim in zip(src_dims, tgt_dims): - self.mapping[src_dim] = tgt_dim + mapping[src_dim] = tgt_dim logger.debug(f"{src_dim}->{tgt_dim}") else: # This situation can only occur due to a systemic internal @@ -504,9 +603,26 @@ def _aux_mapping(self, src_coverage, tgt_coverage): tgt_item.dims, ) ) + return mapping @staticmethod def _categorise_items(cube): + """ + Inspect the provided :class:`~iris.cube.Cube` and group its + coordinates and associated metadata into dimension, auxiliary and + scalar categories. + + Args: + + * cube: + The :class:`~iris.cube.Cube` that will have its coordinates and + metadata grouped into their associated dimension, auxiliary and + scalar categories. + + Returns: + :class:`~iris.common.resolve._CategoryItems` + + """ category = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) # Categorise the dim coordinates of the cube. @@ -530,15 +646,40 @@ def _categorise_items(cube): return category @staticmethod - def _create_prepared_item(coord, dims, src=None, tgt=None): - if src is not None and tgt is not None: - combined = src.combine(tgt) + def _create_prepared_item( + coord, dims, src_metadata=None, tgt_metadata=None + ): + """ + Convenience method that creates a :class:`~iris.common.resolve._PreparedItem` + containing the data and metadata required to construct and attach a coordinate + to the resultant resolved cube. + + Args: + + * coord: + The coordinate with the ``points`` and ``bounds`` to be extracted. + + * dims: + The dimensions that the ``coord`` spans on the resulting resolved :class:`~iris.cube.Cube`. + + * src_metadata: + The coordinate metadata from the ``src`` :class:`~iris.cube.Cube`. + + * tgt_metadata: + The coordinate metadata from the ``tgt`` :class:`~iris.cube.Cube`. + + Returns: + The :class:`~iris.common.resolve._PreparedItem`. + + """ + if src_metadata is not None and tgt_metadata is not None: + combined = src_metadata.combine(tgt_metadata) else: - combined = src or tgt + combined = src_metadata or tgt_metadata if not isinstance(dims, Iterable): dims = (dims,) prepared_metadata = _PreparedMetadata( - combined=combined, src=src, tgt=tgt + combined=combined, src=src_metadata, tgt=tgt_metadata ) bounds = coord.bounds result = _PreparedItem( @@ -573,6 +714,30 @@ def _show(items, heading): @staticmethod def _dim_coverage(cube, cube_items_dim, common_dim_metadata): + """ + Determine the dimensions covered by each of the local and common + dimension coordinates of the provided :class:`~iris.cube.Cube`. + + The cube dimensions not covered by any of the dimension coordinates is + also determined; these are known as `free` dimensions. + + Args: + + * cube: + The :class:`~iris.cube.Cube` to be analysed for coverage. + + * cube_items_dim: + The list of associated :class:`~iris.common.resolve._Item` metadata + for each dimension coordinate owned by the cube. + + * common_dim_metadata: + The list of common dimension coordinate metadata shared by both + the LHS and RHS cube operands being resolved. + + Returns: + :class:`~iris.common.resolve._DimCoverage` + + """ ndim = cube.ndim metadata = [None] * ndim coords = [None] * ndim @@ -599,13 +764,39 @@ def _dim_coverage(cube, cube_items_dim, common_dim_metadata): dims_free=sorted(dims_free), ) - def _dim_mapping(self, src_coverage, tgt_coverage): + @staticmethod + def _dim_mapping(src_coverage, tgt_coverage): + """ + Establish the mapping of dimensions from the ``src`` to ``tgt`` + :class:`~iris.cube.Cube` using the dimension coordinate metadata + common between each of the operands. + + The ``src`` to ``tgt`` common dimension coordinate mapping is held by + the :attr:`~iris.common.resolve.Resolve.mapping`. + + Args: + + * src_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``src`` + :class:`~iris.cube.Cube` i.e., map from the common ``src`` + dimensions. + + * tgt_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``tgt`` + :class:`~iris.cube.Cube` i.e., map to the common ``tgt`` + dimensions. + + Returns: + Dictionary of ``src`` to ``tgt`` dimension mapping. + + """ + mapping = {} for tgt_dim in tgt_coverage.dims_common: # Search for a src dim metadata match. tgt_metadata = tgt_coverage.metadata[tgt_dim] try: src_dim = src_coverage.metadata.index(tgt_metadata) - self.mapping[src_dim] = tgt_dim + mapping[src_dim] = tgt_dim logger.debug(f"{src_dim}->{tgt_dim}") except ValueError: # This exception can only occur due to a systemic internal @@ -621,9 +812,10 @@ def _dim_mapping(self, src_coverage, tgt_coverage): src_coverage.cube.name(), tgt_coverage.cube.name(), tgt_metadata, - tuple([tgt_dim]), + (tgt_dim,), ) ) + return mapping def _free_mapping( self, @@ -632,6 +824,57 @@ def _free_mapping( src_aux_coverage, tgt_aux_coverage, ): + """ + Attempt to update the :attr:`~iris.common.resolve.Resolve.mapping` with + ``src`` to ``tgt`` :class:`~iris.cube.Cube` mappings from unmapped ``src`` + dimensions that are free from coordinate metadata coverage to ``tgt`` + dimensions that have local metadata coverage (i.e., is not common between + the ``src`` and ``tgt``) or dimensions that are free from coordinate + metadata coverage. + + If the ``src`` :class:`~iris.cube.Cube` does not have any free dimensions, + the attempt to map unmapped ``tgt`` dimensions that have local metadata + coverage to ``src`` dimensions that are free from coordinate metadata + coverage. + + An exception will be raised if there are any ``src`` :class:`~iris.cube.Cube` + dimensions not mapped to an associated ``tgt`` dimension. + + Args: + + * src_dim_coverage: + The :class:`~iris.common.resolve.._DimCoverage` of the ``src`` + :class:`~iris.cube.Cube`. + + * tgt_dim_coverage: + The :class:`~iris.common.resolve.._DimCoverage` of the ``tgt`` + :class:`~iris.cube.Cube`. + + * src_aux_coverage: + The :class:`~iris.common.resolve._AuxCoverage` of the ``src`` + :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:`~iris.common.resolve._AuxCoverage` of the ``tgt`` + :class:`~iris.cube.Cube`. + + .. note:: + + All unmapped dimensions with an extend >1 are mapped before those + with an extent of 1, as such dimensions cannot be broadcast. It + is important to map specific non-broadcastable dimensions before + generic broadcastable dimensions otherwise we are open to failing to + map all the src dimensions as a generic src broadcast dimension has + been mapped to the only tgt dimension that a specific non-broadcastable + dimension can be mapped to. + + .. note:: + + A local dimension cannot be mapped to another local dimension, + by definition, otherwise this dimension would be classed as a + common dimension. + + """ src_cube = src_dim_coverage.cube tgt_cube = tgt_dim_coverage.cube src_ndim = src_cube.ndim @@ -663,11 +906,16 @@ def _free_mapping( tgt_shape = tgt_cube.shape src_max, tgt_max = max(src_shape), max(tgt_shape) - def assign_mapping(extent, unmapped_local_items, free_items=None): + def _assign_mapping(extent, unmapped_local_items, free_items=None): result = None if free_items is None: free_items = [] if extent == 1: + # Map to the first available unmapped local dimension or + # the first available free dimension. + # Dimension shape doesn't matter here as the extent is 1, + # therefore broadcasting will take care of any discrepency + # between src and tgt dimension extent. if unmapped_local_items: result, _ = unmapped_local_items.pop(0) elif free_items: @@ -680,10 +928,10 @@ def _filter(items): ) def _pop(item, items): - result, _ = item + dim, _ = item index = items.index(item) items.pop(index) - return result + return dim items = _filter(unmapped_local_items) if items: @@ -700,11 +948,12 @@ def _pop(item, items): (dim, tgt_shape[dim]) for dim in tgt_unmapped_local ] tgt_free_items = [(dim, tgt_shape[dim]) for dim in tgt_free] + # Sort by decreasing src dimension extent and increasing src dimension + # as we want broadcast src dimensions to be mapped last. + src_key_func = lambda dim: (src_max - src_shape[dim], dim) - for src_dim in sorted( - src_free, key=lambda dim: (src_max - src_shape[dim], dim) - ): - tgt_dim = assign_mapping( + for src_dim in sorted(src_free, key=src_key_func): + tgt_dim = _assign_mapping( src_shape[src_dim], tgt_unmapped_local_items, tgt_free_items, @@ -725,11 +974,12 @@ def _pop(item, items): src_unmapped_local_items = [ (dim, src_shape[dim]) for dim in src_unmapped_local ] + # Sort by decreasing tgt dimension extent and increasing tgt dimension + # as we want broadcast tgt dimensions to be mapped last. + tgt_key_func = lambda dim: (tgt_max - tgt_shape[dim], dim) - for tgt_dim in sorted( - tgt_free, key=lambda dim: (tgt_max - tgt_shape[dim], dim) - ): - src_dim = assign_mapping( + for tgt_dim in sorted(tgt_free, key=tgt_key_func): + src_dim = _assign_mapping( tgt_shape[tgt_dim], src_unmapped_local_items ) if src_dim is not None: @@ -758,6 +1008,17 @@ def _pop(item, items): logger.debug(f"mapping free dimensions gives, mapping={self.mapping}") def _metadata_coverage(self): + """ + Using the pre-categorised metadata of the cubes, determine the dimensions + covered by their associated dimension and auxiliary coordinates, and which + dimensions are free of metadata coverage. + + This coverage analysis clarifies how the dimensions covered by common + metadata are related, thus establishing a dimensional mapping between + the cubes. It also identifies the dimensions covered by metadata that + is local to each cube, and indeed which dimensions are free of metadata. + + """ # Determine the common dim coordinate metadata coverage. common_dim_metadata = [ item.metadata for item in self.category_common.items_dim @@ -798,6 +1059,37 @@ def _metadata_coverage(self): ) def _metadata_mapping(self): + """ + Ensure that each ``src`` :class:`~iris.cube.Cube` dimension is mapped to an associated + ``tgt`` :class:`~iris.cube.Cube` dimension using the common dim and aux coordinate metadata. + + If the common metadata does not result in a full mapping of ``src`` to ``tgt`` dimensions + then free dimensions are analysed to determine whether the mapping can be completed. + + Once the ``src`` has been mapped to the ``tgt``, the cubes are checked to ensure that they + will successfully broadcast, and the ``src`` :class:`~iris.cube.Cube` is transposed appropriately, + if necessary. + + The :attr:`~iris.common.resolve.Resolve._broadcast_shape` is set, along with the + :attr:`~iris.common.resolve.Resolve._src_cube_resolved` and :attr:`~iris.common.resolve.Resolve._tgt_cube_resolved`, + which are the broadcast/transposed ``src`` and ``tgt``. + + .. note:: + + An exception will be raised if a ``src`` dimension cannot be mapped to a ``tgt`` dimension. + + .. note:: + + An exception will be raised if the full mapped ``src`` :class:`~iris.cube.Cube` cannot be + broadcast or transposed with the ``tgt`` :class:`~iris.cube.Cube`. + + .. note:: + + The ``src`` and ``tgt`` may be swapped in the case where they both have equal dimensionality and + the ``tgt`` does have the same shape as the resolved broadcast shape (and the ``src`` does) or + the ``tgt`` has more free dimensions than the ``src``. + + """ # Initialise the state. self.mapping = {} @@ -819,7 +1111,9 @@ def _metadata_mapping(self): # Use the dim coordinates to fully map the # src cube dimensions to the tgt cube dimensions. - self._dim_mapping(src_dim_coverage, tgt_dim_coverage) + self.mapping.update( + self._dim_mapping(src_dim_coverage, tgt_dim_coverage) + ) logger.debug( f"mapping common dim coordinates gives, mapping={self.mapping}" ) @@ -827,7 +1121,9 @@ def _metadata_mapping(self): # If necessary, use the aux coordinates to fully map the # src cube dimensions to the tgt cube dimensions. if not self.mapped: - self._aux_mapping(src_aux_coverage, tgt_aux_coverage) + self.mapping.update( + self._aux_mapping(src_aux_coverage, tgt_aux_coverage) + ) logger.debug( f"mapping common aux coordinates, mapping={self.mapping}" ) @@ -886,6 +1182,12 @@ def _metadata_mapping(self): self._as_compatible_cubes() def _metadata_prepare(self): + """ + Populate the :attr:`~iris.common.resolve.Resolve.prepared_category` and + :attr:`~iris.common.resolve.Resolve.prepared_factories` with the necessary metadata to be constructed + and attached to the resulting resolved :class:`~iris.cube.Cube`. + + """ # Initialise the state. self.prepared_category = _CategoryItems( items_dim=[], items_aux=[], items_scalar=[] @@ -1053,6 +1355,41 @@ def _prepare_common_aux_payload( prepared_items, ignore_mismatch=None, ): + """ + Populate the ``prepared_items`` with a :class:`~iris.common.resolve._PreparedItem` containing + the necessary metadata for each auxiliary coordinate to be constructed and attached to the + resulting resolved :class:`~iris.cube.Cube`. + + .. note:: + + For mixed ``src`` and ``tgt`` coordinate types with matching metadata, an + :class:`~iris.coords.AuxCoord` will be nominated for construction. + + Args: + + * src_common_items: + The list of :attr:`~iris.common.resolve._AuxCoverage.common_items_aux` metadata + for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_common_items: + The list of :attr:`~iris.common.resolve._AuxCoverage.common_items_aux` metadata + for the ``tgt`` :class:`~iris.cube.Cube`. + + * prepared_items: + The list of :class:`~iris.common.resolve._PreparedItem` metadata that will be used + to construct the auxiliary coordinates that will be attached to the resulting + resolved :class:`~iris.cube.Cube`. + + Kwargs: + + * ignore_mismatch: + When ``False``, an exception will be raised if a difference is detected between corresponding + ``src`` and ``tgt`` coordinate ``points`` and/or ``bounds``. + When ``True``, the coverage metadata is ignored i.e., a coordinate will not be constructed and + added to the resulting resolved :class:`~iris.cube.Cube`. + Defaults to ``False``. + + """ from iris.coords import AuxCoord if ignore_mismatch is None: @@ -1115,6 +1452,30 @@ def _prepare_common_aux_payload( def _prepare_common_dim_payload( self, src_coverage, tgt_coverage, ignore_mismatch=None ): + """ + Populate the ``items_dim`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for + each :class:`~iris.coords.DimCoord` to be constructed and attached to the resulting resolved + :class:`~iris.cube.Cube`. + + Args: + + * src_coverage: + The :class:`~iris.common.resolve._DimCoverage` metadata for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_coverage: + The :class:`~iris.common.resolve._DimCoverage` metadata for the ``tgt`` :class:`~iris.cube.Cube`. + + Kwargs: + + * ignore_mismatch: + When ``False``, an exception will be raised if a difference is detected between corresponding + ``src`` and ``tgt`` :class:`~iris.coords.DimCoord` ``points`` and/or ``bounds``. + When ``True``, the coverage metadata is ignored i.e., a :class:`~iris.coords.DimCoord` will not + be constructed and added to the resulting resolved :class:`~iris.cube.Cube`. + Defaults to ``False``. + + """ from iris.coords import DimCoord if ignore_mismatch is None: @@ -1153,55 +1514,123 @@ def _prepare_common_dim_payload( ) self.prepared_category.items_dim.append(prepared_item) - def _prepare_factory_payload(self, cube, category_local, from_src=True): - def _get_prepared_item(metadata, from_src=True, from_local=False): - result = None - if from_local: - category = category_local - match = lambda item: item.metadata == metadata + def _get_prepared_item( + self, metadata, category_local, from_src=True, from_local=False + ): + """ + Find the :attr:`~iris.common.resolve._PreparedItem` from the + :attr:`~iris.common.resolve.Resolve.prepared_category` that matches the provided ``metadata``. + + Alternatively, the ``category_local`` is searched to find a :class:`~iris.common.resolve._Item` + with matching ``metadata`` from either the local ``src`` or ``tgt`` :class:`~iris.cube.Cube`. + If a match is found, then a new `~iris.common.resolve._PreparedItem` is created and added to + :attr:`~iris.common.resolve.Resolve.prepared_category` and returned. See ``from_local``. + + Args: + + * metadata: + The target metadata of the prepared (or local) item to retrieve. + + * category_local: + The :class:`~iris.common.resolve._CategoryItems` containing the + local metadata of either the ``src`` or ``tgt`` :class:`~iris.cube.Cube`. + See ``from_local``. + + Kwargs: + + * from_src: + Boolean stating whether the ``metadata`` is from the ``src`` (``True``) + or ``tgt`` :class:`~iris.cube.Cube`. + Defaults to ``True``. + + * from_local: + Boolean controlling whether the ``metadata`` is used to search the + ``category_local`` (``True``) or the :attr:`~iris.common.resolve.Resolve.prepared_category`. + Defaults to ``False``. + + Returns: + The :class:`~iris.common.resolve._PreparedItem` matching the provided ``metadata``. + + """ + result = None + + if from_local: + category = category_local + match = lambda item: item.metadata == metadata + else: + category = self.prepared_category + if from_src: + match = lambda item: item.metadata.src == metadata else: - category = self.prepared_category - if from_src: - match = lambda item: item.metadata.src == metadata + match = lambda item: item.metadata.tgt == metadata + + for member in category._fields: + category_items = getattr(category, member) + matched_items = tuple(filter(match, category_items)) + if matched_items: + if len(matched_items) > 1: + dmsg = ( + f"ignoring factory dependency {metadata}, multiple {'src' if from_src else 'tgt'} " + f"{'local' if from_local else 'prepared'} metadata matches" + ) + logger.debug(dmsg) else: - match = lambda item: item.metadata.tgt == metadata - for member in category._fields: - category_items = getattr(category, member) - matched_items = tuple(filter(match, category_items)) - if matched_items: - if len(matched_items) > 1: - dmsg = ( - f"ignoring factory dependency {metadata}, multiple {'src' if from_src else 'tgt'} " - f"{'local' if from_local else 'prepared'} metadata matches" - ) - logger.debug(dmsg) - else: - (item,) = matched_items - if from_local: - src = tgt = None - if from_src: - src = item.metadata - dims = tuple( - [self.mapping[dim] for dim in item.dims] - ) - else: - tgt = item.metadata - dims = item.dims - result = self._create_prepared_item( - item.coord, dims, src=src, tgt=tgt - ) - getattr(self.prepared_category, member).append( - result + (item,) = matched_items + if from_local: + src = tgt = None + if from_src: + src = item.metadata + dims = tuple( + [self.mapping[dim] for dim in item.dims] ) else: - result = item - break - return result + tgt = item.metadata + dims = item.dims + result = self._create_prepared_item( + item.coord, + dims, + src_metadata=src, + tgt_metadata=tgt, + ) + getattr(self.prepared_category, member).append(result) + else: + result = item + break + return result + + def _prepare_factory_payload(self, cube, category_local, from_src=True): + """ + Populate the :attr:`~iris.common.resolve.Resolve.prepared_factories` with a :class:`~iris.common.resolve._PreparedFactory` + containing the necessary metadata for each ``src`` and/or ``tgt`` auxiliary factory to be constructed and + attached to the resulting resolved :class:`~iris.cube.Cube`. + + .. note:: + + The required dependencies of an auxiliary factory may not all be available in the + :attr:`~iris.common.resolve.Resolve.prepared_category` and therefore this is a legitimate + reason to add the associated metadata of the local dependency to the ``prepared_category``. + + Args: + + * cube: + The :class:`~iris.cube.Cube` that may contain an auxiliary factory to be prepared. + + * category_local: + The :class:`~iris.common.resolve._CategoryItems` of all metadata local to the provided ``cube``. + Kwargs: + + * from_src: + Boolean stating whether the provided ``cube`` is either a ``src`` or ``tgt`` + :class:`~iris.cube.Cube` - used to retrieve the appropriate metadata from a + :class:`~iris.common.resolve._PreparedMetadata`. + + """ for factory in cube.aux_factories: container = type(factory) dependencies = {} prepared_item = None + found = True if tuple( filter( @@ -1222,18 +1651,24 @@ def _get_prepared_item(metadata, from_src=True, from_local=False): dependency_coord, ) in factory.dependencies.items(): metadata = dependency_coord.metadata - prepared_item = _get_prepared_item(metadata, from_src=from_src) + prepared_item = self._get_prepared_item( + metadata, category_local, from_src=from_src + ) if prepared_item is None: - prepared_item = _get_prepared_item( - metadata, from_src=from_src, from_local=True + prepared_item = self._get_prepared_item( + metadata, + category_local, + from_src=from_src, + from_local=True, ) if prepared_item is None: dmsg = f"cannot find matching {metadata} for {container} dependency {dependency_name}" logger.debug(dmsg) + found = False break dependencies[dependency_name] = prepared_item.metadata - if prepared_item is not None: + if found and prepared_item is not None: prepared_factory = _PreparedFactory( container=container, dependencies=dependencies ) @@ -1243,6 +1678,29 @@ def _get_prepared_item(metadata, from_src=True, from_local=False): logger.debug(dmsg) def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): + """ + Populate the ``items_aux`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for each + ``src`` or ``tgt`` local auxiliary coordinate to be constructed and attached to the resulting + resolved :class:`~iris.cube.Cube`. + + .. note:: + + In general, lenient behaviour subscribes to the philosophy that it is easier to remove + metadata than it is to find then add metadata. To those ends, lenient behaviour supports + metadata richness by adding both local ``src`` and ``tgt`` auxiliary coordinates. + Alternatively, strict behaviour will only add a ``tgt`` local auxiliary coordinate that + spans dimensions not mapped to by the ``src`` e.g., extra ``tgt`` dimensions. + + Args: + + * src_aux_coverage: + The :class:`~iris.common.resolve.Resolve._AuxCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:~iris.common.resolve.Resolve._AuxCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ # Determine whether there are tgt dimensions not mapped to by an # associated src dimension, and thus may be covered by any local # tgt aux coordinates. @@ -1259,7 +1717,7 @@ def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): if all([dim in mapped_src_dims for dim in item.dims]): tgt_dims = tuple([self.mapping[dim] for dim in item.dims]) prepared_item = self._create_prepared_item( - item.coord, tgt_dims, src=item.metadata + item.coord, tgt_dims, src_metadata=item.metadata ) self.prepared_category.items_aux.append(prepared_item) else: @@ -1281,7 +1739,7 @@ def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): [dim in extra_tgt_dims for dim in tgt_dims] ): prepared_item = self._create_prepared_item( - item.coord, tgt_dims, tgt=item.metadata + item.coord, tgt_dims, tgt_metadata=item.metadata ) self.prepared_category.items_aux.append(prepared_item) else: @@ -1293,6 +1751,28 @@ def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): logger.debug(dmsg) def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): + """ + Populate the ``items_dim`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for each + ``src`` or ``tgt`` local :class:`~iris.coords.DimCoord` to be constructed and attached to the + resulting resolved :class:`~iris.cube.Cube`. + + .. note:: + + In general, a local coordinate will only be added if there is no other metadata competing + to describe the same dimension/s on the ``tgt`` :class:`~iris.cube.Cube`. Lenient behaviour + is more liberal, whereas strict behaviour will only add a local ``tgt`` coordinate covering + an unmapped "extra" ``tgt`` dimension/s. + + Args: + + * src_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ mapped_tgt_dims = self.mapping.values() # Determine whether there are tgt dimensions not mapped to by an @@ -1314,7 +1794,7 @@ def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): metadata = src_dim_coverage.metadata[src_dim] coord = src_dim_coverage.coords[src_dim] prepared_item = self._create_prepared_item( - coord, tgt_dim, src=metadata + coord, tgt_dim, src_metadata=metadata ) self.prepared_category.items_dim.append(prepared_item) else: @@ -1347,13 +1827,36 @@ def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): if metadata is not None: coord = tgt_dim_coverage.coords[tgt_dim] prepared_item = self._create_prepared_item( - coord, tgt_dim, tgt=metadata + coord, tgt_dim, tgt_metadata=metadata ) self.prepared_category.items_dim.append(prepared_item) def _prepare_local_payload_scalar( self, src_aux_coverage, tgt_aux_coverage ): + """ + Populate the ``items_scalar`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for each + ``src`` or ``tgt`` local scalar coordinate to be constructed and attached to the resulting + resolved :class:`~iris.cube.Cube`. + + .. note:: + + In general, lenient behaviour subscribes to the philosophy that it is easier to remove + metadata than it is to find then add metadata. To those ends, lenient behaviour supports + metadata richness by adding both local ``src`` and ``tgt`` scalar coordinates. + Alternatively, strict behaviour will only add a ``tgt`` local scalar coordinate when the + ``src`` is a scalar :class:`~iris.cube.Cube` with no local scalar coordinates. + + Args: + + * src_aux_coverage: + The :class:`~iris.common.resolve.Resolve._AuxCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:~iris.common.resolve.Resolve._AuxCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ # Add all local tgt scalar coordinates iff the src cube is a # scalar cube with no local src scalar coordinates. # Only for strict maths. @@ -1367,14 +1870,14 @@ def _prepare_local_payload_scalar( # Add any local src scalar coordinates, if available. for item in src_aux_coverage.local_items_scalar: prepared_item = self._create_prepared_item( - item.coord, item.dims, src=item.metadata + item.coord, item.dims, src_metadata=item.metadata ) self.prepared_category.items_scalar.append(prepared_item) # Add any local tgt scalar coordinates, if available. for item in tgt_aux_coverage.local_items_scalar: prepared_item = self._create_prepared_item( - item.coord, item.dims, tgt=item.metadata + item.coord, item.dims, tgt_metadata=item.metadata ) self.prepared_category.items_scalar.append(prepared_item) @@ -1385,6 +1888,27 @@ def _prepare_local_payload( tgt_dim_coverage, tgt_aux_coverage, ): + """ + Populate the :attr:`~iris.common.resolve.Resolve.prepared_category_items` with a + :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata from the ``src`` + and/or ``tgt`` :class:`~iris.cube.Cube` for each coordinate to be constructed and attached + to the resulting resolved :class:`~iris.cube.Cube`. + + Args: + + * src_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * src_aux_coverage: + The :class:`~iris.common.resolve.Resolve._AuxCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:~iris.common.resolve.Resolve._AuxCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ # Add local src/tgt dim coordinates. self._prepare_local_payload_dim(src_dim_coverage, tgt_dim_coverage) @@ -1397,6 +1921,47 @@ def _prepare_local_payload( def _prepare_points_and_bounds( self, src_coord, tgt_coord, src_dims, tgt_dims, ignore_mismatch=None ): + """ + Compare the points and bounds of the ``src`` and ``tgt`` coordinates to ensure + that they are equivalent, taking into account broadcasting when appropriate. + + .. note:: + + An exception will be raised if the ``src`` and ``tgt`` coordinates cannot + be broadcast. + + .. note:: + + An exception will be raised if either the points or bounds are different, + however appropriate lenient behaviour concessions are applied. + + Args: + + * src_coord: + The ``src`` :class:`~iris.cube.Cube` coordinate with metadata matching + the ``tgt_coord``. + + * tgt_coord: + The ``tgt`` :class`~iris.cube.Cube` coordinate with metadata matching + the ``src_coord``. + + * src_dims: + The dimension/s of the ``src_coord`` attached to the ``src`` :class:`~iris.cube.Cube`. + + * tgt_dims: + The dimension/s of the ``tgt_coord`` attached to the ``tgt`` :class:`~iris.cube.Cube`. + + Kwargs: + + * ignore_mismatch: + For lenient behaviour only, don't raise an exception if there is a difference between + the ``src`` and ``tgt`` coordinate points or bounds. + Defaults to ``False``. + + Returns: + Tuple of equivalent ``points`` and ``bounds``, otherwise ``None``. + + """ from iris.util import array_equal if ignore_mismatch is None: @@ -1443,6 +2008,7 @@ def _prepare_points_and_bounds( tgt_broadcasting = tgt_shape != tgt_shape_broadcast if src_broadcasting and tgt_broadcasting: + # TBD: Extend capability to support attempting to broadcast two-way multi-dimensional coordinates. emsg = ( f"Cannot broadcast the coordinate {src_coord.name()!r} on " f"{self._src_cube_position} cube {self._src_cube.name()!r} and " diff --git a/lib/iris/cube.py b/lib/iris/cube.py index bb631cae73..7c7d6c58e9 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -981,9 +981,7 @@ def convert_units(self, unit): celsius and subtract 273.15 from each value in :attr:`~iris.cube.Cube.data`. - .. warning:: - Calling this method will trigger any deferred loading, causing - the cube's data array to be loaded into memory. + This operation preserves lazy data. """ # If the cube has units convert the data. @@ -2186,23 +2184,20 @@ def _summary_coord_extra(self, coord, indent): extra = "" similar_coords = self.coords(coord.name()) if len(similar_coords) > 1: - # Find all the attribute keys - keys = set() - for similar_coord in similar_coords: - keys.update(similar_coord.attributes.keys()) - # Look for any attributes that vary + similar_coords.remove(coord) + # Look for any attributes that vary. vary = set() - attributes = {} - for key in keys: + for key, value in coord.attributes.items(): for similar_coord in similar_coords: if key not in similar_coord.attributes: vary.add(key) break - value = similar_coord.attributes[key] - if attributes.setdefault(key, value) != value: + if not np.array_equal( + similar_coord.attributes[key], value + ): vary.add(key) break - keys = sorted(vary & set(coord.attributes.keys())) + keys = sorted(vary) bits = [ "{}={!r}".format(key, coord.attributes[key]) for key in keys ] @@ -3923,10 +3918,15 @@ def collapsed(self, coords, aggregator, **kwargs): # on the cube lazy array. # NOTE: do not reform the data in this case, as 'lazy_aggregate' # accepts multiple axes (unlike 'aggregate'). - collapse_axis = list(dims_to_collapse) + collapse_axes = list(dims_to_collapse) + if len(collapse_axes) == 1: + # Replace a "list of 1 axes" with just a number : This single-axis form is *required* by functions + # like da.average (and np.average), if a 1d weights array is specified. + collapse_axes = collapse_axes[0] + try: data_result = aggregator.lazy_aggregate( - self.lazy_data(), axis=collapse_axis, **kwargs + self.lazy_data(), axis=collapse_axes, **kwargs ) except TypeError: # TypeError - when unexpected keywords passed through (such as @@ -3950,8 +3950,10 @@ def collapsed(self, coords, aggregator, **kwargs): unrolled_data = np.transpose(self.data, dims).reshape(new_shape) # Perform the same operation on the weights if applicable - if kwargs.get("weights") is not None: - weights = kwargs["weights"].view() + weights = kwargs.get("weights") + if weights is not None and weights.ndim > 1: + # Note: *don't* adjust 1d weights arrays, these have a special meaning for statistics functions. + weights = weights.view() kwargs["weights"] = np.transpose(weights, dims).reshape( new_shape ) diff --git a/lib/iris/tests/unit/common/resolve/__init__.py b/lib/iris/tests/unit/common/resolve/__init__.py new file mode 100644 index 0000000000..d0b189e59d --- /dev/null +++ b/lib/iris/tests/unit/common/resolve/__init__.py @@ -0,0 +1,6 @@ +# 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 :mod:`iris.common.resolve` package.""" diff --git a/lib/iris/tests/unit/common/resolve/test_Resolve.py b/lib/iris/tests/unit/common/resolve/test_Resolve.py new file mode 100644 index 0000000000..94ec48de88 --- /dev/null +++ b/lib/iris/tests/unit/common/resolve/test_Resolve.py @@ -0,0 +1,4795 @@ +# 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 :class:`iris.common.resolve.Resolve`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from collections import namedtuple +from copy import deepcopy + +from cf_units import Unit +import numpy as np +import unittest.mock as mock +from unittest.mock import sentinel + +from iris.common.lenient import LENIENT +from iris.common.metadata import CubeMetadata +from iris.common.resolve import ( + Resolve, + _AuxCoverage, + _CategoryItems, + _DimCoverage, + _Item, + _PreparedItem, + _PreparedFactory, + _PreparedMetadata, +) +from iris.coords import DimCoord +from iris.cube import Cube + + +class Test___init__(tests.IrisTest): + def setUp(self): + target = "iris.common.resolve.Resolve.__call__" + self.m_call = mock.MagicMock(return_value=sentinel.return_value) + _ = self.patch(target, new=self.m_call) + + def _assert_members_none(self, resolve): + self.assertIsNone(resolve.lhs_cube_resolved) + self.assertIsNone(resolve.rhs_cube_resolved) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + self.assertIsNone(resolve.lhs_cube_dim_coverage) + self.assertIsNone(resolve.lhs_cube_aux_coverage) + self.assertIsNone(resolve.rhs_cube_dim_coverage) + self.assertIsNone(resolve.rhs_cube_aux_coverage) + self.assertIsNone(resolve.map_rhs_to_lhs) + self.assertIsNone(resolve.mapping) + self.assertIsNone(resolve.prepared_category) + self.assertIsNone(resolve.prepared_factories) + self.assertIsNone(resolve._broadcast_shape) + + def test_lhs_rhs_default(self): + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self._assert_members_none(resolve) + self.assertEqual(0, self.m_call.call_count) + + def test_lhs_rhs_provided(self): + m_lhs = sentinel.lhs + m_rhs = sentinel.rhs + resolve = Resolve(lhs=m_lhs, rhs=m_rhs) + # The lhs_cube and rhs_cube are only None due + # to __call__ being mocked. See Test___call__ + # for appropriate test coverage. + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self._assert_members_none(resolve) + self.assertEqual(1, self.m_call.call_count) + call_args = mock.call(m_lhs, m_rhs) + self.assertEqual(call_args, self.m_call.call_args) + + +class Test___call__(tests.IrisTest): + def setUp(self): + self.m_lhs = mock.MagicMock(spec=Cube) + self.m_rhs = mock.MagicMock(spec=Cube) + target = "iris.common.resolve.Resolve.{method}" + method = target.format(method="_metadata_resolve") + self.m_metadata_resolve = self.patch(method) + method = target.format(method="_metadata_coverage") + self.m_metadata_coverage = self.patch(method) + method = target.format(method="_metadata_mapping") + self.m_metadata_mapping = self.patch(method) + method = target.format(method="_metadata_prepare") + self.m_metadata_prepare = self.patch(method) + + def test_lhs_not_cube(self): + emsg = "'LHS' argument to be a 'Cube'" + with self.assertRaisesRegex(TypeError, emsg): + _ = Resolve(rhs=self.m_rhs) + + def test_rhs_not_cube(self): + emsg = "'RHS' argument to be a 'Cube'" + with self.assertRaisesRegex(TypeError, emsg): + _ = Resolve(lhs=self.m_lhs) + + def _assert_called_metadata_methods(self): + call_args = mock.call() + self.assertEqual(1, self.m_metadata_resolve.call_count) + self.assertEqual(call_args, self.m_metadata_resolve.call_args) + self.assertEqual(1, self.m_metadata_coverage.call_count) + self.assertEqual(call_args, self.m_metadata_coverage.call_args) + self.assertEqual(1, self.m_metadata_mapping.call_count) + self.assertEqual(call_args, self.m_metadata_mapping.call_args) + self.assertEqual(1, self.m_metadata_prepare.call_count) + self.assertEqual(call_args, self.m_metadata_prepare.call_args) + + def test_map_rhs_to_lhs__less_than(self): + self.m_lhs.ndim = 2 + self.m_rhs.ndim = 1 + resolve = Resolve(lhs=self.m_lhs, rhs=self.m_rhs) + self.assertEqual(self.m_lhs, resolve.lhs_cube) + self.assertEqual(self.m_rhs, resolve.rhs_cube) + self.assertTrue(resolve.map_rhs_to_lhs) + self._assert_called_metadata_methods() + + def test_map_rhs_to_lhs__equal(self): + self.m_lhs.ndim = 2 + self.m_rhs.ndim = 2 + resolve = Resolve(lhs=self.m_lhs, rhs=self.m_rhs) + self.assertEqual(self.m_lhs, resolve.lhs_cube) + self.assertEqual(self.m_rhs, resolve.rhs_cube) + self.assertTrue(resolve.map_rhs_to_lhs) + self._assert_called_metadata_methods() + + def test_map_lhs_to_rhs(self): + self.m_lhs.ndim = 2 + self.m_rhs.ndim = 3 + resolve = Resolve(lhs=self.m_lhs, rhs=self.m_rhs) + self.assertEqual(self.m_lhs, resolve.lhs_cube) + self.assertEqual(self.m_rhs, resolve.rhs_cube) + self.assertFalse(resolve.map_rhs_to_lhs) + self._assert_called_metadata_methods() + + +class Test__categorise_items(tests.IrisTest): + def setUp(self): + self.coord_dims = {} + # configure dim coords + coord = mock.Mock(metadata=sentinel.dim_metadata1) + self.dim_coords = [coord] + self.coord_dims[coord] = sentinel.dims1 + # configure aux and scalar coords + self.aux_coords = [] + pairs = [ + (sentinel.aux_metadata2, sentinel.dims2), + (sentinel.aux_metadata3, sentinel.dims3), + (sentinel.scalar_metadata4, None), + (sentinel.scalar_metadata5, None), + (sentinel.scalar_metadata6, None), + ] + for metadata, dims in pairs: + coord = mock.Mock(metadata=metadata) + self.aux_coords.append(coord) + self.coord_dims[coord] = dims + func = lambda coord: self.coord_dims[coord] + self.cube = mock.Mock( + aux_coords=self.aux_coords, + dim_coords=self.dim_coords, + coord_dims=func, + ) + + def test(self): + result = Resolve._categorise_items(self.cube) + self.assertIsInstance(result, _CategoryItems) + self.assertEqual(1, len(result.items_dim)) + # check dim coords + for item in result.items_dim: + self.assertIsInstance(item, _Item) + (coord,) = self.dim_coords + dims = self.coord_dims[coord] + expected = [_Item(metadata=coord.metadata, coord=coord, dims=dims)] + self.assertEqual(expected, result.items_dim) + # check aux coords + self.assertEqual(2, len(result.items_aux)) + for item in result.items_aux: + self.assertIsInstance(item, _Item) + expected_aux, expected_scalar = [], [] + for coord in self.aux_coords: + dims = self.coord_dims[coord] + item = _Item(metadata=coord.metadata, coord=coord, dims=dims) + if dims: + expected_aux.append(item) + else: + expected_scalar.append(item) + self.assertEqual(expected_aux, result.items_aux) + # check scalar coords + self.assertEqual(3, len(result.items_scalar)) + for item in result.items_scalar: + self.assertIsInstance(item, _Item) + self.assertEqual(expected_scalar, result.items_scalar) + + +class Test__metadata_resolve(tests.IrisTest): + def setUp(self): + self.target = "iris.common.resolve.Resolve._categorise_items" + self.m_lhs_cube = sentinel.lhs_cube + self.m_rhs_cube = sentinel.rhs_cube + + @staticmethod + def _create_items(pairs): + # this wrapper (hack) is necessary in order to support mocking + # the "name" method (callable) of the metadata, as "name" is already + # part of the mock API - this is always troublesome in mock-world. + Wrapper = namedtuple("Wrapper", ("name", "value")) + result = [] + for name, dims in pairs: + metadata = Wrapper(name=lambda: str(name), value=name) + coord = mock.Mock(metadata=metadata) + item = _Item(metadata=metadata, coord=coord, dims=dims) + result.append(item) + return result + + def test_metadata_same(self): + category = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + # configure dim coords + pairs = [(sentinel.dim_metadata1, sentinel.dims1)] + category.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata1, sentinel.dims2), + (sentinel.aux_metadata2, sentinel.dims3), + ] + category.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata1, None), + (sentinel.scalar_metadata2, None), + (sentinel.scalar_metadata3, None), + ] + category.items_scalar.extend(self._create_items(pairs)) + + side_effect = (category, category) + mocker = self.patch(self.target, side_effect=side_effect) + + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + + # require to explicitly configure cubes + resolve.lhs_cube = self.m_lhs_cube + resolve.rhs_cube = self.m_rhs_cube + resolve._metadata_resolve() + + self.assertEqual(mocker.call_count, 2) + calls = [mock.call(self.m_lhs_cube), mock.call(self.m_rhs_cube)] + self.assertEqual(calls, mocker.call_args_list) + + self.assertEqual(category, resolve.lhs_cube_category) + self.assertEqual(category, resolve.rhs_cube_category) + expected = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + self.assertEqual(expected, resolve.lhs_cube_category_local) + self.assertEqual(expected, resolve.rhs_cube_category_local) + self.assertEqual(category, resolve.category_common) + + def test_metadata_overlap(self): + # configure the lhs cube category + category_lhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + pairs = [ + (sentinel.dim_metadata1, sentinel.dims1), + (sentinel.dim_metadata2, sentinel.dims2), + ] + category_lhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata1, sentinel.dims3), + (sentinel.aux_metadata2, sentinel.dims4), + ] + category_lhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata1, None), + (sentinel.scalar_metadata2, None), + ] + category_lhs.items_scalar.extend(self._create_items(pairs)) + + # configure the rhs cube category + category_rhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + category_rhs.items_dim.append(category_lhs.items_dim[0]) + pairs = [(sentinel.dim_metadata200, sentinel.dims2)] + category_rhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + category_rhs.items_aux.append(category_lhs.items_aux[0]) + pairs = [(sentinel.aux_metadata200, sentinel.dims4)] + category_rhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + category_rhs.items_scalar.append(category_lhs.items_scalar[0]) + pairs = [(sentinel.scalar_metadata200, None)] + category_rhs.items_scalar.extend(self._create_items(pairs)) + + side_effect = (category_lhs, category_rhs) + mocker = self.patch(self.target, side_effect=side_effect) + + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + + # require to explicitly configure cubes + resolve.lhs_cube = self.m_lhs_cube + resolve.rhs_cube = self.m_rhs_cube + resolve._metadata_resolve() + + self.assertEqual(2, mocker.call_count) + calls = [mock.call(self.m_lhs_cube), mock.call(self.m_rhs_cube)] + self.assertEqual(calls, mocker.call_args_list) + + self.assertEqual(category_lhs, resolve.lhs_cube_category) + self.assertEqual(category_rhs, resolve.rhs_cube_category) + + items_dim = [category_lhs.items_dim[1]] + items_aux = [category_lhs.items_aux[1]] + items_scalar = [category_lhs.items_scalar[1]] + expected = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.assertEqual(expected, resolve.lhs_cube_category_local) + + items_dim = [category_rhs.items_dim[1]] + items_aux = [category_rhs.items_aux[1]] + items_scalar = [category_rhs.items_scalar[1]] + expected = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.assertEqual(expected, resolve.rhs_cube_category_local) + + items_dim = [category_lhs.items_dim[0]] + items_aux = [category_lhs.items_aux[0]] + items_scalar = [category_lhs.items_scalar[0]] + expected = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.assertEqual(expected, resolve.category_common) + + def test_metadata_different(self): + # configure the lhs cube category + category_lhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + pairs = [ + (sentinel.dim_metadata1, sentinel.dims1), + (sentinel.dim_metadata2, sentinel.dims2), + ] + category_lhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata1, sentinel.dims3), + (sentinel.aux_metadata2, sentinel.dims4), + ] + category_lhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata1, None), + (sentinel.scalar_metadata2, None), + ] + category_lhs.items_scalar.extend(self._create_items(pairs)) + + # configure the rhs cube category + category_rhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + pairs = [ + (sentinel.dim_metadata100, sentinel.dims1), + (sentinel.dim_metadata200, sentinel.dims2), + ] + category_rhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata100, sentinel.dims3), + (sentinel.aux_metadata200, sentinel.dims4), + ] + category_rhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata100, None), + (sentinel.scalar_metadata200, None), + ] + category_rhs.items_scalar.extend(self._create_items(pairs)) + + side_effect = (category_lhs, category_rhs) + mocker = self.patch(self.target, side_effect=side_effect) + + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + + # first require to explicitly lhs/rhs configure cubes + resolve.lhs_cube = self.m_lhs_cube + resolve.rhs_cube = self.m_rhs_cube + resolve._metadata_resolve() + + self.assertEqual(2, mocker.call_count) + calls = [mock.call(self.m_lhs_cube), mock.call(self.m_rhs_cube)] + self.assertEqual(calls, mocker.call_args_list) + + self.assertEqual(category_lhs, resolve.lhs_cube_category) + self.assertEqual(category_rhs, resolve.rhs_cube_category) + self.assertEqual(category_lhs, resolve.lhs_cube_category_local) + self.assertEqual(category_rhs, resolve.rhs_cube_category_local) + expected = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + self.assertEqual(expected, resolve.category_common) + + +class Test__dim_coverage(tests.IrisTest): + def setUp(self): + self.ndim = 4 + self.cube = mock.Mock(ndim=self.ndim) + self.items = [] + parts = [ + (sentinel.metadata0, sentinel.coord0, (0,)), + (sentinel.metadata1, sentinel.coord1, (1,)), + (sentinel.metadata2, sentinel.coord2, (2,)), + (sentinel.metadata3, sentinel.coord3, (3,)), + ] + column_parts = [x for x in zip(*parts)] + self.metadata, self.coords, self.dims = [list(x) for x in column_parts] + self.dims = [dim for dim, in self.dims] + for metadata, coord, dims in parts: + item = _Item(metadata=metadata, coord=coord, dims=dims) + self.items.append(item) + + def test_coverage_no_local_no_common_all_free(self): + items = [] + common = [] + result = Resolve._dim_coverage(self.cube, items, common) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + expected = [None] * self.ndim + self.assertEqual(expected, result.metadata) + self.assertEqual(expected, result.coords) + self.assertEqual([], result.dims_common) + self.assertEqual([], result.dims_local) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_free) + + def test_coverage_all_local_no_common_no_free(self): + common = [] + result = Resolve._dim_coverage(self.cube, self.items, common) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual(self.metadata, result.metadata) + self.assertEqual(self.coords, result.coords) + self.assertEqual([], result.dims_common) + self.assertEqual(self.dims, result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_no_local_all_common_no_free(self): + result = Resolve._dim_coverage(self.cube, self.items, self.metadata) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual(self.metadata, result.metadata) + self.assertEqual(self.coords, result.coords) + self.assertEqual(self.dims, result.dims_common) + self.assertEqual([], result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_mixed(self): + common = [self.items[1].metadata, self.items[2].metadata] + self.items.pop(0) + self.items.pop(-1) + metadata, coord, dims = sentinel.metadata100, sentinel.coord100, (0,) + self.items.append(_Item(metadata=metadata, coord=coord, dims=dims)) + result = Resolve._dim_coverage(self.cube, self.items, common) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + expected = [ + metadata, + self.items[0].metadata, + self.items[1].metadata, + None, + ] + self.assertEqual(expected, result.metadata) + expected = [coord, self.items[0].coord, self.items[1].coord, None] + self.assertEqual(expected, result.coords) + self.assertEqual([1, 2], result.dims_common) + self.assertEqual([0], result.dims_local) + self.assertEqual([3], result.dims_free) + + +class Test__aux_coverage(tests.IrisTest): + def setUp(self): + self.ndim = 4 + self.cube = mock.Mock(ndim=self.ndim) + # configure aux coords + self.items_aux = [] + aux_parts = [ + (sentinel.aux_metadata0, sentinel.aux_coord0, (0,)), + (sentinel.aux_metadata1, sentinel.aux_coord1, (1,)), + (sentinel.aux_metadata23, sentinel.aux_coord23, (2, 3)), + ] + column_aux_parts = [x for x in zip(*aux_parts)] + self.aux_metadata, self.aux_coords, self.aux_dims = [ + list(x) for x in column_aux_parts + ] + for metadata, coord, dims in aux_parts: + item = _Item(metadata=metadata, coord=coord, dims=dims) + self.items_aux.append(item) + # configure scalar coords + self.items_scalar = [] + scalar_parts = [ + (sentinel.scalar_metadata0, sentinel.scalar_coord0, ()), + (sentinel.scalar_metadata1, sentinel.scalar_coord1, ()), + (sentinel.scalar_metadata2, sentinel.scalar_coord2, ()), + ] + column_scalar_parts = [x for x in zip(*scalar_parts)] + self.scalar_metadata, self.scalar_coords, self.scalar_dims = [ + list(x) for x in column_scalar_parts + ] + for metadata, coord, dims in scalar_parts: + item = _Item(metadata=metadata, coord=coord, dims=dims) + self.items_scalar.append(item) + + def test_coverage_no_local_no_common_all_free(self): + items_aux, items_scalar = [], [] + common_aux, common_scalar = [], [] + result = Resolve._aux_coverage( + self.cube, items_aux, items_scalar, common_aux, common_scalar + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual([], result.common_items_aux) + self.assertEqual([], result.common_items_scalar) + self.assertEqual([], result.local_items_aux) + self.assertEqual([], result.local_items_scalar) + self.assertEqual([], result.dims_common) + self.assertEqual([], result.dims_local) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_free) + + def test_coverage_all_local_no_common_no_free(self): + common_aux, common_scalar = [], [] + result = Resolve._aux_coverage( + self.cube, + self.items_aux, + self.items_scalar, + common_aux, + common_scalar, + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + expected = [] + self.assertEqual(expected, result.common_items_aux) + self.assertEqual(expected, result.common_items_scalar) + self.assertEqual(self.items_aux, result.local_items_aux) + self.assertEqual(self.items_scalar, result.local_items_scalar) + self.assertEqual([], result.dims_common) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_no_local_all_common_no_free(self): + result = Resolve._aux_coverage( + self.cube, + self.items_aux, + self.items_scalar, + self.aux_metadata, + self.scalar_metadata, + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual(self.items_aux, result.common_items_aux) + self.assertEqual(self.items_scalar, result.common_items_scalar) + self.assertEqual([], result.local_items_aux) + self.assertEqual([], result.local_items_scalar) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_common) + self.assertEqual([], result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_mixed(self): + common_aux = [self.items_aux[-1].metadata] + common_scalar = [self.items_scalar[1].metadata] + self.items_aux.pop(1) + result = Resolve._aux_coverage( + self.cube, + self.items_aux, + self.items_scalar, + common_aux, + common_scalar, + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + expected = [self.items_aux[-1]] + self.assertEqual(expected, result.common_items_aux) + expected = [self.items_scalar[1]] + self.assertEqual(expected, result.common_items_scalar) + expected = [self.items_aux[0]] + self.assertEqual(expected, result.local_items_aux) + expected = [self.items_scalar[0], self.items_scalar[2]] + self.assertEqual(expected, result.local_items_scalar) + self.assertEqual([2, 3], result.dims_common) + self.assertEqual([0], result.dims_local) + self.assertEqual([1], result.dims_free) + + +class Test__metadata_coverage(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.m_lhs_cube = sentinel.lhs_cube + self.resolve.lhs_cube = self.m_lhs_cube + self.m_rhs_cube = sentinel.rhs_cube + self.resolve.rhs_cube = self.m_rhs_cube + self.m_items_dim_metadata = sentinel.items_dim_metadata + self.m_items_aux_metadata = sentinel.items_aux_metadata + self.m_items_scalar_metadata = sentinel.items_scalar_metadata + items_dim = [mock.Mock(metadata=self.m_items_dim_metadata)] + items_aux = [mock.Mock(metadata=self.m_items_aux_metadata)] + items_scalar = [mock.Mock(metadata=self.m_items_scalar_metadata)] + category = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.resolve.category_common = category + self.m_items_dim = sentinel.items_dim + self.m_items_aux = sentinel.items_aux + self.m_items_scalar = sentinel.items_scalar + category = _CategoryItems( + items_dim=self.m_items_dim, + items_aux=self.m_items_aux, + items_scalar=self.m_items_scalar, + ) + self.resolve.lhs_cube_category = category + self.resolve.rhs_cube_category = category + target = "iris.common.resolve.Resolve._dim_coverage" + self.m_lhs_cube_dim_coverage = sentinel.lhs_cube_dim_coverage + self.m_rhs_cube_dim_coverage = sentinel.rhs_cube_dim_coverage + side_effect = ( + self.m_lhs_cube_dim_coverage, + self.m_rhs_cube_dim_coverage, + ) + self.mocker_dim_coverage = self.patch(target, side_effect=side_effect) + target = "iris.common.resolve.Resolve._aux_coverage" + self.m_lhs_cube_aux_coverage = sentinel.lhs_cube_aux_coverage + self.m_rhs_cube_aux_coverage = sentinel.rhs_cube_aux_coverage + side_effect = ( + self.m_lhs_cube_aux_coverage, + self.m_rhs_cube_aux_coverage, + ) + self.mocker_aux_coverage = self.patch(target, side_effect=side_effect) + + def test(self): + self.resolve._metadata_coverage() + self.assertEqual(2, self.mocker_dim_coverage.call_count) + calls = [ + mock.call( + self.m_lhs_cube, self.m_items_dim, [self.m_items_dim_metadata] + ), + mock.call( + self.m_rhs_cube, self.m_items_dim, [self.m_items_dim_metadata] + ), + ] + self.assertEqual(calls, self.mocker_dim_coverage.call_args_list) + self.assertEqual(2, self.mocker_aux_coverage.call_count) + calls = [ + mock.call( + self.m_lhs_cube, + self.m_items_aux, + self.m_items_scalar, + [self.m_items_aux_metadata], + [self.m_items_scalar_metadata], + ), + mock.call( + self.m_rhs_cube, + self.m_items_aux, + self.m_items_scalar, + [self.m_items_aux_metadata], + [self.m_items_scalar_metadata], + ), + ] + self.assertEqual(calls, self.mocker_aux_coverage.call_args_list) + self.assertEqual( + self.m_lhs_cube_dim_coverage, self.resolve.lhs_cube_dim_coverage + ) + self.assertEqual( + self.m_rhs_cube_dim_coverage, self.resolve.rhs_cube_dim_coverage + ) + self.assertEqual( + self.m_lhs_cube_aux_coverage, self.resolve.lhs_cube_aux_coverage + ) + self.assertEqual( + self.m_rhs_cube_aux_coverage, self.resolve.rhs_cube_aux_coverage + ) + + +class Test__dim_mapping(tests.IrisTest): + def setUp(self): + self.ndim = 3 + Wrapper = namedtuple("Wrapper", ("name",)) + cube = Wrapper(name=lambda: sentinel.name) + self.src_coverage = _DimCoverage( + cube=cube, + metadata=[], + coords=None, + dims_common=None, + dims_local=None, + dims_free=None, + ) + self.tgt_coverage = _DimCoverage( + cube=cube, + metadata=[], + coords=None, + dims_common=[], + dims_local=None, + dims_free=None, + ) + self.metadata = [ + sentinel.metadata_0, + sentinel.metadata_1, + sentinel.metadata_2, + ] + self.dummy = [sentinel.dummy_0, sentinel.dummy_1, sentinel.dummy_2] + + def test_no_mapping(self): + self.src_coverage.metadata.extend(self.metadata) + self.tgt_coverage.metadata.extend(self.dummy) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + self.assertEqual(dict(), result) + + def test_full_mapping(self): + self.src_coverage.metadata.extend(self.metadata) + self.tgt_coverage.metadata.extend(self.metadata) + dims_common = list(range(self.ndim)) + self.tgt_coverage.dims_common.extend(dims_common) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 1: 1, 2: 2} + self.assertEqual(expected, result) + + def test_transpose_mapping(self): + self.src_coverage.metadata.extend(self.metadata[::-1]) + self.tgt_coverage.metadata.extend(self.metadata) + dims_common = list(range(self.ndim)) + self.tgt_coverage.dims_common.extend(dims_common) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 1: 1, 2: 0} + self.assertEqual(expected, result) + + def test_partial_mapping__transposed(self): + self.src_coverage.metadata.extend(self.metadata) + self.metadata[1] = sentinel.nope + self.tgt_coverage.metadata.extend(self.metadata[::-1]) + dims_common = [0, 2] + self.tgt_coverage.dims_common.extend(dims_common) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 2: 0} + self.assertEqual(expected, result) + + def test_bad_metadata_mapping(self): + self.src_coverage.metadata.extend(self.metadata) + self.metadata[0] = sentinel.bad + self.tgt_coverage.metadata.extend(self.metadata) + dims_common = [0] + self.tgt_coverage.dims_common.extend(dims_common) + emsg = "Failed to map common dim coordinate metadata" + with self.assertRaisesRegex(ValueError, emsg): + _ = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + + +class Test__aux_mapping(tests.IrisTest): + def setUp(self): + self.ndim = 3 + Wrapper = namedtuple("Wrapper", ("name",)) + cube = Wrapper(name=lambda: sentinel.name) + self.src_coverage = _AuxCoverage( + cube=cube, + common_items_aux=[], + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=None, + dims_common=None, + dims_local=None, + dims_free=None, + ) + self.tgt_coverage = _AuxCoverage( + cube=cube, + common_items_aux=[], + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=None, + dims_common=None, + dims_local=None, + dims_free=None, + ) + self.items = [ + _Item( + metadata=sentinel.metadata0, coord=sentinel.coord0, dims=[0] + ), + _Item( + metadata=sentinel.metadata1, coord=sentinel.coord1, dims=[1] + ), + _Item( + metadata=sentinel.metadata2, coord=sentinel.coord2, dims=[2] + ), + ] + + def _copy(self, items): + # Due to a bug in python 3.6.x, performing a deepcopy of a mock.sentinel + # will yield an object that is not equivalent to its parent, so this + # is a work-around until we drop support for python 3.6.x. + import sys + + version = sys.version_info + major, minor = version.major, version.minor + result = deepcopy(items) + if major == 3 and minor <= 6: + for i, item in enumerate(items): + result[i] = result[i]._replace(metadata=item.metadata) + return result + + def test_no_mapping(self): + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + self.assertEqual(dict(), result) + + def test_full_mapping(self): + self.src_coverage.common_items_aux.extend(self.items) + self.tgt_coverage.common_items_aux.extend(self.items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 1: 1, 2: 2} + self.assertEqual(expected, result) + + def test_transpose_mapping(self): + self.src_coverage.common_items_aux.extend(self.items) + items = self._copy(self.items) + items[0].dims[0] = 2 + items[2].dims[0] = 0 + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 1: 1, 2: 0} + self.assertEqual(expected, result) + + def test_partial_mapping__transposed(self): + _ = self.items.pop(1) + self.src_coverage.common_items_aux.extend(self.items) + items = self._copy(self.items) + items[0].dims[0] = 2 + items[1].dims[0] = 0 + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 2: 0} + self.assertEqual(expected, result) + + def test_mapping__match_multiple_src_metadata(self): + items = self._copy(self.items) + _ = self.items.pop(1) + self.src_coverage.common_items_aux.extend(self.items) + items[1] = items[0] + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 2: 2} + self.assertEqual(expected, result) + + def test_mapping__skip_match_multiple_src_metadata(self): + items = self._copy(self.items) + _ = self.items.pop(1) + self.tgt_coverage.common_items_aux.extend(self.items) + items[1] = items[0]._replace(dims=[1]) + self.src_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {2: 2} + self.assertEqual(expected, result) + + def test_mapping__skip_different_rank(self): + items = self._copy(self.items) + self.src_coverage.common_items_aux.extend(self.items) + items[2] = items[2]._replace(dims=[1, 2]) + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 1: 1} + self.assertEqual(expected, result) + + def test_bad_metadata_mapping(self): + self.src_coverage.common_items_aux.extend(self.items) + items = self._copy(self.items) + items[0] = items[0]._replace(metadata=sentinel.bad) + self.tgt_coverage.common_items_aux.extend(items) + emsg = "Failed to map common aux coordinate metadata" + with self.assertRaisesRegex(ValueError, emsg): + _ = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + + +class Test_mapped(tests.IrisTest): + def test_mapping_none(self): + resolve = Resolve() + self.assertIsNone(resolve.mapping) + self.assertIsNone(resolve.mapped) + + def test_mapped__src_cube_lhs(self): + resolve = Resolve() + lhs = mock.Mock(ndim=2) + rhs = mock.Mock(ndim=3) + resolve.lhs_cube = lhs + resolve.rhs_cube = rhs + resolve.map_rhs_to_lhs = False + resolve.mapping = {0: 0, 1: 1} + self.assertTrue(resolve.mapped) + + def test_mapped__src_cube_rhs(self): + resolve = Resolve() + lhs = mock.Mock(ndim=3) + rhs = mock.Mock(ndim=2) + resolve.lhs_cube = lhs + resolve.rhs_cube = rhs + resolve.map_rhs_to_lhs = True + resolve.mapping = {0: 0, 1: 1} + self.assertTrue(resolve.mapped) + + def test_partial_mapping(self): + resolve = Resolve() + lhs = mock.Mock(ndim=3) + rhs = mock.Mock(ndim=2) + resolve.lhs_cube = lhs + resolve.rhs_cube = rhs + resolve.map_rhs_to_lhs = True + resolve.mapping = {0: 0} + self.assertFalse(resolve.mapped) + + +class Test__free_mapping(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Wrapper", ("name", "ndim", "shape")) + self.src_dim_coverage = dict( + cube=None, + metadata=None, + coords=None, + dims_common=None, + dims_local=None, + dims_free=[], + ) + self.tgt_dim_coverage = deepcopy(self.src_dim_coverage) + self.src_aux_coverage = dict( + cube=None, + common_items_aux=None, + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=None, + dims_common=None, + dims_local=None, + dims_free=[], + ) + self.tgt_aux_coverage = deepcopy(self.src_aux_coverage) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve.mapping = {} + + def _make_args(self): + args = dict( + src_dim_coverage=_DimCoverage(**self.src_dim_coverage), + tgt_dim_coverage=_DimCoverage(**self.tgt_dim_coverage), + src_aux_coverage=_AuxCoverage(**self.src_aux_coverage), + tgt_aux_coverage=_AuxCoverage(**self.tgt_aux_coverage), + ) + return args + + def test_mapping_no_dims_free(self): + ndim = 4 + shape = tuple(range(ndim)) + cube = self.Cube(name=lambda: "name", ndim=ndim, shape=shape) + self.src_dim_coverage["cube"] = cube + self.tgt_dim_coverage["cube"] = cube + args = self._make_args() + emsg = "Insufficient matching coordinate metadata" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._free_mapping(**args) + + def _make_coverage(self, name, shape, dims_free): + if name == "src": + dim_coverage = self.src_dim_coverage + aux_coverage = self.src_aux_coverage + else: + dim_coverage = self.tgt_dim_coverage + aux_coverage = self.tgt_aux_coverage + ndim = len(shape) + cube = self.Cube(name=lambda: name, ndim=ndim, shape=shape) + dim_coverage["cube"] = cube + dim_coverage["dims_free"].extend(dims_free) + aux_coverage["cube"] = cube + aux_coverage["dims_free"].extend(dims_free) + + def test_mapping_src_free_to_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 4 + # state f l c l state f c f + # coord d d d a coord a d d + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_local__broadcast_src_first(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 4 + # state f l c l state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (1, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_local__broadcast_src_last(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 1 + # state f l c l state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_local__broadcast_src_both(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 1 + # state f l c l state f c f + # coord d d d a coord a d d + # bcast ^ ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->1 1->2 2->3 + src_shape = (1, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 1, 1: 2, 2: 3} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 4 + # state f f c f state f c f + # coord d d d a coord a d d + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free__broadcast_src_first(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 4 + # state f f c f state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (1, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free__broadcast_src_last(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 1 + # state f f c f state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (2, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free__broadcast_src_both(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 1 + # state f f c f state f c f + # coord d d d a coord a d d + # bcast ^ ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (1, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt__fail(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 5 + # state f f c f state f c f + # coord d d d a coord a d d + # fail ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->? + src_shape = (2, 3, 5) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + emsg = "Insufficient matching coordinate metadata to resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._free_mapping(**args) + + def test_mapping_tgt_free_to_src_local(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_local__broadcast_tgt_first(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 1 3 2 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 1, 3, 2) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_local__broadcast_tgt_last(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 1 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 1) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_local__broadcast_tgt_both(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 1 3 1 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # bcast ^ ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->1 1->2 2->3 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 1, 3, 1) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 1, 1: 2, 2: 3} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_no_free__fail(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 5 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # fail ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->? + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 5) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + emsg = "Insufficient matching coordinate metadata to resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._free_mapping(**args) + + +class Test__src_cube(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._src_cube) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.lhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._src_cube) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube + + +class Test__src_cube_position(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.assertEqual("RHS", self.resolve._src_cube_position) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.assertEqual("LHS", self.resolve._src_cube_position) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube_position + + +class Test__src_cube_resolved__getter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._src_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.lhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._src_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube_resolved + + +class Test__src_cube_resolved__setter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve._src_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.rhs_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve._src_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.lhs_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube_resolved = self.expected + + +class Test__tgt_cube(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.rhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.lhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube + + +class Test__tgt_cube_position(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.assertEqual("RHS", self.resolve._tgt_cube_position) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.assertEqual("LHS", self.resolve._tgt_cube_position) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube_position + + +class Test__tgt_cube_resolved__getter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.rhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.lhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube_resolved + + +class Test__tgt_cube_resolved__setter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve._tgt_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.rhs_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve._tgt_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.lhs_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube_resolved = self.expected + + +class Test_shape(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + + def test_no_shape(self): + self.assertIsNone(self.resolve.shape) + + def test_shape(self): + expected = sentinel.shape + self.resolve._broadcast_shape = expected + self.assertEqual(expected, self.resolve.shape) + + +class Test__as_compatible_cubes(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple( + "Wrapper", + ( + "name", + "ndim", + "shape", + "metadata", + "core_data", + "coord_dims", + "dim_coords", + "aux_coords", + "aux_factories", + ), + ) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve.mapping = {} + self.mocker = self.patch("iris.cube.Cube") + self.args = dict( + name=None, + ndim=None, + shape=None, + metadata=None, + core_data=None, + coord_dims=None, + dim_coords=None, + aux_coords=None, + aux_factories=None, + ) + + def _make_cube(self, name, shape, transpose_shape=None): + self.args["name"] = lambda: name + ndim = len(shape) + self.args["ndim"] = ndim + self.args["shape"] = shape + if name == "src": + self.args["metadata"] = sentinel.metadata + self.reshape = sentinel.reshape + m_reshape = mock.Mock(return_value=self.reshape) + self.transpose = mock.Mock( + shape=transpose_shape, reshape=m_reshape + ) + m_transpose = mock.Mock(return_value=self.transpose) + self.data = mock.Mock( + shape=shape, transpose=m_transpose, reshape=m_reshape + ) + m_copy = mock.Mock(return_value=self.data) + m_core_data = mock.Mock(copy=m_copy) + self.args["core_data"] = mock.Mock(return_value=m_core_data) + self.args["coord_dims"] = mock.Mock(side_effect=([0], [ndim - 1])) + self.dim_coord = sentinel.dim_coord + self.aux_coord = sentinel.aux_coord + self.aux_factory = sentinel.aux_factory + self.args["dim_coords"] = [self.dim_coord] + self.args["aux_coords"] = [self.aux_coord] + self.args["aux_factories"] = [self.aux_factory] + cube = self.Cube(**self.args) + self.resolve.rhs_cube = cube + self.cube = mock.Mock() + self.mocker.return_value = self.cube + else: + cube = self.Cube(**self.args) + self.resolve.lhs_cube = cube + + def test_incomplete_src_to_tgt_mapping__fail(self): + src_shape = (1, 2) + self._make_cube("src", src_shape) + tgt_shape = (3, 4) + self._make_cube("tgt", tgt_shape) + with self.assertRaises(AssertionError): + self.resolve._as_compatible_cubes() + + def test_incompatible_shapes__fail(self): + # key: (state) c=common, f=free + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 2 3 4 shape 2 3 5 + # state f c c c state c c c + # fail ^ fail ^ + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + src_shape = (2, 3, 5) + self._make_cube("src", src_shape) + tgt_shape = (2, 2, 3, 4) + self._make_cube("tgt", tgt_shape) + self.resolve.mapping = {0: 1, 1: 2, 2: 3} + emsg = "Cannot resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._as_compatible_cubes() + + def test_incompatible_shapes__fail_broadcast(self): + # key: (state) c=common, f=free + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 5 + # state f c c c state c c c + # fail ^ fail ^ + # + # src-to-tgt mapping: + # 0->3, 1->2, 2->1 + src_shape = (2, 3, 5) + self._make_cube("src", src_shape) + tgt_shape = (2, 4, 3, 2) + self._make_cube("tgt", tgt_shape) + self.resolve.mapping = {0: 3, 1: 2, 2: 1} + emsg = "Cannot resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._as_compatible_cubes() + + def _check_compatible(self, broadcast_shape): + self.assertEqual( + self.resolve.lhs_cube, self.resolve._tgt_cube_resolved + ) + self.assertEqual(self.cube, self.resolve._src_cube_resolved) + self.assertEqual(broadcast_shape, self.resolve._broadcast_shape) + self.assertEqual(1, self.mocker.call_count) + self.assertEqual(self.args["metadata"], self.cube.metadata) + self.assertEqual(2, self.resolve.rhs_cube.coord_dims.call_count) + self.assertEqual( + [mock.call(self.dim_coord), mock.call(self.aux_coord)], + self.resolve.rhs_cube.coord_dims.call_args_list, + ) + self.assertEqual(1, self.cube.add_dim_coord.call_count) + self.assertEqual( + [mock.call(self.dim_coord, [self.resolve.mapping[0]])], + self.cube.add_dim_coord.call_args_list, + ) + self.assertEqual(1, self.cube.add_aux_coord.call_count) + self.assertEqual( + [mock.call(self.aux_coord, [self.resolve.mapping[2]])], + self.cube.add_aux_coord.call_args_list, + ) + self.assertEqual(1, self.cube.add_aux_factory.call_count) + self.assertEqual( + [mock.call(self.aux_factory)], + self.cube.add_aux_factory.call_args_list, + ) + + def test_compatible(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 4 3 2 shape 4 3 2 + # state c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2 + src_shape = (4, 3, 2) + self._make_cube("src", src_shape) + tgt_shape = (4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 0, 1: 1, 2: 2} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual([mock.call(self.data)], self.mocker.call_args_list) + + def test_compatible__transpose(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 4 3 2 shape 2 3 4 + # state c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->2, 1->1, 2->0 + src_shape = (2, 3, 4) + self._make_cube("src", src_shape, transpose_shape=(4, 3, 2)) + tgt_shape = (4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 2, 1: 1, 2: 0} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual(1, self.data.transpose.call_count) + self.assertEqual( + [mock.call([2, 1, 0])], self.data.transpose.call_args_list + ) + self.assertEqual( + [mock.call(self.transpose)], self.mocker.call_args_list + ) + + def test_compatible__reshape(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + src_shape = (4, 3, 2) + self._make_cube("src", src_shape) + tgt_shape = (5, 4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 1, 1: 2, 2: 3} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual(1, self.data.reshape.call_count) + self.assertEqual( + [mock.call((1,) + src_shape)], self.data.reshape.call_args_list + ) + self.assertEqual([mock.call(self.reshape)], self.mocker.call_args_list) + + def test_compatible__transpose_reshape(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 2 3 4 + # state f c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->3, 1->2, 2->1 + src_shape = (2, 3, 4) + transpose_shape = (4, 3, 2) + self._make_cube("src", src_shape, transpose_shape=transpose_shape) + tgt_shape = (5, 4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 3, 1: 2, 2: 1} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual(1, self.data.transpose.call_count) + self.assertEqual( + [mock.call([2, 1, 0])], self.data.transpose.call_args_list + ) + self.assertEqual(1, self.data.reshape.call_count) + self.assertEqual( + [mock.call((1,) + transpose_shape)], + self.data.reshape.call_args_list, + ) + self.assertEqual([mock.call(self.reshape)], self.mocker.call_args_list) + + def test_compatible__broadcast(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 1 3 2 shape 4 1 2 + # state c c c state c c c + # coord d a + # bcast ^ bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2 + src_shape = (4, 1, 2) + self._make_cube("src", src_shape) + tgt_shape = (1, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 0, 1: 1, 2: 2} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=(4, 3, 2)) + self.assertEqual([mock.call(self.data)], self.mocker.call_args_list) + + def test_compatible__broadcast_transpose_reshape(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 1 3 2 shape 2 1 4 + # state f c c c state c c c + # coord d a + # bcast ^ bcast ^ + # + # src-to-tgt mapping: + # 0->3, 1->2, 2->1 + src_shape = (2, 1, 4) + transpose_shape = (4, 1, 2) + self._make_cube("src", src_shape) + tgt_shape = (5, 1, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 3, 1: 2, 2: 1} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=(5, 4, 3, 2)) + self.assertEqual(1, self.data.transpose.call_count) + self.assertEqual( + [mock.call([2, 1, 0])], self.data.transpose.call_args_list + ) + self.assertEqual(1, self.data.reshape.call_count) + self.assertEqual( + [mock.call((1,) + transpose_shape)], + self.data.reshape.call_args_list, + ) + self.assertEqual([mock.call(self.reshape)], self.mocker.call_args_list) + + +class Test__metadata_mapping(tests.IrisTest): + def setUp(self): + self.ndim = sentinel.ndim + self.src_cube = mock.Mock(ndim=self.ndim) + self.src_dim_coverage = mock.Mock(dims_free=[]) + self.src_aux_coverage = mock.Mock(dims_free=[]) + self.tgt_cube = mock.Mock(ndim=self.ndim) + self.tgt_dim_coverage = mock.Mock(dims_free=[]) + self.tgt_aux_coverage = mock.Mock(dims_free=[]) + self.resolve = Resolve() + self.map_rhs_to_lhs = True + self.resolve.map_rhs_to_lhs = self.map_rhs_to_lhs + self.resolve.rhs_cube = self.src_cube + self.resolve.rhs_cube_dim_coverage = self.src_dim_coverage + self.resolve.rhs_cube_aux_coverage = self.src_aux_coverage + self.resolve.lhs_cube = self.tgt_cube + self.resolve.lhs_cube_dim_coverage = self.tgt_dim_coverage + self.resolve.lhs_cube_aux_coverage = self.tgt_aux_coverage + self.resolve.mapping = {} + self.shape = sentinel.shape + self.resolve._broadcast_shape = self.shape + self.resolve._src_cube_resolved = mock.Mock(shape=self.shape) + self.resolve._tgt_cube_resolved = mock.Mock(shape=self.shape) + self.m_dim_mapping = self.patch( + "iris.common.resolve.Resolve._dim_mapping", return_value={} + ) + self.m_aux_mapping = self.patch( + "iris.common.resolve.Resolve._aux_mapping", return_value={} + ) + self.m_free_mapping = self.patch( + "iris.common.resolve.Resolve._free_mapping" + ) + self.m_as_compatible_cubes = self.patch( + "iris.common.resolve.Resolve._as_compatible_cubes" + ) + self.mapping = {0: 1, 1: 2, 2: 3} + + def test_mapped__dim_coords(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord d d d coord d d d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.src_cube.ndim = 3 + self.m_dim_mapping.return_value = self.mapping + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(0, self.m_aux_mapping.call_count) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__aux_coords(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord a a a coord a a a + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.src_cube.ndim = 3 + self.m_aux_mapping.return_value = self.mapping + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__dim_and_aux_coords(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord d a d coord d a d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + dim_mapping = {0: 1, 2: 3} + aux_mapping = {1: 2} + self.src_cube.ndim = 3 + self.m_dim_mapping.return_value = dim_mapping + self.m_aux_mapping.return_value = aux_mapping + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__dim_coords_and_free_dims(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state l f c c state f c c + # coord d d d coord d d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + dim_mapping = {1: 2, 2: 3} + free_mapping = {0: 1} + self.src_cube.ndim = 3 + self.m_dim_mapping.return_value = dim_mapping + side_effect = lambda a, b, c, d: self.resolve.mapping.update( + free_mapping + ) + self.m_free_mapping.side_effect = side_effect + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(1, self.m_free_mapping.call_count) + expected = [ + mock.call( + self.src_dim_coverage, + self.tgt_dim_coverage, + self.src_aux_coverage, + self.tgt_aux_coverage, + ) + ] + self.assertEqual(expected, self.m_free_mapping.call_args_list) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__dim_coords_with_broadcast_flip(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 4 dims 0 1 2 4 + # shape 1 4 3 2 shape 5 4 3 2 + # state c c c c state c c c c + # coord d d d d coord d d d d + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2, 3->3 + mapping = {0: 0, 1: 1, 2: 2, 3: 3} + self.src_cube.ndim = 4 + self.tgt_cube.ndim = 4 + self.m_dim_mapping.return_value = mapping + broadcast_shape = (5, 4, 3, 2) + self.resolve._broadcast_shape = broadcast_shape + self.resolve._src_cube_resolved.shape = broadcast_shape + self.resolve._tgt_cube_resolved.shape = (1, 4, 3, 2) + self.resolve._metadata_mapping() + self.assertEqual(mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(0, self.m_aux_mapping.call_count) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(2, self.m_as_compatible_cubes.call_count) + self.assertEqual(not self.map_rhs_to_lhs, self.resolve.map_rhs_to_lhs) + + def test_mapped__dim_coords_free_flip_with_free_flip(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 4 3 2 shape 4 3 2 + # state f f c state l l c + # coord d coord d d d + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2 + dim_mapping = {2: 2} + free_mapping = {0: 0, 1: 1} + mapping = {0: 0, 1: 1, 2: 2} + self.src_cube.ndim = 3 + self.tgt_cube.ndim = 3 + self.m_dim_mapping.return_value = dim_mapping + side_effect = lambda a, b, c, d: self.resolve.mapping.update( + free_mapping + ) + self.m_free_mapping.side_effect = side_effect + self.tgt_dim_coverage.dims_free = [0, 1] + self.tgt_aux_coverage.dims_free = [0, 1] + self.resolve._metadata_mapping() + self.assertEqual(mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(1, self.m_free_mapping.call_count) + expected = [ + mock.call( + self.src_dim_coverage, + self.tgt_dim_coverage, + self.src_aux_coverage, + self.tgt_aux_coverage, + ) + ] + self.assertEqual(expected, self.m_free_mapping.call_args_list) + self.assertEqual(2, self.m_as_compatible_cubes.call_count) + + +class Test__prepare_common_dim_payload(tests.IrisTest): + def setUp(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state l c c c state c c c + # coord d d d coord d d d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.points = (sentinel.points_0, sentinel.points_1, sentinel.points_2) + self.bounds = (sentinel.bounds_0, sentinel.bounds_1, sentinel.bounds_2) + self.pb_0 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[0])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[0])), + ) + self.pb_1 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[1])), + None, + ) + self.pb_2 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[2])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[2])), + ) + side_effect = (self.pb_0, self.pb_1, self.pb_2) + self.m_prepare_points_and_bounds = self.patch( + "iris.common.resolve.Resolve._prepare_points_and_bounds", + side_effect=side_effect, + ) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.mapping = {0: 1, 1: 2, 2: 3} + self.resolve.mapping = self.mapping + self.metadata_combined = ( + sentinel.combined_0, + sentinel.combined_1, + sentinel.combined_2, + ) + self.src_metadata = mock.Mock( + combine=mock.Mock(side_effect=self.metadata_combined) + ) + metadata = [self.src_metadata] * len(self.mapping) + self.src_coords = [ + sentinel.src_coord_0, + sentinel.src_coord_1, + sentinel.src_coord_2, + ] + self.src_dims_common = [0, 1, 2] + self.container = DimCoord + self.src_dim_coverage = _DimCoverage( + cube=None, + metadata=metadata, + coords=self.src_coords, + dims_common=self.src_dims_common, + dims_local=[], + dims_free=[], + ) + self.tgt_metadata = [ + sentinel.tgt_metadata_0, + sentinel.tgt_metadata_1, + sentinel.tgt_metadata_2, + sentinel.tgt_metadata_3, + ] + self.tgt_coords = [ + sentinel.tgt_coord_0, + sentinel.tgt_coord_1, + sentinel.tgt_coord_2, + sentinel.tgt_coord_3, + ] + self.tgt_dims_common = [1, 2, 3] + self.tgt_dim_coverage = _DimCoverage( + cube=None, + metadata=self.tgt_metadata, + coords=self.tgt_coords, + dims_common=self.tgt_dims_common, + dims_local=[], + dims_free=[], + ) + + def _check(self, ignore_mismatch=None, bad_points=None): + if bad_points is None: + bad_points = False + self.resolve._prepare_common_dim_payload( + self.src_dim_coverage, + self.tgt_dim_coverage, + ignore_mismatch=ignore_mismatch, + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + if not bad_points: + self.assertEqual(3, len(self.resolve.prepared_category.items_dim)) + expected = [ + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[0], + src=self.src_metadata, + tgt=self.tgt_metadata[self.mapping[0]], + ), + points=self.points[0], + bounds=self.bounds[0], + dims=(self.mapping[0],), + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[1], + src=self.src_metadata, + tgt=self.tgt_metadata[self.mapping[1]], + ), + points=self.points[1], + bounds=None, + dims=(self.mapping[1],), + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[2], + src=self.src_metadata, + tgt=self.tgt_metadata[self.mapping[2]], + ), + points=self.points[2], + bounds=self.bounds[2], + dims=(self.mapping[2],), + container=self.container, + ), + ] + self.assertEqual( + expected, self.resolve.prepared_category.items_dim + ) + else: + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + self.assertEqual(3, self.m_prepare_points_and_bounds.call_count) + if ignore_mismatch is None: + ignore_mismatch = False + expected = [ + mock.call( + self.src_coords[0], + self.tgt_coords[self.mapping[0]], + 0, + 1, + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[1], + self.tgt_coords[self.mapping[1]], + 1, + 2, + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[2], + self.tgt_coords[self.mapping[2]], + 2, + 3, + ignore_mismatch=ignore_mismatch, + ), + ] + self.assertEqual( + expected, self.m_prepare_points_and_bounds.call_args_list + ) + if not bad_points: + self.assertEqual(3, self.src_metadata.combine.call_count) + expected = [ + mock.call(metadata) for metadata in self.tgt_metadata[1:] + ] + self.assertEqual( + expected, self.src_metadata.combine.call_args_list + ) + + def test__default_ignore_mismatch(self): + self._check() + + def test__not_ignore_mismatch(self): + self._check(ignore_mismatch=False) + + def test__ignore_mismatch(self): + self._check(ignore_mismatch=True) + + def test__bad_points(self): + side_effect = [(None, None)] * len(self.mapping) + self.m_prepare_points_and_bounds.side_effect = side_effect + self._check(bad_points=True) + + +class Test__prepare_common_aux_payload(tests.IrisTest): + def setUp(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state l c c c state c c c + # coord a a a coord a a a + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.points = (sentinel.points_0, sentinel.points_1, sentinel.points_2) + self.bounds = (sentinel.bounds_0, sentinel.bounds_1, sentinel.bounds_2) + self.pb_0 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[0])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[0])), + ) + self.pb_1 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[1])), + None, + ) + self.pb_2 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[2])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[2])), + ) + side_effect = (self.pb_0, self.pb_1, self.pb_2) + self.m_prepare_points_and_bounds = self.patch( + "iris.common.resolve.Resolve._prepare_points_and_bounds", + side_effect=side_effect, + ) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.mapping = {0: 1, 1: 2, 2: 3} + self.resolve.mapping = self.mapping + self.resolve.map_rhs_to_lhs = True + self.metadata_combined = ( + sentinel.combined_0, + sentinel.combined_1, + sentinel.combined_2, + ) + self.src_metadata = [ + mock.Mock( + combine=mock.Mock(return_value=self.metadata_combined[0]) + ), + mock.Mock( + combine=mock.Mock(return_value=self.metadata_combined[1]) + ), + mock.Mock( + combine=mock.Mock(return_value=self.metadata_combined[2]) + ), + ] + self.src_coords = [ + sentinel.src_coord_0, + sentinel.src_coord_1, + sentinel.src_coord_2, + ] + self.src_dims = [(dim,) for dim in self.mapping.keys()] + self.src_common_items = [ + _Item(*item) + for item in zip(self.src_metadata, self.src_coords, self.src_dims) + ] + self.tgt_metadata = [sentinel.tgt_metadata_0] + self.src_metadata + self.tgt_coords = [ + sentinel.tgt_coord_0, + sentinel.tgt_coord_1, + sentinel.tgt_coord_2, + sentinel.tgt_coord_3, + ] + self.tgt_dims = [None] + [(dim,) for dim in self.mapping.values()] + self.tgt_common_items = [ + _Item(*item) + for item in zip(self.tgt_metadata, self.tgt_coords, self.tgt_dims) + ] + self.container = type(self.src_coords[0]) + + def _check(self, ignore_mismatch=None, bad_points=None): + if bad_points is None: + bad_points = False + prepared_items = [] + self.resolve._prepare_common_aux_payload( + self.src_common_items, + self.tgt_common_items, + prepared_items, + ignore_mismatch=ignore_mismatch, + ) + if not bad_points: + self.assertEqual(3, len(prepared_items)) + expected = [ + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[0], + src=self.src_metadata[0], + tgt=self.tgt_metadata[self.mapping[0]], + ), + points=self.points[0], + bounds=self.bounds[0], + dims=self.tgt_dims[self.mapping[0]], + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[1], + src=self.src_metadata[1], + tgt=self.tgt_metadata[self.mapping[1]], + ), + points=self.points[1], + bounds=None, + dims=self.tgt_dims[self.mapping[1]], + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[2], + src=self.src_metadata[2], + tgt=self.tgt_metadata[self.mapping[2]], + ), + points=self.points[2], + bounds=self.bounds[2], + dims=self.tgt_dims[self.mapping[2]], + container=self.container, + ), + ] + self.assertEqual(expected, prepared_items) + else: + self.assertEqual(0, len(prepared_items)) + self.assertEqual(3, self.m_prepare_points_and_bounds.call_count) + if ignore_mismatch is None: + ignore_mismatch = False + expected = [ + mock.call( + self.src_coords[0], + self.tgt_coords[self.mapping[0]], + self.src_dims[0], + self.tgt_dims[self.mapping[0]], + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[1], + self.tgt_coords[self.mapping[1]], + self.src_dims[1], + self.tgt_dims[self.mapping[1]], + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[2], + self.tgt_coords[self.mapping[2]], + self.src_dims[2], + self.tgt_dims[self.mapping[2]], + ignore_mismatch=ignore_mismatch, + ), + ] + self.assertEqual( + expected, self.m_prepare_points_and_bounds.call_args_list + ) + if not bad_points: + for src_metadata, tgt_metadata in zip( + self.src_metadata, self.tgt_metadata[1:] + ): + self.assertEqual(1, src_metadata.combine.call_count) + expected = [mock.call(tgt_metadata)] + self.assertEqual(expected, src_metadata.combine.call_args_list) + + def test__default_ignore_mismatch(self): + self._check() + + def test__not_ignore_mismatch(self): + self._check(ignore_mismatch=False) + + def test__ignore_mismatch(self): + self._check(ignore_mismatch=True) + + def test__bad_points(self): + side_effect = [(None, None)] * len(self.mapping) + self.m_prepare_points_and_bounds.side_effect = side_effect + self._check(bad_points=True) + + def test__no_tgt_metadata_match(self): + item = self.tgt_common_items[0] + tgt_common_items = [item] * len(self.tgt_common_items) + prepared_items = [] + self.resolve._prepare_common_aux_payload( + self.src_common_items, tgt_common_items, prepared_items + ) + self.assertEqual(0, len(prepared_items)) + + def test__multi_tgt_metadata_match(self): + item = self.tgt_common_items[1] + tgt_common_items = [item] * len(self.tgt_common_items) + prepared_items = [] + self.resolve._prepare_common_aux_payload( + self.src_common_items, tgt_common_items, prepared_items + ) + self.assertEqual(0, len(prepared_items)) + + +class Test__prepare_points_and_bounds(tests.IrisTest): + def setUp(self): + self.Coord = namedtuple( + "Coord", + [ + "name", + "points", + "bounds", + "metadata", + "ndim", + "shape", + "has_bounds", + ], + ) + self.Cube = namedtuple("Cube", ["name", "shape"]) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.src_name = sentinel.src_name + self.src_points = sentinel.src_points + self.src_bounds = sentinel.src_bounds + self.src_metadata = sentinel.src_metadata + self.src_items = dict( + name=lambda: self.src_name, + points=self.src_points, + bounds=self.src_bounds, + metadata=self.src_metadata, + ndim=None, + shape=None, + has_bounds=None, + ) + self.tgt_name = sentinel.tgt_name + self.tgt_points = sentinel.tgt_points + self.tgt_bounds = sentinel.tgt_bounds + self.tgt_metadata = sentinel.tgt_metadata + self.tgt_items = dict( + name=lambda: self.tgt_name, + points=self.tgt_points, + bounds=self.tgt_bounds, + metadata=self.tgt_metadata, + ndim=None, + shape=None, + has_bounds=None, + ) + self.m_array_equal = self.patch( + "iris.util.array_equal", side_effect=(True, True) + ) + + def test_coord_ndim_unequal__tgt_ndim_greater(self): + self.src_items["ndim"] = 1 + src_coord = self.Coord(**self.src_items) + self.tgt_items["ndim"] = 10 + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims=None, tgt_dims=None + ) + self.assertEqual(self.tgt_points, points) + self.assertEqual(self.tgt_bounds, bounds) + + def test_coord_ndim_unequal__src_ndim_greater(self): + self.src_items["ndim"] = 10 + src_coord = self.Coord(**self.src_items) + self.tgt_items["ndim"] = 1 + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims=None, tgt_dims=None + ) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + + def test_coord_ndim_equal__shape_unequal_with_src_broadcasting(self): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 9 9 shape 1 9 + # state c c state c c + # coord x-x coord x-x + # bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + broadcast_shape = (9, 9) + ndim = len(broadcast_shape) + self.resolve.mapping = mapping + self.resolve._broadcast_shape = broadcast_shape + src_shape = (1, 9) + src_dims = tuple(mapping.keys()) + self.resolve.rhs_cube = self.Cube(name=None, shape=src_shape) + self.src_items["ndim"] = ndim + self.src_items["shape"] = src_shape + src_coord = self.Coord(**self.src_items) + tgt_shape = broadcast_shape + tgt_dims = tuple(mapping.values()) + self.resolve.lhs_cube = self.Cube(name=None, shape=tgt_shape) + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = tgt_shape + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims, tgt_dims + ) + self.assertEqual(self.tgt_points, points) + self.assertEqual(self.tgt_bounds, bounds) + + def test_coord_ndim_equal__shape_unequal_with_tgt_broadcasting(self): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 1 9 shape 9 9 + # state c c state c c + # coord x-x coord x-x + # bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + broadcast_shape = (9, 9) + ndim = len(broadcast_shape) + self.resolve.mapping = mapping + self.resolve._broadcast_shape = broadcast_shape + src_shape = broadcast_shape + src_dims = tuple(mapping.keys()) + self.resolve.rhs_cube = self.Cube(name=None, shape=src_shape) + self.src_items["ndim"] = ndim + self.src_items["shape"] = src_shape + src_coord = self.Coord(**self.src_items) + tgt_shape = (1, 9) + tgt_dims = tuple(mapping.values()) + self.resolve.lhs_cube = self.Cube(name=None, shape=tgt_shape) + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = tgt_shape + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims, tgt_dims + ) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + + def test_coord_ndim_equal__shape_unequal_with_unsupported_broadcasting( + self, + ): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 1 9 shape 9 1 + # state c c state c c + # coord x-x coord x-x + # bcast ^ bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + broadcast_shape = (9, 9) + ndim = len(broadcast_shape) + self.resolve.mapping = mapping + self.resolve._broadcast_shape = broadcast_shape + src_shape = (9, 1) + src_dims = tuple(mapping.keys()) + self.resolve.rhs_cube = self.Cube( + name=lambda: sentinel.src_cube, shape=src_shape + ) + self.src_items["ndim"] = ndim + self.src_items["shape"] = src_shape + src_coord = self.Coord(**self.src_items) + tgt_shape = (1, 9) + tgt_dims = tuple(mapping.values()) + self.resolve.lhs_cube = self.Cube( + name=lambda: sentinel.tgt_cube, shape=tgt_shape + ) + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = tgt_shape + tgt_coord = self.Coord(**self.tgt_items) + emsg = "Cannot broadcast" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims, tgt_dims + ) + + def _populate( + self, src_points, tgt_points, src_bounds=None, tgt_bounds=None + ): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state f c state f c + # coord x coord x + # + # src-to-tgt mapping: + # 0->0, 1->1 + shape = (2, 3) + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube = self.Cube( + name=lambda: sentinel.src_cube, shape=None + ) + self.resolve.lhs_cube = self.Cube( + name=lambda: sentinel.tgt_cube, shape=None + ) + ndim = 1 + src_dims = 1 + self.src_items["ndim"] = ndim + self.src_items["shape"] = (shape[src_dims],) + self.src_items["points"] = src_points + self.src_items["bounds"] = src_bounds + self.src_items["has_bounds"] = lambda: src_bounds is not None + src_coord = self.Coord(**self.src_items) + tgt_dims = 1 + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = (shape[mapping[tgt_dims]],) + self.tgt_items["points"] = tgt_points + self.tgt_items["bounds"] = tgt_bounds + self.tgt_items["has_bounds"] = lambda: tgt_bounds is not None + tgt_coord = self.Coord(**self.tgt_items) + args = dict( + src_coord=src_coord, + tgt_coord=tgt_coord, + src_dims=src_dims, + tgt_dims=tgt_dims, + ) + return args + + def test_coord_ndim_and_shape_equal__points_equal_with_no_bounds(self): + args = self._populate(self.src_points, self.src_points) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertIsNone(bounds) + self.assertEqual(1, self.m_array_equal.call_count) + expected = [mock.call(self.src_points, self.src_points, withnans=True)] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_src_bounds_only( + self, + ): + args = self._populate( + self.src_points, self.src_points, src_bounds=self.src_bounds + ) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + self.assertEqual(1, self.m_array_equal.call_count) + expected = [mock.call(self.src_points, self.src_points, withnans=True)] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_tgt_bounds_only( + self, + ): + args = self._populate( + self.src_points, self.src_points, tgt_bounds=self.tgt_bounds + ) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertEqual(self.tgt_bounds, bounds) + self.assertEqual(1, self.m_array_equal.call_count) + expected = [mock.call(self.src_points, self.src_points, withnans=True)] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_src_bounds_only_strict( + self, + ): + args = self._populate( + self.src_points, self.src_points, src_bounds=self.src_bounds + ) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.src_name} has bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_equal_with_tgt_bounds_only_strict( + self, + ): + args = self._populate( + self.src_points, self.src_points, tgt_bounds=self.tgt_bounds + ) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.tgt_name} has bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_equal(self): + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.src_bounds, + ) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + self.assertEqual(2, self.m_array_equal.call_count) + expected = [ + mock.call(self.src_points, self.src_points, withnans=True), + mock.call(self.src_bounds, self.src_bounds, withnans=True), + ] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_different( + self, + ): + self.m_array_equal.side_effect = (True, False) + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.tgt_bounds, + ) + emsg = f"Coordinate {self.src_name} has different bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_different_ignore_mismatch( + self, + ): + self.m_array_equal.side_effect = (True, False) + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.tgt_bounds, + ) + points, bounds = self.resolve._prepare_points_and_bounds( + **args, ignore_mismatch=True + ) + self.assertEqual(self.src_points, points) + self.assertIsNone(bounds) + self.assertEqual(2, self.m_array_equal.call_count) + expected = [ + mock.call(self.src_points, self.src_points, withnans=True), + mock.call(self.src_bounds, self.tgt_bounds, withnans=True), + ] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_different_strict( + self, + ): + self.m_array_equal.side_effect = (True, False) + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.tgt_bounds, + ) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.src_name} has different bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_different(self): + self.m_array_equal.side_effect = (False,) + args = self._populate(self.src_points, self.tgt_points) + emsg = f"Coordinate {self.src_name} has different points" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_different_ignore_mismatch( + self, + ): + self.m_array_equal.side_effect = (False,) + args = self._populate(self.src_points, self.tgt_points) + points, bounds = self.resolve._prepare_points_and_bounds( + **args, ignore_mismatch=True + ) + self.assertIsNone(points) + self.assertIsNone(bounds) + + def test_coord_ndim_and_shape_equal__points_different_strict(self): + self.m_array_equal.side_effect = (False,) + args = self._populate(self.src_points, self.tgt_points) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.src_name} has different points" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + +class Test__create_prepared_item(tests.IrisTest): + def setUp(self): + Coord = namedtuple("Coord", ["points", "bounds"]) + self.points_value = sentinel.points + self.points = mock.Mock(copy=mock.Mock(return_value=self.points_value)) + self.bounds_value = sentinel.bounds + self.bounds = mock.Mock(copy=mock.Mock(return_value=self.bounds_value)) + self.coord = Coord(points=self.points, bounds=self.bounds) + self.container = type(self.coord) + self.combined = sentinel.combined + self.src = mock.Mock(combine=mock.Mock(return_value=self.combined)) + self.tgt = sentinel.tgt + + def _check(self, src=None, tgt=None): + dims = 0 + if src is not None and tgt is not None: + combined = self.combined + else: + combined = src or tgt + result = Resolve._create_prepared_item( + self.coord, dims, src_metadata=src, tgt_metadata=tgt + ) + self.assertIsInstance(result, _PreparedItem) + self.assertIsInstance(result.metadata, _PreparedMetadata) + expected = _PreparedMetadata(combined=combined, src=src, tgt=tgt) + self.assertEqual(expected, result.metadata) + self.assertEqual(self.points_value, result.points) + self.assertEqual(1, self.points.copy.call_count) + self.assertEqual([mock.call()], self.points.copy.call_args_list) + self.assertEqual(self.bounds_value, result.bounds) + self.assertEqual(1, self.bounds.copy.call_count) + self.assertEqual([mock.call()], self.bounds.copy.call_args_list) + self.assertEqual((dims,), result.dims) + self.assertEqual(self.container, result.container) + + def test__no_metadata(self): + self._check() + + def test__src_metadata_only(self): + self._check(src=self.src) + + def test__tgt_metadata_only(self): + self._check(tgt=self.tgt) + + def test__combine_metadata(self): + self._check(src=self.src, tgt=self.tgt) + + +class Test__prepare_local_payload_dim(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["ndim"]) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.resolve.map_rhs_to_lhs = True + self.src_coverage = dict( + cube=None, + metadata=[], + coords=[], + dims_common=None, + dims_local=[], + dims_free=None, + ) + self.tgt_coverage = deepcopy(self.src_coverage) + self.prepared_item = sentinel.prepared_item + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item", + return_value=self.prepared_item, + ) + + def test_src_no_local_with_tgt_no_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_no_local_with_tgt_no_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_local_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + self.src_coverage["dims_local"] = (1,) + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["dims_local"] = (1,) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_local_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + self.src_coverage["dims_local"] = (1,) + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["dims_local"] = (1,) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_local_with_tgt_free(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_dim = 1 + self.src_coverage["dims_local"] = (src_dim,) + src_metadata = sentinel.src_metadata + self.src_coverage["metadata"] = [None, src_metadata] + src_coord = sentinel.src_coord + self.src_coverage["coords"] = [None, src_coord] + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [ + mock.call(src_coord, mapping[src_dim], src_metadata=src_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_free__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_dim = 1 + self.src_coverage["dims_local"] = (src_dim,) + src_metadata = sentinel.src_metadata + self.src_coverage["metadata"] = [None, src_metadata] + src_coord = sentinel.src_coord + self.src_coverage["coords"] = [None, src_coord] + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_free_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord d d coord d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_dim = 1 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [None, tgt_metadata] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [None, tgt_coord] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [mock.call(tgt_coord, tgt_dim, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_free_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord d d coord d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_dim = 1 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [None, tgt_metadata] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [None, tgt_coord] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_no_local_with_tgt_local__extra_dims(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord d d d coord d d + # + # src-to-tgt mapping: + # 0->1, 1->2 + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_dim = 0 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [tgt_metadata, None, None] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [tgt_coord, None, None] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [mock.call(tgt_coord, tgt_dim, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__extra_dims_strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord d d d coord d d + # + # src-to-tgt mapping: + # 0->1, 1->2 + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_dim = 0 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [tgt_metadata, None, None] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [tgt_coord, None, None] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [mock.call(tgt_coord, tgt_dim, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + +class Test__prepare_local_payload_aux(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["ndim"]) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.resolve.map_rhs_to_lhs = True + self.src_coverage = dict( + cube=None, + common_items_aux=None, + common_items_scalar=None, + local_items_aux=[], + local_items_scalar=None, + dims_common=None, + dims_local=[], + dims_free=None, + ) + self.tgt_coverage = deepcopy(self.src_coverage) + self.src_prepared_item = sentinel.src_prepared_item + self.tgt_prepared_item = sentinel.tgt_prepared_item + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item", + side_effect=(self.src_prepared_item, self.tgt_prepared_item), + ) + + def test_src_no_local_with_tgt_no_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_no_local_with_tgt_no_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_local_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(2, len(self.resolve.prepared_category.items_aux)) + expected = [self.src_prepared_item, self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [ + mock.call(src_coord, tgt_dims, src_metadata=src_metadata), + mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata), + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_local_with_tgt_free(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + self.src_coverage["dims_local"].extend(src_dims) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.src_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(src_coord, src_dims, src_metadata=src_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_free__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + self.src_coverage["dims_local"].extend(src_dims) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_free_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord a a coord a + # + # src-to-tgt mapping: + # 0->0, 1->1 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_free_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord a a coord a + # + # src-to-tgt mapping: + # 0->0, 1->1 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_no_local_with_tgt_local__extra_dims(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord a a a coord a a + # + # src-to-tgt mapping: + # 0->1, 1->2 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (0,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__extra_dims_strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord a a a coord a a + # + # src-to-tgt mapping: + # 0->1, 1->2 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (0,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=True): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + +class Test__prepare_local_payload_scalar(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["ndim"]) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.src_coverage = dict( + cube=None, + common_items_aux=None, + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=[], + dims_common=None, + dims_local=[], + dims_free=None, + ) + self.tgt_coverage = deepcopy(self.src_coverage) + self.src_prepared_item = sentinel.src_prepared_item + self.tgt_prepared_item = sentinel.tgt_prepared_item + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item", + side_effect=(self.src_prepared_item, self.tgt_prepared_item), + ) + self.src_dims = () + self.tgt_dims = () + + def test_src_no_local_with_tgt_no_local(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_no_local__strict(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_no_local__src_scalar_cube(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_no_local__src_scalar_cube_strict(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_local_with_tgt_no_local(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_no_local__strict(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_local_with_tgt_no_local__src_scalar_cube(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_no_local__src_scalar_cube_strict(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_local(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__strict(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_local__src_scalar_cube(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__src_scalar_cube_strict(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item, self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata), + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata), + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local__strict(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_local_with_tgt_local__src_scalar_cube(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item, self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata), + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata), + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local__src_scalar_cube_strict(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + +class Test__prepare_local_payload(tests.IrisTest): + def test(self): + src_dim_coverage = sentinel.src_dim_coverage + src_aux_coverage = sentinel.src_aux_coverage + tgt_dim_coverage = sentinel.tgt_dim_coverage + tgt_aux_coverage = sentinel.tgt_aux_coverage + root = "iris.common.resolve.Resolve" + m_prepare_dim = self.patch(f"{root}._prepare_local_payload_dim") + m_prepare_aux = self.patch(f"{root}._prepare_local_payload_aux") + m_prepare_scalar = self.patch(f"{root}._prepare_local_payload_scalar") + resolve = Resolve() + resolve._prepare_local_payload( + src_dim_coverage, + src_aux_coverage, + tgt_dim_coverage, + tgt_aux_coverage, + ) + self.assertEqual(1, m_prepare_dim.call_count) + expected = [mock.call(src_dim_coverage, tgt_dim_coverage)] + self.assertEqual(expected, m_prepare_dim.call_args_list) + self.assertEqual(1, m_prepare_aux.call_count) + expected = [mock.call(src_aux_coverage, tgt_aux_coverage)] + self.assertEqual(expected, m_prepare_aux.call_args_list) + self.assertEqual(1, m_prepare_scalar.call_count) + expected = [mock.call(src_aux_coverage, tgt_aux_coverage)] + self.assertEqual(expected, m_prepare_scalar.call_args_list) + + +class Test__metadata_prepare(tests.IrisTest): + def setUp(self): + self.src_cube = sentinel.src_cube + self.src_category_local = sentinel.src_category_local + self.src_dim_coverage = sentinel.src_dim_coverage + self.src_aux_coverage = mock.Mock( + common_items_aux=sentinel.src_aux_coverage_common_items_aux, + common_items_scalar=sentinel.src_aux_coverage_common_items_scalar, + ) + self.tgt_cube = sentinel.tgt_cube + self.tgt_category_local = sentinel.tgt_category_local + self.tgt_dim_coverage = sentinel.tgt_dim_coverage + self.tgt_aux_coverage = mock.Mock( + common_items_aux=sentinel.tgt_aux_coverage_common_items_aux, + common_items_scalar=sentinel.tgt_aux_coverage_common_items_scalar, + ) + self.resolve = Resolve() + root = "iris.common.resolve.Resolve" + self.m_prepare_common_dim_payload = self.patch( + f"{root}._prepare_common_dim_payload" + ) + self.m_prepare_common_aux_payload = self.patch( + f"{root}._prepare_common_aux_payload" + ) + self.m_prepare_local_payload = self.patch( + f"{root}._prepare_local_payload" + ) + self.m_prepare_factory_payload = self.patch( + f"{root}._prepare_factory_payload" + ) + + def _check(self): + self.assertIsNone(self.resolve.prepared_category) + self.assertIsNone(self.resolve.prepared_factories) + self.resolve._metadata_prepare() + expected = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + self.assertEqual(expected, self.resolve.prepared_category) + self.assertEqual([], self.resolve.prepared_factories) + self.assertEqual(1, self.m_prepare_common_dim_payload.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual( + expected, self.m_prepare_common_dim_payload.call_args_list + ) + self.assertEqual(2, self.m_prepare_common_aux_payload.call_count) + expected = [ + mock.call( + self.src_aux_coverage.common_items_aux, + self.tgt_aux_coverage.common_items_aux, + [], + ), + mock.call( + self.src_aux_coverage.common_items_scalar, + self.tgt_aux_coverage.common_items_scalar, + [], + ignore_mismatch=True, + ), + ] + self.assertEqual( + expected, self.m_prepare_common_aux_payload.call_args_list + ) + self.assertEqual(1, self.m_prepare_local_payload.call_count) + expected = [ + mock.call( + self.src_dim_coverage, + self.src_aux_coverage, + self.tgt_dim_coverage, + self.tgt_aux_coverage, + ) + ] + self.assertEqual(expected, self.m_prepare_local_payload.call_args_list) + self.assertEqual(2, self.m_prepare_factory_payload.call_count) + expected = [ + mock.call(self.tgt_cube, self.tgt_category_local, from_src=False), + mock.call(self.src_cube, self.src_category_local), + ] + self.assertEqual( + expected, self.m_prepare_factory_payload.call_args_list + ) + + def test_map_rhs_to_lhs__true(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube = self.src_cube + self.resolve.rhs_cube_category_local = self.src_category_local + self.resolve.rhs_cube_dim_coverage = self.src_dim_coverage + self.resolve.rhs_cube_aux_coverage = self.src_aux_coverage + self.resolve.lhs_cube = self.tgt_cube + self.resolve.lhs_cube_category_local = self.tgt_category_local + self.resolve.lhs_cube_dim_coverage = self.tgt_dim_coverage + self.resolve.lhs_cube_aux_coverage = self.tgt_aux_coverage + self._check() + + def test_map_rhs_to_lhs__false(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.lhs_cube = self.src_cube + self.resolve.lhs_cube_category_local = self.src_category_local + self.resolve.lhs_cube_dim_coverage = self.src_dim_coverage + self.resolve.lhs_cube_aux_coverage = self.src_aux_coverage + self.resolve.rhs_cube = self.tgt_cube + self.resolve.rhs_cube_category_local = self.tgt_category_local + self.resolve.rhs_cube_dim_coverage = self.tgt_dim_coverage + self.resolve.rhs_cube_aux_coverage = self.tgt_aux_coverage + self._check() + + +class Test__prepare_factory_payload(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["aux_factories"]) + self.Coord = namedtuple("Coord", ["metadata"]) + self.Factory_T1 = namedtuple( + "Factory_T1", ["dependencies"] + ) # dummy factory type + self.container_T1 = type(self.Factory_T1(None)) + self.Factory_T2 = namedtuple( + "Factory_T2", ["dependencies"] + ) # dummy factory type + self.container_T2 = type(self.Factory_T2(None)) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve.prepared_factories = [] + self.m_get_prepared_item = self.patch( + "iris.common.resolve.Resolve._get_prepared_item" + ) + self.category_local = sentinel.category_local + self.from_src = sentinel.from_src + + def test_no_factory(self): + cube = self.Cube(aux_factories=[]) + self.resolve._prepare_factory_payload(cube, self.category_local) + self.assertEqual(0, len(self.resolve.prepared_factories)) + + def test_skip_factory__already_prepared(self): + aux_factory = self.Factory_T1(dependencies=None) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + prepared_factories = [ + _PreparedFactory(container=self.container_T1, dependencies=None), + _PreparedFactory(container=self.container_T2, dependencies=None), + ] + self.resolve.prepared_factories.extend(prepared_factories) + self.resolve._prepare_factory_payload(cube, self.category_local) + self.assertEqual(prepared_factories, self.resolve.prepared_factories) + + def test_factory__dependency_already_prepared(self): + coord_a = self.Coord(metadata=sentinel.coord_a_metadata) + coord_b = self.Coord(metadata=sentinel.coord_b_metadata) + coord_c = self.Coord(metadata=sentinel.coord_c_metadata) + side_effect = (coord_a, coord_b, coord_c) + self.m_get_prepared_item.side_effect = side_effect + dependencies = dict(name_a=coord_a, name_b=coord_b, name_c=coord_c) + aux_factory = self.Factory_T1(dependencies=dependencies) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + self.resolve._prepare_factory_payload( + cube, self.category_local, from_src=self.from_src + ) + self.assertEqual(1, len(self.resolve.prepared_factories)) + prepared_dependencies = { + name: coord.metadata for name, coord in dependencies.items() + } + expected = [ + _PreparedFactory( + container=self.container_T1, dependencies=prepared_dependencies + ) + ] + self.assertEqual(expected, self.resolve.prepared_factories) + self.assertEqual(len(side_effect), self.m_get_prepared_item.call_count) + expected = [ + mock.call( + coord_a.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_b.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_c.metadata, self.category_local, from_src=self.from_src + ), + ] + actual = self.m_get_prepared_item.call_args_list + for call in expected: + self.assertIn(call, actual) + + def test_factory__dependency_local_not_prepared(self): + coord_a = self.Coord(metadata=sentinel.coord_a_metadata) + coord_b = self.Coord(metadata=sentinel.coord_b_metadata) + coord_c = self.Coord(metadata=sentinel.coord_c_metadata) + side_effect = (None, coord_a, None, coord_b, None, coord_c) + self.m_get_prepared_item.side_effect = side_effect + dependencies = dict(name_a=coord_a, name_b=coord_b, name_c=coord_c) + aux_factory = self.Factory_T1(dependencies=dependencies) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + self.resolve._prepare_factory_payload( + cube, self.category_local, from_src=self.from_src + ) + self.assertEqual(1, len(self.resolve.prepared_factories)) + prepared_dependencies = { + name: coord.metadata for name, coord in dependencies.items() + } + expected = [ + _PreparedFactory( + container=self.container_T1, dependencies=prepared_dependencies + ) + ] + self.assertEqual(expected, self.resolve.prepared_factories) + self.assertEqual(len(side_effect), self.m_get_prepared_item.call_count) + expected = [ + mock.call( + coord_a.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_b.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_c.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_a.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_b.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_c.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + ] + actual = self.m_get_prepared_item.call_args_list + for call in expected: + self.assertIn(call, actual) + + def test_factory__dependency_not_found(self): + coord_a = self.Coord(metadata=sentinel.coord_a_metadata) + coord_b = self.Coord(metadata=sentinel.coord_b_metadata) + coord_c = self.Coord(metadata=sentinel.coord_c_metadata) + side_effect = (None, None) + self.m_get_prepared_item.side_effect = side_effect + dependencies = dict(name_a=coord_a, name_b=coord_b, name_c=coord_c) + aux_factory = self.Factory_T1(dependencies=dependencies) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + self.resolve._prepare_factory_payload( + cube, self.category_local, from_src=self.from_src + ) + self.assertEqual(0, len(self.resolve.prepared_factories)) + self.assertEqual(len(side_effect), self.m_get_prepared_item.call_count) + expected = [ + mock.call( + coord_a.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_b.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_c.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_a.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_b.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_c.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + ] + actual = self.m_get_prepared_item.call_args_list + for call in actual: + self.assertIn(call, expected) + + +class Test__get_prepared_item(tests.IrisTest): + def setUp(self): + PreparedItem = namedtuple("PreparedItem", ["metadata"]) + self.resolve = Resolve() + self.prepared_dim_metadata_src = sentinel.prepared_dim_metadata_src + self.prepared_dim_metadata_tgt = sentinel.prepared_dim_metadata_tgt + self.prepared_items_dim = PreparedItem( + metadata=_PreparedMetadata( + combined=None, + src=self.prepared_dim_metadata_src, + tgt=self.prepared_dim_metadata_tgt, + ) + ) + self.prepared_aux_metadata_src = sentinel.prepared_aux_metadata_src + self.prepared_aux_metadata_tgt = sentinel.prepared_aux_metadata_tgt + self.prepared_items_aux = PreparedItem( + metadata=_PreparedMetadata( + combined=None, + src=self.prepared_aux_metadata_src, + tgt=self.prepared_aux_metadata_tgt, + ) + ) + self.prepared_scalar_metadata_src = ( + sentinel.prepared_scalar_metadata_src + ) + self.prepared_scalar_metadata_tgt = ( + sentinel.prepared_scalar_metadata_tgt + ) + self.prepared_items_scalar = PreparedItem( + metadata=_PreparedMetadata( + combined=None, + src=self.prepared_scalar_metadata_src, + tgt=self.prepared_scalar_metadata_tgt, + ) + ) + self.resolve.prepared_category = _CategoryItems( + items_dim=[self.prepared_items_dim], + items_aux=[self.prepared_items_aux], + items_scalar=[self.prepared_items_scalar], + ) + self.resolve.mapping = {0: 10} + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item" + ) + self.local_dim_metadata = sentinel.local_dim_metadata + self.local_aux_metadata = sentinel.local_aux_metadata + self.local_scalar_metadata = sentinel.local_scalar_metadata + self.local_coord = sentinel.local_coord + self.local_coord_dims = (0,) + self.local_items_dim = _Item( + metadata=self.local_dim_metadata, + coord=self.local_coord, + dims=self.local_coord_dims, + ) + self.local_items_aux = _Item( + metadata=self.local_aux_metadata, + coord=self.local_coord, + dims=self.local_coord_dims, + ) + self.local_items_scalar = _Item( + metadata=self.local_scalar_metadata, + coord=self.local_coord, + dims=self.local_coord_dims, + ) + self.category_local = _CategoryItems( + items_dim=[self.local_items_dim], + items_aux=[self.local_items_aux], + items_scalar=[self.local_items_scalar], + ) + + def test_missing_prepared_coord__from_src(self): + metadata = sentinel.missing + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertIsNone(result) + + def test_missing_prepared_coord__from_tgt(self): + metadata = sentinel.missing + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertIsNone(result) + + def test_get_prepared_dim_coord__from_src(self): + metadata = self.prepared_dim_metadata_src + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertEqual(self.prepared_items_dim, result) + + def test_get_prepared_dim_coord__from_tgt(self): + metadata = self.prepared_dim_metadata_tgt + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertEqual(self.prepared_items_dim, result) + + def test_get_prepared_aux_coord__from_src(self): + metadata = self.prepared_aux_metadata_src + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertEqual(self.prepared_items_aux, result) + + def test_get_prepared_aux_coord__from_tgt(self): + metadata = self.prepared_aux_metadata_tgt + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertEqual(self.prepared_items_aux, result) + + def test_get_prepared_scalar_coord__from_src(self): + metadata = self.prepared_scalar_metadata_src + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertEqual(self.prepared_items_scalar, result) + + def test_get_prepared_scalar_coord__from_tgt(self): + metadata = self.prepared_scalar_metadata_tgt + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertEqual(self.prepared_items_scalar, result) + + def test_missing_local_coord__from_src(self): + metadata = sentinel.missing + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + self.assertIsNone(result) + + def test_missing_local_coord__from_tgt(self): + metadata = sentinel.missing + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + self.assertIsNone(result) + + def test_get_local_dim_coord__from_src(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_dim_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_dim)) + self.assertEqual(expected, self.resolve.prepared_category.items_dim[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = (self.resolve.mapping[self.local_coord_dims[0]],) + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=metadata, + tgt_metadata=None, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_dim_coord__from_tgt(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_dim_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_dim)) + self.assertEqual(expected, self.resolve.prepared_category.items_dim[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = self.local_coord_dims + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=None, + tgt_metadata=metadata, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_aux_coord__from_src(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_aux_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_aux)) + self.assertEqual(expected, self.resolve.prepared_category.items_aux[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = (self.resolve.mapping[self.local_coord_dims[0]],) + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=metadata, + tgt_metadata=None, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_aux_coord__from_tgt(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_aux_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_aux)) + self.assertEqual(expected, self.resolve.prepared_category.items_aux[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = self.local_coord_dims + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=None, + tgt_metadata=metadata, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_scalar_coord__from_src(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_scalar_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + self.assertEqual( + expected, self.resolve.prepared_category.items_scalar[1] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = (self.resolve.mapping[self.local_coord_dims[0]],) + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=metadata, + tgt_metadata=None, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_scalar_coord__from_tgt(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_scalar_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + self.assertEqual( + expected, self.resolve.prepared_category.items_scalar[1] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = self.local_coord_dims + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=None, + tgt_metadata=metadata, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + +class Test_cube(tests.IrisTest): + def setUp(self): + self.shape = (2, 3) + self.data = np.zeros(np.multiply(*self.shape), dtype=np.int8).reshape( + self.shape + ) + self.bad_data = np.zeros(np.multiply(*self.shape), dtype=np.int8) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve._broadcast_shape = self.shape + self.cube_metadata = CubeMetadata( + standard_name="air_temperature", + long_name="air temp", + var_name="airT", + units=Unit("K"), + attributes={}, + cell_methods=(), + ) + lhs_cube = Cube(self.data) + lhs_cube.metadata = self.cube_metadata + self.resolve.lhs_cube = lhs_cube + rhs_cube = Cube(self.data) + rhs_cube.metadata = self.cube_metadata + self.resolve.rhs_cube = rhs_cube + self.m_add_dim_coord = self.patch("iris.cube.Cube.add_dim_coord") + self.m_add_aux_coord = self.patch("iris.cube.Cube.add_aux_coord") + self.m_add_aux_factory = self.patch("iris.cube.Cube.add_aux_factory") + self.m_coord = self.patch("iris.cube.Cube.coord") + # + # prepared coordinates + # + prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # prepared dim coordinates + self.prepared_dim_0_metadata = _PreparedMetadata( + combined=sentinel.prepared_dim_0_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_dim_0_points = sentinel.prepared_dim_0_points + self.prepared_dim_0_bounds = sentinel.prepared_dim_0_bounds + self.prepared_dim_0_dims = (0,) + self.prepared_dim_0_coord = mock.Mock(metadata=None) + self.prepared_dim_0_container = mock.Mock( + return_value=self.prepared_dim_0_coord + ) + self.prepared_dim_0 = _PreparedItem( + metadata=self.prepared_dim_0_metadata, + points=self.prepared_dim_0_points, + bounds=self.prepared_dim_0_bounds, + dims=self.prepared_dim_0_dims, + container=self.prepared_dim_0_container, + ) + prepared_category.items_dim.append(self.prepared_dim_0) + self.prepared_dim_1_metadata = _PreparedMetadata( + combined=sentinel.prepared_dim_1_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_dim_1_points = sentinel.prepared_dim_1_points + self.prepared_dim_1_bounds = sentinel.prepared_dim_1_bounds + self.prepared_dim_1_dims = (1,) + self.prepared_dim_1_coord = mock.Mock(metadata=None) + self.prepared_dim_1_container = mock.Mock( + return_value=self.prepared_dim_1_coord + ) + self.prepared_dim_1 = _PreparedItem( + metadata=self.prepared_dim_1_metadata, + points=self.prepared_dim_1_points, + bounds=self.prepared_dim_1_bounds, + dims=self.prepared_dim_1_dims, + container=self.prepared_dim_1_container, + ) + prepared_category.items_dim.append(self.prepared_dim_1) + + # prepared auxiliary coordinates + self.prepared_aux_0_metadata = _PreparedMetadata( + combined=sentinel.prepared_aux_0_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_aux_0_points = sentinel.prepared_aux_0_points + self.prepared_aux_0_bounds = sentinel.prepared_aux_0_bounds + self.prepared_aux_0_dims = (0,) + self.prepared_aux_0_coord = mock.Mock(metadata=None) + self.prepared_aux_0_container = mock.Mock( + return_value=self.prepared_aux_0_coord + ) + self.prepared_aux_0 = _PreparedItem( + metadata=self.prepared_aux_0_metadata, + points=self.prepared_aux_0_points, + bounds=self.prepared_aux_0_bounds, + dims=self.prepared_aux_0_dims, + container=self.prepared_aux_0_container, + ) + prepared_category.items_aux.append(self.prepared_aux_0) + self.prepared_aux_1_metadata = _PreparedMetadata( + combined=sentinel.prepared_aux_1_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_aux_1_points = sentinel.prepared_aux_1_points + self.prepared_aux_1_bounds = sentinel.prepared_aux_1_bounds + self.prepared_aux_1_dims = (1,) + self.prepared_aux_1_coord = mock.Mock(metadata=None) + self.prepared_aux_1_container = mock.Mock( + return_value=self.prepared_aux_1_coord + ) + self.prepared_aux_1 = _PreparedItem( + metadata=self.prepared_aux_1_metadata, + points=self.prepared_aux_1_points, + bounds=self.prepared_aux_1_bounds, + dims=self.prepared_aux_1_dims, + container=self.prepared_aux_1_container, + ) + prepared_category.items_aux.append(self.prepared_aux_1) + + # prepare scalar coordinates + self.prepared_scalar_0_metadata = _PreparedMetadata( + combined=sentinel.prepared_scalar_0_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_scalar_0_points = sentinel.prepared_scalar_0_points + self.prepared_scalar_0_bounds = sentinel.prepared_scalar_0_bounds + self.prepared_scalar_0_dims = () + self.prepared_scalar_0_coord = mock.Mock(metadata=None) + self.prepared_scalar_0_container = mock.Mock( + return_value=self.prepared_scalar_0_coord + ) + self.prepared_scalar_0 = _PreparedItem( + metadata=self.prepared_scalar_0_metadata, + points=self.prepared_scalar_0_points, + bounds=self.prepared_scalar_0_bounds, + dims=self.prepared_scalar_0_dims, + container=self.prepared_scalar_0_container, + ) + prepared_category.items_scalar.append(self.prepared_scalar_0) + self.prepared_scalar_1_metadata = _PreparedMetadata( + combined=sentinel.prepared_scalar_1_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_scalar_1_points = sentinel.prepared_scalar_1_points + self.prepared_scalar_1_bounds = sentinel.prepared_scalar_1_bounds + self.prepared_scalar_1_dims = () + self.prepared_scalar_1_coord = mock.Mock(metadata=None) + self.prepared_scalar_1_container = mock.Mock( + return_value=self.prepared_scalar_1_coord + ) + self.prepared_scalar_1 = _PreparedItem( + metadata=self.prepared_scalar_1_metadata, + points=self.prepared_scalar_1_points, + bounds=self.prepared_scalar_1_bounds, + dims=self.prepared_scalar_1_dims, + container=self.prepared_scalar_1_container, + ) + prepared_category.items_scalar.append(self.prepared_scalar_1) + # + # prepared factories + # + prepared_factories = [] + self.aux_factory = sentinel.aux_factory + self.prepared_factory_container = mock.Mock( + return_value=self.aux_factory + ) + self.prepared_factory_metadata_a = _PreparedMetadata( + combined=sentinel.prepared_factory_metadata_a_combined, + src=None, + tgt=None, + ) + self.prepared_factory_metadata_b = _PreparedMetadata( + combined=sentinel.prepared_factory_metadata_b_combined, + src=None, + tgt=None, + ) + self.prepared_factory_metadata_c = _PreparedMetadata( + combined=sentinel.prepared_factory_metadata_c_combined, + src=None, + tgt=None, + ) + self.prepared_factory_dependencies = dict( + name_a=self.prepared_factory_metadata_a, + name_b=self.prepared_factory_metadata_b, + name_c=self.prepared_factory_metadata_c, + ) + self.prepared_factory = _PreparedFactory( + container=self.prepared_factory_container, + dependencies=self.prepared_factory_dependencies, + ) + prepared_factories.append(self.prepared_factory) + self.prepared_factory_side_effect = ( + sentinel.prepared_factory_coord_a, + sentinel.prepared_factory_coord_b, + sentinel.prepared_factory_coord_c, + ) + self.m_coord.side_effect = self.prepared_factory_side_effect + self.resolve.prepared_category = prepared_category + self.resolve.prepared_factories = prepared_factories + + def test_no_resolved_shape(self): + self.resolve._broadcast_shape = None + data = None + emsg = "Cannot resolve resultant cube, as no candidate cubes have been provided" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve.cube(data) + + def test_bad_data_shape(self): + emsg = "Cannot resolve resultant cube, as the provided data must have shape" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve.cube(self.bad_data) + + def test_bad_data_shape__inplace(self): + self.resolve.lhs_cube = Cube(self.bad_data) + emsg = "Cannot resolve resultant cube in-place" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve.cube(self.data, in_place=True) + + def _check(self): + # check dim coordinate 0 + self.assertEqual(1, self.prepared_dim_0.container.call_count) + expected = [ + mock.call( + self.prepared_dim_0_points, bounds=self.prepared_dim_0_bounds + ) + ] + self.assertEqual( + expected, self.prepared_dim_0.container.call_args_list + ) + self.assertEqual( + self.prepared_dim_0_coord.metadata, + self.prepared_dim_0_metadata.combined, + ) + # check dim coordinate 1 + self.assertEqual(1, self.prepared_dim_1.container.call_count) + expected = [ + mock.call( + self.prepared_dim_1_points, bounds=self.prepared_dim_1_bounds + ) + ] + self.assertEqual( + expected, self.prepared_dim_1.container.call_args_list + ) + self.assertEqual( + self.prepared_dim_1_coord.metadata, + self.prepared_dim_1_metadata.combined, + ) + # check add_dim_coord + self.assertEqual(2, self.m_add_dim_coord.call_count) + expected = [ + mock.call(self.prepared_dim_0_coord, self.prepared_dim_0_dims), + mock.call(self.prepared_dim_1_coord, self.prepared_dim_1_dims), + ] + self.assertEqual(expected, self.m_add_dim_coord.call_args_list) + + # check aux coordinate 0 + self.assertEqual(1, self.prepared_aux_0.container.call_count) + expected = [ + mock.call( + self.prepared_aux_0_points, bounds=self.prepared_aux_0_bounds + ) + ] + self.assertEqual( + expected, self.prepared_aux_0.container.call_args_list + ) + self.assertEqual( + self.prepared_aux_0_coord.metadata, + self.prepared_aux_0_metadata.combined, + ) + # check aux coordinate 1 + self.assertEqual(1, self.prepared_aux_1.container.call_count) + expected = [ + mock.call( + self.prepared_aux_1_points, bounds=self.prepared_aux_1_bounds + ) + ] + self.assertEqual( + expected, self.prepared_aux_1.container.call_args_list + ) + self.assertEqual( + self.prepared_aux_1_coord.metadata, + self.prepared_aux_1_metadata.combined, + ) + # check scalar coordinate 0 + self.assertEqual(1, self.prepared_scalar_0.container.call_count) + expected = [ + mock.call( + self.prepared_scalar_0_points, + bounds=self.prepared_scalar_0_bounds, + ) + ] + self.assertEqual( + expected, self.prepared_scalar_0.container.call_args_list + ) + self.assertEqual( + self.prepared_scalar_0_coord.metadata, + self.prepared_scalar_0_metadata.combined, + ) + # check scalar coordinate 1 + self.assertEqual(1, self.prepared_scalar_1.container.call_count) + expected = [ + mock.call( + self.prepared_scalar_1_points, + bounds=self.prepared_scalar_1_bounds, + ) + ] + self.assertEqual( + expected, self.prepared_scalar_1.container.call_args_list + ) + self.assertEqual( + self.prepared_scalar_1_coord.metadata, + self.prepared_scalar_1_metadata.combined, + ) + # check add_aux_coord + self.assertEqual(4, self.m_add_aux_coord.call_count) + expected = [ + mock.call(self.prepared_aux_0_coord, self.prepared_aux_0_dims), + mock.call(self.prepared_aux_1_coord, self.prepared_aux_1_dims), + mock.call( + self.prepared_scalar_0_coord, self.prepared_scalar_0_dims + ), + mock.call( + self.prepared_scalar_1_coord, self.prepared_scalar_1_dims + ), + ] + self.assertEqual(expected, self.m_add_aux_coord.call_args_list) + + # check auxiliary factories + self.assertEqual(1, self.m_add_aux_factory.call_count) + expected = [mock.call(self.aux_factory)] + self.assertEqual(expected, self.m_add_aux_factory.call_args_list) + self.assertEqual(1, self.prepared_factory_container.call_count) + expected = [ + mock.call( + **{ + name: value + for name, value in zip( + sorted(self.prepared_factory_dependencies.keys()), + self.prepared_factory_side_effect, + ) + } + ) + ] + self.assertEqual( + expected, self.prepared_factory_container.call_args_list + ) + self.assertEqual(3, self.m_coord.call_count) + expected = [ + mock.call(self.prepared_factory_metadata_a.combined), + mock.call(self.prepared_factory_metadata_b.combined), + mock.call(self.prepared_factory_metadata_c.combined), + ] + self.assertEqual(expected, self.m_coord.call_args_list) + + def test_resolve(self): + result = self.resolve.cube(self.data) + self.assertEqual(self.cube_metadata, result.metadata) + self._check() + self.assertIsNot(self.resolve.lhs_cube, result) + + def test_resolve__inplace(self): + result = self.resolve.cube(self.data, in_place=True) + self.assertEqual(self.cube_metadata, result.metadata) + self._check() + self.assertIs(self.resolve.lhs_cube, 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 63553ac821..ded401cab3 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -336,6 +336,108 @@ def test_non_lazy_aggregator(self): self.assertArrayEqual(result.data, np.mean(self.data, axis=1)) +class Test_collapsed__multidim_weighted(tests.IrisTest): + def setUp(self): + self.data = np.arange(6.0).reshape((2, 3)) + self.lazydata = as_lazy_data(self.data) + # Test cubes wth (same-valued) real and lazy data + cube_real = Cube(self.data) + for i_dim, name in enumerate(("y", "x")): + npts = cube_real.shape[i_dim] + coord = DimCoord(np.arange(npts), long_name=name) + cube_real.add_dim_coord(coord, i_dim) + self.cube_real = cube_real + self.cube_lazy = cube_real.copy(data=self.lazydata) + # Test weights and expected result for a y-collapse + self.y_weights = np.array([0.3, 0.5]) + self.full_weights_y = np.broadcast_to( + self.y_weights.reshape((2, 1)), cube_real.shape + ) + self.expected_result_y = np.array([1.875, 2.875, 3.875]) + # Test weights and expected result for an x-collapse + self.x_weights = np.array([0.7, 0.4, 0.6]) + self.full_weights_x = np.broadcast_to( + self.x_weights.reshape((1, 3)), cube_real.shape + ) + self.expected_result_x = np.array([0.941176, 3.941176]) + + def test_weighted_fullweights_real_y(self): + # Supplying full-shape weights for collapsing over a single dimension. + cube_collapsed = self.cube_real.collapsed( + "y", MEAN, weights=self.full_weights_y + ) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_y + ) + + def test_weighted_fullweights_lazy_y(self): + # Full-shape weights, lazy data : Check lazy result, same values as real calc. + cube_collapsed = self.cube_lazy.collapsed( + "y", MEAN, weights=self.full_weights_y + ) + self.assertTrue(cube_collapsed.has_lazy_data()) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_y + ) + + def test_weighted_1dweights_real_y(self): + # 1-D weights, real data : Check same results as full-shape. + cube_collapsed = self.cube_real.collapsed( + "y", MEAN, weights=self.y_weights + ) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_y + ) + + def test_weighted_1dweights_lazy_y(self): + # 1-D weights, lazy data : Check lazy result, same values as real calc. + cube_collapsed = self.cube_lazy.collapsed( + "y", MEAN, weights=self.y_weights + ) + self.assertTrue(cube_collapsed.has_lazy_data()) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_y + ) + + def test_weighted_fullweights_real_x(self): + # Full weights, real data, ** collapse X ** : as for 'y' case above + cube_collapsed = self.cube_real.collapsed( + "x", MEAN, weights=self.full_weights_x + ) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_x + ) + + def test_weighted_fullweights_lazy_x(self): + # Full weights, lazy data, ** collapse X ** : as for 'y' case above + cube_collapsed = self.cube_lazy.collapsed( + "x", MEAN, weights=self.full_weights_x + ) + self.assertTrue(cube_collapsed.has_lazy_data()) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_x + ) + + def test_weighted_1dweights_real_x(self): + # 1-D weights, real data, ** collapse X ** : as for 'y' case above + cube_collapsed = self.cube_real.collapsed( + "x", MEAN, weights=self.x_weights + ) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_x + ) + + def test_weighted_1dweights_lazy_x(self): + # 1-D weights, lazy data, ** collapse X ** : as for 'y' case above + cube_collapsed = self.cube_lazy.collapsed( + "x", MEAN, weights=self.x_weights + ) + self.assertTrue(cube_collapsed.has_lazy_data()) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_x + ) + + class Test_collapsed__cellmeasure_ancils(tests.IrisTest): def setUp(self): cube = Cube(np.arange(6.0).reshape((2, 3))) @@ -484,6 +586,16 @@ def test_ancillary_variable(self): ) self.assertEqual(cube.summary(), expected_summary) + def test_similar_coords(self): + coord1 = AuxCoord( + 42, long_name="foo", attributes=dict(bar=np.array([2, 5])) + ) + coord2 = coord1.copy() + coord2.attributes = dict(bar="baz") + for coord in [coord1, coord2]: + self.cube.add_aux_coord(coord) + self.assertIn("baz", self.cube.summary()) + class Test_is_compatible(tests.IrisTest): def setUp(self): diff --git a/noxfile.py b/noxfile.py index cd97e8ef8b..7bfcc73dd7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -19,7 +19,7 @@ PACKAGE = str("lib" / Path("iris")) #: Cirrus-CI environment variable hook. -PY_VER = os.environ.get("PY_VER", "3.7") +PY_VER = os.environ.get("PY_VER", ["3.6", "3.7"]) #: Default cartopy cache directory. CARTOPY_CACHE_DIR = os.environ.get("HOME") / Path(".local/share/cartopy") @@ -41,7 +41,7 @@ def venv_cached(session): """ result = False - yml = Path(f"requirements/ci/py{PY_VER.replace('.', '')}.yml") + yml = Path(f"requirements/ci/py{session.python.replace('.', '')}.yml") tmp_dir = Path(session.create_tmp()) cache = tmp_dir / yml.name if cache.is_file(): @@ -66,7 +66,7 @@ def cache_venv(session): A `nox.sessions.Session` object. """ - yml = Path(f"requirements/ci/py{PY_VER.replace('.', '')}.yml") + yml = Path(f"requirements/ci/py{session.python.replace('.', '')}.yml") with open(yml, "rb") as fi: hexdigest = hashlib.sha256(fi.read()).hexdigest() tmp_dir = Path(session.create_tmp()) @@ -131,7 +131,7 @@ def black(session): session.run("black", "--check", __file__) -@nox.session(python=[PY_VER], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") def tests(session): """ Perform iris system, integration and unit tests. @@ -150,7 +150,7 @@ def tests(session): """ if not venv_cached(session): # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" # Back-door approach to force nox to use "conda env update". command = ( "conda", @@ -164,7 +164,7 @@ def tests(session): cache_venv(session) cache_cartopy(session) - session.run("python", "setup.py", "develop") + session.install("--no-deps", "--editable", ".") session.run( "python", "-m", @@ -174,7 +174,7 @@ def tests(session): ) -@nox.session(python=[PY_VER], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") def gallery(session): """ Perform iris gallery doc-tests. @@ -193,7 +193,7 @@ def gallery(session): """ if not venv_cached(session): # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" # Back-door approach to force nox to use "conda env update". command = ( "conda", @@ -207,7 +207,7 @@ def gallery(session): cache_venv(session) cache_cartopy(session) - session.run("python", "setup.py", "develop") + session.install("--no-deps", "--editable", ".") session.run( "python", "-m", @@ -216,7 +216,7 @@ def gallery(session): ) -@nox.session(python=[PY_VER], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") def doctest(session): """ Perform iris doc-tests. @@ -235,7 +235,7 @@ def doctest(session): """ if not venv_cached(session): # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" # Back-door approach to force nox to use "conda env update". command = ( "conda", @@ -249,7 +249,7 @@ def doctest(session): cache_venv(session) cache_cartopy(session) - session.run("python", "setup.py", "develop") + session.install("--no-deps", "--editable", ".") session.cd("docs/iris") session.run( "make", @@ -264,7 +264,7 @@ def doctest(session): ) -@nox.session(python=[PY_VER], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") def linkcheck(session): """ Perform iris doc link check. @@ -283,7 +283,7 @@ def linkcheck(session): """ if not venv_cached(session): # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" # Back-door approach to force nox to use "conda env update". command = ( "conda", @@ -297,7 +297,7 @@ def linkcheck(session): cache_venv(session) cache_cartopy(session) - session.run("python", "setup.py", "develop") + session.install("--no-deps", "--editable", ".") session.cd("docs/iris") session.run( "make", diff --git a/requirements/ci/py36.yml b/requirements/ci/py36.yml index 2b40fbad4e..4d9d25d7c6 100644 --- a/requirements/ci/py36.yml +++ b/requirements/ci/py36.yml @@ -44,11 +44,8 @@ dependencies: # Documentation dependencies. - sphinx + - sphinxcontrib-napoleon - sphinx-copybutton - sphinx-gallery + - sphinx-panels - sphinx_rtd_theme - - pip - - pip: - - sphinxcontrib-napoleon - - sphinx-panels - diff --git a/requirements/ci/py37.yml b/requirements/ci/py37.yml index 0f01f0ef75..bdb097796a 100644 --- a/requirements/ci/py37.yml +++ b/requirements/ci/py37.yml @@ -44,10 +44,8 @@ dependencies: # Documentation dependencies. - sphinx + - sphinxcontrib-napoleon - sphinx-copybutton - sphinx-gallery + - sphinx-panels - sphinx_rtd_theme - - pip - - pip: - - sphinxcontrib-napoleon - - sphinx-panels From f0d72f72b66bae6ae9aca668cd7bc7060bb8cd7e Mon Sep 17 00:00:00 2001 From: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> Date: Wed, 27 Jan 2021 12:22:13 +0000 Subject: [PATCH 3/9] Captilise installation heading - align #3958 content with #3940. (#3963) --- docs/iris/src/installing.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/iris/src/installing.rst b/docs/iris/src/installing.rst index cd6a648516..8b3ae8d3e7 100644 --- a/docs/iris/src/installing.rst +++ b/docs/iris/src/installing.rst @@ -43,8 +43,8 @@ at https://conda.io/en/latest/index.html. .. _installing_from_source_without_conda: -Installing from source without conda on Debian-based Linux distros (devs) -------------------------------------------------------------------------- +Installing from Source Without Conda on Debian-Based Linux Distros (Developers) +------------------------------------------------------------------------------- Iris can also be installed without a conda environment. The instructions in this section are valid for Debian-based Linux distributions (Debian, Ubuntu, @@ -55,11 +55,11 @@ These can be installed with apt:: sudo apt-get install python3-pip python3-tk libudunits2-dev libproj-dev proj-bin libgeos-dev libcunit1-dev - + Consider executing:: sudo apt-get update - + before and after installation of Debian packages. The rest can be done with pip. Begin with numpy:: From 40def237ecd23e1175e1586c12ed434221f99120 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Thu, 28 Jan 2021 11:09:52 +0000 Subject: [PATCH 4/9] Merge back v3p0p1 (#3966) * Add release highlights and pin rc version (#3898) * Add release highlights and pin rc version * review actions * reorder release highlights (#3899) Tweak release highlights * Add whatsnew announcement (#3900) * Fix spelling (#3903) * Fix unit label handling (#3902) * Add failing test of plotting * Implement fix to pass test * Update idiff to ignore irrelevant hyphens in path * Update imagerepo (following docs) * Update after review by @trexfeathers * Add whatsnew entries * Move whatsnew entries into correct file * Release Docs Improvements (#3895) * Minor phrasing change in 'Release candidate'. * Before release deprecations. * Whatsnew highlights section. * Relax setup.py setup requirements (#3909) * Updated CF saver version in User Guide and docstring (#3925) * Updated CF saver version in User Guide and docstring * Remove references to CF version of the loader in docstrings * Added whatsnew * Pin cftime<1.3.0 * Migrate to cirrus-ci (#3928) * migrate from travis-ci to cirrus-ci * added whatsnew entries * ignore url for doc link check (#3929) * whatsnew for coord default units (#3924) * Cube._summary_coord_extra: efficiency and bugfix (#3922) * Add Documentation Title Case Capitalization (#3940) * Use Title Case Capitalisation for Documentation * add whatsnew enter * CI requirements drop pip packages (#3939) * requirements pip to conda * use pip install over develop * default PY_VER to python versions * update links (#3942) * update links * added s to http * Add support for 1-d weights in collapse. (#3943) * Remove warning for convert_units on lazy data (#3951) * drop stickler references in docs (#3953) * drop stickler references in docs * remove sticker from common links * update docs for travis-ci to cirrus-ci (#3954) * update docs for travis-ci to cirrus-ci * add 'travis-ci' reference locally to whatsnew * update whatsnew comment * docs for nox (#3955) * docs for nox * add titles, notices and additional detail * review actions * Resolve test coverage (#3947) * test coverage for __init__ and __call__ * test coverage for metadata resolve and coverage * partial test coverage for metadata mapping * python 3.6 workaround for deepcopy of mock.sentinel * test coverage for Resolve._free_mapping * test coverage for Resolve convenience methods * add test stub for Resolve._metadata_mapping * fix Test__tgt_cube_position * test coverage for shape * test coverage for _as_compatible_cubes * test coverage for Resolve._metadata_mapping * test coverage for Resolve._prepare_common_dim_payload * test coverage for Resolve._prepare_common_aux_payload * test coverage for Resolve._prepare_points_and_bounds * test coverage for Resolve._create_prepared_item * test coverage for Resolve._prepare_local_payload_dim * test coverage for Resolve._prepare_local_payload_aux * test coverage for Resolve._prepare_local_payload_scalar + docs URL skip * test coverage for Resolve._prepare_local_payload * test coverage for Resolve._metadata_prepare * added docs URL linkcheck skip * test coverage for Resolve._prepare_factory_payload * test coverage for Resolve._get_prepared_item * review actions * test coverage for Resolve.cube * pin v3.0.0 version and whatnew date (#3956) * update github ci checks image (#3957) * Promote unknown units to dimensionless in aux factories (#3965) * promote unknown to dimensionless units in aux factories * patch aux factories to promote unknown to dimensionless units for formula terms * add whatnew PR for entry Co-authored-by: tkknight <2108488+tkknight@users.noreply.github.com> Co-authored-by: Zeb Nicholls Co-authored-by: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> Co-authored-by: Jon Seddon <17068361+jonseddon@users.noreply.github.com> Co-authored-by: Ruth Comer Co-authored-by: Patrick Peglar --- docs/iris/src/conf.py | 4 +- docs/iris/src/whatsnew/3.0.1.rst | 520 ++++++++++++++++++ docs/iris/src/whatsnew/index.rst | 1 + lib/iris/aux_factory.py | 25 + lib/iris/fileformats/netcdf.py | 3 + .../aux_factory/test_HybridPressureFactory.py | 9 + .../unit/aux_factory/test_OceanSFactory.py | 6 + .../unit/aux_factory/test_OceanSg1Factory.py | 9 + .../unit/aux_factory/test_OceanSg2Factory.py | 9 + .../aux_factory/test_OceanSigmaFactory.py | 6 + .../aux_factory/test_OceanSigmaZFactory.py | 6 + .../netcdf/test__load_aux_factory.py | 41 +- 12 files changed, 635 insertions(+), 4 deletions(-) create mode 100644 docs/iris/src/whatsnew/3.0.1.rst diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index 7232d7c40e..8be849b7ce 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -82,8 +82,8 @@ def autolog(message): if iris.__version__ == "dev": version = "dev" else: - # major.feature(.minor)-dev -> major.minor - version = ".".join(iris.__version__.split("-")[0].split(".")[:2]) + # major.minor.patch-dev -> major.minor.patch + version = ".".join(iris.__version__.split("-")[0].split(".")[:3]) # The full version, including alpha/beta/rc tags. release = iris.__version__ diff --git a/docs/iris/src/whatsnew/3.0.1.rst b/docs/iris/src/whatsnew/3.0.1.rst new file mode 100644 index 0000000000..597e235ebe --- /dev/null +++ b/docs/iris/src/whatsnew/3.0.1.rst @@ -0,0 +1,520 @@ +.. include:: ../common_links.inc + +v3.0.1 (27 Jan 2021) +******************** + +This document explains the changes made to Iris for this release +(:doc:`View all changes `.) + + +.. dropdown:: :opticon:`alert` v3.0.1 Patches + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + :open: + + The patches included in this release include: + + 💼 **Internal** + + * `@bjlittle`_ gracefully promote formula terms within :mod:`~iris.aux_factory` that have ``units`` of ``unknown`` + to ``units`` of ``1`` (dimensionless), where the formula term **must** have dimensionless ``units``. Without this + graceful treatment of ``units`` the resulting :class:`~iris.cube.Cube` will **not** contain the expected auxiliary + factory, and the associated derived coordinate will be missing. (:pull:`3965`) + + +.. dropdown:: :opticon:`report` Release Highlights + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + :open: + + The highlights for this major release of Iris include: + + * We've finally dropped support for ``Python 2``, so welcome to ``Iris 3`` + and ``Python 3``! + * We've extended our coverage of the `CF Conventions and Metadata`_ by + introducing support for `CF Ancillary Data`_ and `Quality Flags`_, + * Lazy regridding is now available for several regridding schemes, + * Managing and manipulating metadata within Iris is now easier and more + consistent thanks to the introduction of a new common metadata API, + * :ref:`Cube arithmetic ` has been significantly improved with + regards to extended broadcasting, auto-transposition and a more lenient + behaviour towards handling metadata and coordinates, + * Our :ref:`documentation ` has been refreshed, + restructured, revitalised and rehosted on `readthedocs`_, + * It's now easier than ever to :ref:`install Iris ` + as a user or a developer, and the newly revamped developers guide walks + you though how you can :ref:`get involved ` + and contribute to Iris, + * Also, this is a major release of Iris, so please be aware of the + :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! + + +📢 Announcements +================ + +* Congratulations to `@bouweandela`_, `@jvegasbsc`_, and `@zklaus`_ who + recently became Iris core developers. They bring a wealth of expertise to the + team, and are using Iris to underpin `ESMValTool`_ - "*A community diagnostic + and performance metrics tool for routine evaluation of Earth system models + in CMIP*". Welcome aboard! 🎉 + +* Congratulations also goes to `@jonseddon`_ who recently became an Iris core + developer. We look forward to seeing more of your awesome contributions! 🎉 + + +✨ Features +=========== + +* `@MoseleyS`_ greatly enhanced the :mod:`~iris.fileformats.nimrod` + module to provide richer meta-data translation when loading ``Nimrod`` data + into cubes. This covers most known operational use-cases. (:pull:`3647`) + +* `@stephenworsley`_ improved the handling of + :class:`iris.coords.CellMeasure`\ s in the :class:`~iris.cube.Cube` + statistical operations :meth:`~iris.cube.Cube.collapsed`, + :meth:`~iris.cube.Cube.aggregated_by` and + :meth:`~iris.cube.Cube.rolling_window`. These previously removed every + :class:`~iris.coords.CellMeasure` attached to the cube. Now, a + :class:`~iris.coords.CellMeasure` will only be removed if it is associated + with an axis over which the statistic is being run. (:pull:`3549`) + +* `@stephenworsley`_, `@pp-mo`_ and `@abooton`_ added support for + `CF Ancillary Data`_ variables. These are created as + :class:`iris.coords.AncillaryVariable`, and appear as components of cubes + much like :class:`~iris.coords.AuxCoord`\ s, with the new + :class:`~iris.cube.Cube` methods + :meth:`~iris.cube.Cube.add_ancillary_variable`, + :meth:`~iris.cube.Cube.remove_ancillary_variable`, + :meth:`~iris.cube.Cube.ancillary_variable`, + :meth:`~iris.cube.Cube.ancillary_variables` and + :meth:`~iris.cube.Cube.ancillary_variable_dims`. + They are loaded from and saved to NetCDF-CF files. Special support for + `Quality Flags`_ is also provided, to ensure they load and save with + appropriate units. (:pull:`3800`) + +* `@bouweandela`_ implemented lazy regridding for the + :class:`~iris.analysis.Linear`, :class:`~iris.analysis.Nearest`, and + :class:`~iris.analysis.AreaWeighted` regridding schemes. (:pull:`3701`) + +* `@bjlittle`_ added `logging`_ support within :mod:`iris.analysis.maths`, + :mod:`iris.common.metadata`, and :mod:`iris.common.resolve`. Each module + defines a :class:`logging.Logger` instance called ``logger`` with a default + ``level`` of ``INFO``. To enable ``DEBUG`` logging use + ``logger.setLevel("DEBUG")``. (:pull:`3785`) + +* `@bjlittle`_ added the :mod:`iris.common.resolve` module, which provides + infrastructure to support the analysis, identification and combination + of metadata common between two :class:`~iris.cube.Cube` operands into a + single resultant :class:`~iris.cube.Cube` that will be auto-transposed, + and with the appropriate broadcast shape. (:pull:`3785`) + +* `@bjlittle`_ added the :ref:`common metadata API `, which provides + a unified treatment of metadata across Iris, and allows users to easily + manage and manipulate their metadata in a consistent way. (:pull:`3785`) + +* `@bjlittle`_ added :ref:`lenient metadata ` support, to + allow users to control **strict** or **lenient** metadata equivalence, + difference and combination. (:pull:`3785`) + +* `@bjlittle`_ added :ref:`lenient cube maths ` support and + resolved several long standing major issues with cube arithmetic regarding + a more robust treatment of cube broadcasting, cube dimension auto-transposition, + and preservation of common metadata and coordinates during cube math operations. + Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) + +* `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when + collapsing over a single dimension. + Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. + The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works + with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) + + +🐛 Bugs Fixed +============= + +* `@stephenworsley`_ fixed :meth:`~iris.cube.Cube.remove_coord` to now also + remove derived coordinates by removing aux_factories. (:pull:`3641`) + +* `@jonseddon`_ fixed ``isinstance(cube, collections.Iterable)`` to now behave + as expected if a :class:`~iris.cube.Cube` is iterated over, while also + ensuring that ``TypeError`` is still raised. (Fixed by setting the + ``__iter__()`` method in :class:`~iris.cube.Cube` to ``None``). + (:pull:`3656`) + +* `@stephenworsley`_ enabled cube concatenation along an axis shared by cell + measures; these cell measures are now concatenated together in the resulting + cube. Such a scenario would previously cause concatenation to inappropriately + fail. (:pull:`3566`) + +* `@stephenworsley`_ newly included :class:`~iris.coords.CellMeasure`\ s in + :class:`~iris.cube.Cube` copy operations. Previously copying a + :class:`~iris.cube.Cube` would ignore any attached + :class:`~iris.coords.CellMeasure`. (:pull:`3546`) + +* `@bjlittle`_ set a :class:`~iris.coords.CellMeasure`'s + ``measure`` attribute to have a default value of ``area``. + Previously, the ``measure`` was provided as a keyword argument to + :class:`~iris.coords.CellMeasure` with a default value of ``None``, which + caused a ``TypeError`` when no ``measure`` was provided, since ``area`` or + ``volume`` are the only accepted values. (:pull:`3533`) + +* `@trexfeathers`_ set **all** plot types in :mod:`iris.plot` to now use + `matplotlib.dates.date2num`_ to format date/time coordinates for use on a plot + axis (previously :meth:`~iris.plot.pcolor` and :meth:`~iris.plot.pcolormesh` + did not include this behaviour). (:pull:`3762`) + +* `@trexfeathers`_ changed date/time axis labels in :mod:`iris.quickplot` to + now **always** be based on the ``epoch`` used in `matplotlib.dates.date2num`_ + (previously would take the unit from a time coordinate, if present, even + though the coordinate's value had been changed via ``date2num``). + (:pull:`3762`) + +* `@pp-mo`_ newly included attributes of cell measures in NETCDF-CF + file loading; they were previously being discarded. They are now available on + the :class:`~iris.coords.CellMeasure` in the loaded :class:`~iris.cube.Cube`. + (:pull:`3800`) + +* `@pp-mo`_ fixed the netcdf loader to now handle any grid-mapping + variables with missing ``false_easting`` and ``false_northing`` properties, + which was previously failing for some coordinate systems. See :issue:`3629`. + (:pull:`3804`) + +* `@stephenworsley`_ changed the way tick labels are assigned from string coords. + Previously, the first tick label would occasionally be duplicated. This also + removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) + +* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check + ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) + +* `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's + coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) + +.. _whatsnew 3.0.1 changes: + +💣 Incompatible Changes +======================= + +* `@pp-mo`_ rationalised :class:`~iris.cube.CubeList` extraction + methods: + + The former method ``iris.cube.CubeList.extract_strict``, and the ``strict`` + keyword of the :meth:`~iris.cube.CubeList.extract` method have been removed, + and are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` + and :meth:`~iris.cube.CubeList.extract_cubes`. + The new routines perform the same operation, but in a style more like other + ``Iris`` functions such as :meth:`~iris.load_cube` and :meth:`~iris.load_cubes`. + Unlike ``strict`` extraction, the type of return value is now completely + consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a + :class:`~iris.cube.Cube`, and :meth:`~iris.cube.CubeList.extract_cubes` + always returns an :class:`iris.cube.CubeList` of a length equal to the + number of constraints. (:pull:`3715`) + +* `@pp-mo`_ removed the former function + ``iris.analysis.coord_comparison``. (:pull:`3562`) + +* `@bjlittle`_ moved the + :func:`iris.experimental.equalise_cubes.equalise_attributes` function from + the :mod:`iris.experimental` module into the :mod:`iris.util` module. Please + use the :func:`iris.util.equalise_attributes` function instead. + (:pull:`3527`) + +* `@bjlittle`_ removed the module ``iris.experimental.concatenate``. In + ``v1.6.0`` the experimental ``concatenate`` functionality was moved to the + :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the + :func:`iris.experimental.concatenate.concatenate` function raised an + exception. (:pull:`3523`) + +* `@stephenworsley`_ changed the default units of :class:`~iris.coords.DimCoord` + and :class:`~iris.coords.AuxCoord` from `"1"` to `"unknown"`. (:pull:`3795`) + +* `@stephenworsley`_ changed Iris objects loaded from NetCDF-CF files to have + ``units='unknown'`` where the corresponding NetCDF variable has no ``units`` + property. Previously these cases defaulted to ``units='1'``. + This affects loading of coordinates whose file variable has no "units" + attribute (not valid, under `CF units rules`_): These will now have units + of `"unknown"`, rather than `"1"`, which **may prevent the creation of + a hybrid vertical coordinate**. While these cases used to "work", this was + never really correct behaviour. (:pull:`3795`) + +* `@SimonPeatman`_ added attribute ``var_name`` to coordinates created by the + :func:`iris.analysis.trajectory.interpolate` function. This prevents + duplicate coordinate errors in certain circumstances. (:pull:`3718`) + +* `@bjlittle`_ aligned the :func:`iris.analysis.maths.apply_ufunc` with the + rest of the :mod:`iris.analysis.maths` API by changing its keyword argument + from ``other_cube`` to ``other``. (:pull:`3785`) + +* `@bjlittle`_ changed the :meth:`iris.analysis.maths.IFunc.__call__` to ignore + any surplus ``other`` keyword argument for a ``data_func`` that requires + **only one** argument. This aligns the behaviour of + :meth:`iris.analysis.maths.IFunc.__call__` with + :func:`~iris.analysis.maths.apply_ufunc`. Previously a ``ValueError`` + exception was raised. (:pull:`3785`) + + +.. _whatsnew 3.0.1 deprecations: + +🔥 Deprecations +=============== + +* `@stephenworsley`_ removed the deprecated :class:`iris.Future` flags + ``cell_date_time_objects``, ``netcdf_promote``, ``netcdf_no_unlimited`` and + ``clip_latitudes``. (:pull:`3459`) + +* `@stephenworsley`_ changed :attr:`iris.fileformats.pp.PPField.lbproc` to be an + ``int``. The deprecated attributes ``flag1``, ``flag2`` etc. have been + removed from it. (:pull:`3461`) + +* `@bjlittle`_ deprecated :func:`~iris.util.as_compatible_shape` in preference + for :class:`~iris.common.resolve.Resolve` e.g., ``Resolve(src, tgt)(tgt.core_data())``. + The :func:`~iris.util.as_compatible_shape` function will be removed in a future + release of Iris. (:pull:`3892`) + + +🔗 Dependencies +=============== + +* `@stephenworsley`_, `@trexfeathers`_ and `@bjlittle`_ removed ``Python2`` + support, modernising the codebase by switching to exclusive ``Python3`` + support. (:pull:`3513`) + +* `@bjlittle`_ improved the developer set up process. Configuring Iris and + :ref:`installing_from_source` as a developer with all the required package + dependencies is now easier with our curated conda environment YAML files. + (:pull:`3812`) + +* `@stephenworsley`_ pinned Iris to require `Dask`_ ``>=2.0``. (:pull:`3460`) + +* `@stephenworsley`_ and `@trexfeathers`_ pinned Iris to require + `Cartopy`_ ``>=0.18``, in order to remain compatible with the latest version + of `Matplotlib`_. (:pull:`3762`) + +* `@bjlittle`_ unpinned Iris to use the latest version of `Matplotlib`_. + Supporting ``Iris`` for both ``Python2`` and ``Python3`` had resulted in + pinning our dependency on `Matplotlib`_ at ``v2.x``. But this is no longer + necessary now that ``Python2`` support has been dropped. (:pull:`3468`) + +* `@stephenworsley`_ and `@trexfeathers`_ unpinned Iris to use the latest version + of `Proj`_. (:pull:`3762`) + +* `@stephenworsley`_ and `@trexfeathers`_ removed GDAL from the extensions + dependency group. We no longer consider it to be an extension. (:pull:`3762`) + + +.. _whatsnew 3.0.1 docs: + +📚 Documentation +================ + +* `@tkknight`_ moved the + :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` + from the general part of the gallery to oceanography. (:pull:`3761`) + +* `@tkknight`_ updated documentation to use a modern sphinx theme and be + served from https://scitools-iris.readthedocs.io/en/latest/. (:pull:`3752`) + +* `@bjlittle`_ added support for the `black`_ code formatter. This is + now automatically checked on GitHub PRs, replacing the older, unittest-based + ``iris.tests.test_coding_standards.TestCodeFormat``. Black provides automatic + code format correction for most IDEs. See the new developer guide section on + :ref:`code_formatting`. (:pull:`3518`) + +* `@tkknight`_ and `@trexfeathers`_ refreshed the :ref:`whats_new_contributions` + for the :ref:`iris_whatsnew`. This includes always creating the ``latest`` + what's new page so it appears on the latest documentation at + https://scitools-iris.readthedocs.io/en/latest/whatsnew. This resolves + :issue:`2104`, :issue:`3451`, :issue:`3818`, :issue:`3837`. Also updated the + :ref:`iris_development_releases_steps` to follow when making a release. + (:pull:`3769`, :pull:`3838`, :pull:`3843`) + +* `@tkknight`_ enabled the PDF creation of the documentation on the + `Read the Docs`_ service. The PDF may be accessed by clicking on the version + at the bottom of the side bar, then selecting ``PDF`` from the ``Downloads`` + section. (:pull:`3765`) + +* `@stephenworsley`_ added a warning to the + :func:`iris.analysis.cartography.project` function regarding its behaviour on + projections with non-rectangular boundaries. (:pull:`3762`) + +* `@stephenworsley`_ added the :ref:`cube_maths_combining_units` section to the + user guide to clarify how ``Units`` are handled during cube arithmetic. + (:pull:`3803`) + +* `@tkknight`_ overhauled the :ref:`developers_guide` including information on + getting involved in becoming a contributor and general structure of the + guide. This resolves :issue:`2170`, :issue:`2331`, :issue:`3453`, + :issue:`314`, :issue:`2902`. (:pull:`3852`) + +* `@rcomer`_ added argument descriptions to the :class:`~iris.coords.DimCoord` + docstring. (:pull:`3681`) + +* `@tkknight`_ added two url's to be ignored for the ``make linkcheck``. This + will ensure the Iris github project is not repeatedly hit during the + linkcheck for issues and pull requests as it can result in connection + refused and thus travis-ci_ job failures. For more information on linkcheck, + see :ref:`contributing.documentation.testing`. (:pull:`3873`) + +* `@tkknight`_ enabled the napolean_ package that is used by sphinx_ to cater + for the existing google style docstrings and to also allow for `numpy`_ + docstrings. This resolves :issue:`3841`. (:pull:`3871`) + +* `@tkknight`_ configured ``sphinx-build`` to promote warnings to errors when + building the documentation via ``make html``. This will minimise technical + debt accruing for the documentation. (:pull:`3877`) + +* `@tkknight`_ updated :ref:`installing_iris` to include a reference to + Windows Subsystem for Linux. (:pull:`3885`) + +* `@tkknight`_ updated the :ref:`iris_docs` homepage to include panels so the + links are more visible to users. This uses the sphinx-panels_ extension. + (:pull:`3884`) + +* `@bjlittle`_ created the :ref:`Further topics ` section and + included documentation for :ref:`metadata`, :ref:`lenient metadata`, and + :ref:`lenient maths`. (:pull:`3890`) + +* `@jonseddon`_ updated the CF version of the netCDF saver in the + :ref:`saving_iris_cubes` section and in the equivalent function docstring. + (:pull:`3925`) + +* `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. + (:pull:`3940`) + + +💼 Internal +=========== + +* `@pp-mo`_ and `@lbdreyer`_ removed all Iris test dependencies on `iris-grib`_ + by transferring all relevant content to the `iris-grib`_ repository. (:pull:`3662`, + :pull:`3663`, :pull:`3664`, :pull:`3665`, :pull:`3666`, :pull:`3669`, + :pull:`3670`, :pull:`3671`, :pull:`3672`, :pull:`3742`, :pull:`3746`) + +* `@lbdreyer`_ and `@pp-mo`_ overhauled the handling of dimensional + metadata to remove duplication. (:pull:`3422`, :pull:`3551`) + +* `@trexfeathers`_ simplified the standard license header for all files, which + removes the need to repeatedly update year numbers in the header. + (:pull:`3489`) + +* `@stephenworsley`_ changed the numerical values in tests involving the + Robinson projection due to improvements made in + `Proj`_. (:pull:`3762`) (see also `Proj#1292`_ and `Proj#2151`_) + +* `@stephenworsley`_ changed tests to account for more detailed descriptions of + projections in `GDAL`_. (:pull:`3762`) (see also `GDAL#1185`_) + +* `@stephenworsley`_ changed tests to account for `GDAL`_ now saving fill values + for data without masked points. (:pull:`3762`) + +* `@trexfeathers`_ changed every graphics test that includes `Cartopy's coastlines`_ + to account for new adaptive coastline scaling. (:pull:`3762`) + (see also `Cartopy#1105`_) + +* `@trexfeathers`_ changed graphics tests to account for some new default + grid-line spacing in `Cartopy`_. (:pull:`3762`) (see also `Cartopy#1117`_) + +* `@trexfeathers`_ added additional acceptable graphics test targets to account + for very minor changes in `Matplotlib`_ version ``3.3`` (colormaps, fonts and + axes borders). (:pull:`3762`) + +* `@rcomer`_ corrected the Matplotlib backend in Iris tests to ignore + `matplotlib.rcdefaults`_, instead the tests will **always** use ``agg``. + (:pull:`3846`) + +* `@bjlittle`_ migrated the `black`_ support from ``19.10b0`` to ``20.8b1``. + (:pull:`3866`) + +* `@lbdreyer`_ updated the CF standard name table to the latest version: `v75`_. + (:pull:`3867`) + +* `@bjlittle`_ added :pep:`517` and :pep:`518` support for building and + installing Iris, in particular to handle the `PyKE`_ package dependency. + (:pull:`3812`) + +* `@bjlittle`_ added metadata support for comparing :attr:`~iris.cube.Cube.attributes` + dictionaries that contain `numpy`_ arrays using `xxHash`_, an extremely fast + non-cryptographic hash algorithm, running at RAM speed limits. + +* `@bjlittle`_ added the ``iris.tests.assertDictEqual`` method to override + :meth:`unittest.TestCase.assertDictEqual` in order to cope with testing + metadata :attr:`~iris.cube.Cube.attributes` dictionary comparison where + the value of a key may be a `numpy`_ array. (:pull:`3785`) + +* `@bjlittle`_ added the :func:`~iris.config.get_logger` function for creating + a generic :class:`logging.Logger` with a :class:`logging.StreamHandler` and + custom :class:`logging.Formatter`. (:pull:`3785`) + +* `@owena11`_ identified and optimised a bottleneck in ``FieldsFile`` header + loading due to the use of :func:`numpy.fromfile`. (:pull:`3791`) + +* `@znicholls`_ added a test for plotting with the label being taken from the unit's symbol, see :meth:`~iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol` (:pull:`3902`). + +* `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). + +* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ support. (:pull:`3928`) + +* `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. + It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to + run the Iris tests, the doc-tests, the gallery doc-tests, and lint Iris + with `flake8`_ and `black`_. (:pull:`3928`) + + +.. _Read the Docs: https://scitools-iris.readthedocs.io/en/latest/ +.. _Matplotlib: https://matplotlib.org/ +.. _CF units rules: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#units +.. _CF Ancillary Data: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#ancillary-data +.. _Quality Flags: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags +.. _iris-grib: https://github.com/SciTools/iris-grib +.. _Cartopy: https://github.com/SciTools/cartopy +.. _Cartopy's coastlines: https://scitools.org.uk/cartopy/docs/latest/matplotlib/geoaxes.html?highlight=coastlines#cartopy.mpl.geoaxes.GeoAxes.coastlines +.. _Cartopy#1105: https://github.com/SciTools/cartopy/pull/1105 +.. _Cartopy#1117: https://github.com/SciTools/cartopy/pull/1117 +.. _Dask: https://github.com/dask/dask +.. _matplotlib.dates.date2num: https://matplotlib.org/api/dates_api.html#matplotlib.dates.date2num +.. _Proj: https://github.com/OSGeo/PROJ +.. _black: https://black.readthedocs.io/en/stable/ +.. _Proj#1292: https://github.com/OSGeo/PROJ/pull/1292 +.. _Proj#2151: https://github.com/OSGeo/PROJ/pull/2151 +.. _GDAL: https://github.com/OSGeo/gdal +.. _GDAL#1185: https://github.com/OSGeo/gdal/pull/1185 +.. _@MoseleyS: https://github.com/MoseleyS +.. _@stephenworsley: https://github.com/stephenworsley +.. _@pp-mo: https://github.com/pp-mo +.. _@abooton: https://github.com/abooton +.. _@bouweandela: https://github.com/bouweandela +.. _@bjlittle: https://github.com/bjlittle +.. _@trexfeathers: https://github.com/trexfeathers +.. _@jonseddon: https://github.com/jonseddon +.. _@tkknight: https://github.com/tkknight +.. _@lbdreyer: https://github.com/lbdreyer +.. _@SimonPeatman: https://github.com/SimonPeatman +.. _@TomekTrzeciak: https://github.com/TomekTrzeciak +.. _@rcomer: https://github.com/rcomer +.. _@jvegasbsc: https://github.com/jvegasbsc +.. _@zklaus: https://github.com/zklaus +.. _@znicholls: https://github.com/znicholls +.. _ESMValTool: https://github.com/ESMValGroup/ESMValTool +.. _v75: https://cfconventions.org/Data/cf-standard-names/75/build/cf-standard-name-table.html +.. _sphinx-panels: https://sphinx-panels.readthedocs.io/en/latest/ +.. _logging: https://docs.python.org/3/library/logging.html +.. _numpy: https://github.com/numpy/numpy +.. _xxHash: https://github.com/Cyan4973/xxHash +.. _PyKE: https://pypi.org/project/scitools-pyke/ +.. _matplotlib.rcdefaults: https://matplotlib.org/3.1.1/api/matplotlib_configuration_api.html?highlight=rcdefaults#matplotlib.rcdefaults +.. _@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/ +.. _nox: https://nox.thea.codes/en/stable/ +.. _Title Case Capitalization: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case +.. _travis-ci: https://travis-ci.org/github/SciTools/iris +.. _stickler-ci: https://stickler-ci.com/ diff --git a/docs/iris/src/whatsnew/index.rst b/docs/iris/src/whatsnew/index.rst index c7fabc0479..257674718a 100644 --- a/docs/iris/src/whatsnew/index.rst +++ b/docs/iris/src/whatsnew/index.rst @@ -11,6 +11,7 @@ Iris versions. :maxdepth: 1 latest.rst + 3.0.1.rst 3.0.rst 2.4.rst 2.3.rst diff --git a/lib/iris/aux_factory.py b/lib/iris/aux_factory.py index 5b63ff53ed..962b46e9e2 100644 --- a/lib/iris/aux_factory.py +++ b/lib/iris/aux_factory.py @@ -11,6 +11,7 @@ from abc import ABCMeta, abstractmethod import warnings +import cf_units import dask.array as da import numpy as np @@ -619,6 +620,10 @@ def _check_dependencies(delta, sigma, surface_air_pressure): warnings.warn(msg, UserWarning, stacklevel=2) # Check units. + if sigma is not None and sigma.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + sigma.units = cf_units.Unit("1") + if sigma is not None and not sigma.units.is_dimensionless(): raise ValueError("Invalid units: sigma must be dimensionless.") if ( @@ -863,6 +868,10 @@ def _check_dependencies(sigma, eta, depth, depth_c, nsigma, zlev): ) raise ValueError(msg) + if sigma is not None and sigma.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + sigma.units = cf_units.Unit("1") + if sigma is not None and not sigma.units.is_dimensionless(): msg = ( "Invalid units: sigma coordinate {!r} " @@ -1127,6 +1136,10 @@ def _check_dependencies(sigma, eta, depth): warnings.warn(msg, UserWarning, stacklevel=2) # Check units. + if sigma is not None and sigma.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + sigma.units = cf_units.Unit("1") + if sigma is not None and not sigma.units.is_dimensionless(): msg = ( "Invalid units: sigma coordinate {!r} " @@ -1335,6 +1348,10 @@ def _check_dependencies(s, c, eta, depth, depth_c): # Check units. coords = ((s, "s"), (c, "c")) for coord, term in coords: + if coord is not None and coord.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + coord.units = cf_units.Unit("1") + if coord is not None and not coord.units.is_dimensionless(): msg = ( "Invalid units: {} coordinate {!r} " @@ -1551,6 +1568,10 @@ def _check_dependencies(s, eta, depth, a, b, depth_c): raise ValueError(msg) # Check units. + if s is not None and s.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + s.units = cf_units.Unit("1") + if s is not None and not s.units.is_dimensionless(): msg = ( "Invalid units: s coordinate {!r} " @@ -1776,6 +1797,10 @@ def _check_dependencies(s, c, eta, depth, depth_c): # Check units. coords = ((s, "s"), (c, "c")) for coord, term in coords: + if coord is not None and coord.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + coord.units = cf_units.Unit("1") + if coord is not None and not coord.units.is_dimensionless(): msg = ( "Invalid units: {} coordinate {!r} " diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 98f712a970..bb7a870d58 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -719,6 +719,9 @@ def coord_from_term(term): warnings.warn(msg) coord_a = coord_from_term("a") if coord_a is not None: + if coord_a.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + coord_a.units = "1" delta = coord_a * coord_p0.points[0] delta.units = coord_a.units * coord_p0.units delta.rename("vertical pressure") diff --git a/lib/iris/tests/unit/aux_factory/test_HybridPressureFactory.py b/lib/iris/tests/unit/aux_factory/test_HybridPressureFactory.py index 14944891f2..32091c7d63 100644 --- a/lib/iris/tests/unit/aux_factory/test_HybridPressureFactory.py +++ b/lib/iris/tests/unit/aux_factory/test_HybridPressureFactory.py @@ -113,6 +113,15 @@ def test_factory_metadata(self): self.assertIsNone(factory.coord_system) self.assertEqual(factory.attributes, {}) + def test_promote_sigma_units_unknown_to_dimensionless(self): + sigma = mock.Mock(units=cf_units.Unit("unknown"), nbounds=0) + factory = HybridPressureFactory( + delta=self.delta, + sigma=sigma, + surface_air_pressure=self.surface_air_pressure, + ) + self.assertEqual("1", factory.dependencies["sigma"].units) + class Test_dependencies(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/aux_factory/test_OceanSFactory.py b/lib/iris/tests/unit/aux_factory/test_OceanSFactory.py index caf9d303c6..6e8e40cd1b 100644 --- a/lib/iris/tests/unit/aux_factory/test_OceanSFactory.py +++ b/lib/iris/tests/unit/aux_factory/test_OceanSFactory.py @@ -137,6 +137,12 @@ def test_depth_incompatible_units(self): with self.assertRaises(ValueError): OceanSFactory(**self.kwargs) + def test_promote_s_units_unknown_to_dimensionless(self): + s = mock.Mock(units=Unit("unknown"), nbounds=0) + self.kwargs["s"] = s + factory = OceanSFactory(**self.kwargs) + self.assertEqual("1", factory.dependencies["s"].units) + class Test_dependencies(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/aux_factory/test_OceanSg1Factory.py b/lib/iris/tests/unit/aux_factory/test_OceanSg1Factory.py index 99a4fe1732..238df2f073 100644 --- a/lib/iris/tests/unit/aux_factory/test_OceanSg1Factory.py +++ b/lib/iris/tests/unit/aux_factory/test_OceanSg1Factory.py @@ -121,6 +121,15 @@ def test_depth_incompatible_units(self): with self.assertRaises(ValueError): OceanSg1Factory(**self.kwargs) + def test_promote_c_and_s_units_unknown_to_dimensionless(self): + c = mock.Mock(units=Unit("unknown"), nbounds=0) + s = mock.Mock(units=Unit("unknown"), nbounds=0) + self.kwargs["c"] = c + self.kwargs["s"] = s + factory = OceanSg1Factory(**self.kwargs) + self.assertEqual("1", factory.dependencies["c"].units) + self.assertEqual("1", factory.dependencies["s"].units) + class Test_dependencies(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/aux_factory/test_OceanSg2Factory.py b/lib/iris/tests/unit/aux_factory/test_OceanSg2Factory.py index 387f0e48d1..fb3ada382e 100644 --- a/lib/iris/tests/unit/aux_factory/test_OceanSg2Factory.py +++ b/lib/iris/tests/unit/aux_factory/test_OceanSg2Factory.py @@ -121,6 +121,15 @@ def test_depth_incompatible_units(self): with self.assertRaises(ValueError): OceanSg2Factory(**self.kwargs) + def test_promote_c_and_s_units_unknown_to_dimensionless(self): + c = mock.Mock(units=Unit("unknown"), nbounds=0) + s = mock.Mock(units=Unit("unknown"), nbounds=0) + self.kwargs["c"] = c + self.kwargs["s"] = s + factory = OceanSg2Factory(**self.kwargs) + self.assertEqual("1", factory.dependencies["c"].units) + self.assertEqual("1", factory.dependencies["s"].units) + class Test_dependencies(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/aux_factory/test_OceanSigmaFactory.py b/lib/iris/tests/unit/aux_factory/test_OceanSigmaFactory.py index 07c970ad7e..69a8a32c6e 100644 --- a/lib/iris/tests/unit/aux_factory/test_OceanSigmaFactory.py +++ b/lib/iris/tests/unit/aux_factory/test_OceanSigmaFactory.py @@ -59,6 +59,12 @@ def test_depth_incompatible_units(self): with self.assertRaises(ValueError): OceanSigmaFactory(**self.kwargs) + def test_promote_sigma_units_unknown_to_dimensionless(self): + sigma = mock.Mock(units=Unit("unknown"), nbounds=0) + self.kwargs["sigma"] = sigma + factory = OceanSigmaFactory(**self.kwargs) + self.assertEqual("1", factory.dependencies["sigma"].units) + class Test_dependencies(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/aux_factory/test_OceanSigmaZFactory.py b/lib/iris/tests/unit/aux_factory/test_OceanSigmaZFactory.py index 6f1e8cd57a..4a4e30b9ca 100644 --- a/lib/iris/tests/unit/aux_factory/test_OceanSigmaZFactory.py +++ b/lib/iris/tests/unit/aux_factory/test_OceanSigmaZFactory.py @@ -138,6 +138,12 @@ def test_depth_incompatible_units(self): with self.assertRaises(ValueError): OceanSigmaZFactory(**self.kwargs) + def test_promote_sigma_units_unknown_to_dimensionless(self): + sigma = mock.Mock(units=Unit("unknown"), nbounds=0) + self.kwargs["sigma"] = sigma + factory = OceanSigmaZFactory(**self.kwargs) + self.assertEqual("1", factory.dependencies["sigma"].units) + class Test_dependencies(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/fileformats/netcdf/test__load_aux_factory.py b/lib/iris/tests/unit/fileformats/netcdf/test__load_aux_factory.py index 48cc9c0d1a..c8f9460e0f 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test__load_aux_factory.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test__load_aux_factory.py @@ -53,8 +53,44 @@ def test_formula_terms_ap(self): self.assertEqual(factory.surface_air_pressure, self.ps) def test_formula_terms_a_p0(self): - coord_a = DimCoord(np.arange(5), units="Pa") - coord_p0 = DimCoord(10, units="1") + coord_a = DimCoord(np.arange(5), units="1") + coord_p0 = DimCoord(10, units="Pa") + coord_expected = DimCoord( + np.arange(5) * 10, + units="Pa", + long_name="vertical pressure", + var_name="ap", + ) + self.cube_parts["coordinates"].extend( + [(coord_a, "a"), (coord_p0, "p0")] + ) + self.requires["formula_terms"] = dict(a="a", b="b", ps="ps", p0="p0") + _load_aux_factory(self.engine, self.cube) + # Check cube.coord_dims method. + self.assertEqual(self.cube.coord_dims.call_count, 1) + args, _ = self.cube.coord_dims.call_args + self.assertEqual(len(args), 1) + self.assertIs(args[0], coord_a) + # Check cube.add_aux_coord method. + self.assertEqual(self.cube.add_aux_coord.call_count, 1) + args, _ = self.cube.add_aux_coord.call_args + self.assertEqual(len(args), 2) + self.assertEqual(args[0], coord_expected) + self.assertIsInstance(args[1], mock.Mock) + # Check cube.add_aux_factory method. + self.assertEqual(self.cube.add_aux_factory.call_count, 1) + args, _ = self.cube.add_aux_factory.call_args + self.assertEqual(len(args), 1) + factory = args[0] + self.assertEqual(factory.delta, coord_expected) + self.assertEqual(factory.sigma, mock.sentinel.b) + self.assertEqual(factory.surface_air_pressure, self.ps) + + def test_formula_terms_a_p0__promote_a_units_unknown_to_dimensionless( + self, + ): + coord_a = DimCoord(np.arange(5), units="unknown") + coord_p0 = DimCoord(10, units="Pa") coord_expected = DimCoord( np.arange(5) * 10, units="Pa", @@ -71,6 +107,7 @@ def test_formula_terms_a_p0(self): args, _ = self.cube.coord_dims.call_args self.assertEqual(len(args), 1) self.assertIs(args[0], coord_a) + self.assertEqual("1", args[0].units) # Check cube.add_aux_coord method. self.assertEqual(self.cube.add_aux_coord.call_count, 1) args, _ = self.cube.add_aux_coord.call_args From c5efe72e37f8b5bef37ddc2170587e592f917c87 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Sun, 31 Jan 2021 16:21:47 +0000 Subject: [PATCH 5/9] Docs whatsnew enumerated lists (#3970) * use enumerated lists in whatsnew * add enumerated list for latest template * enumerate patch dropdown * review actions --- .../documenting/whats_new_contributions.rst | 24 +- docs/iris/src/whatsnew/3.0.1.rst | 652 +++++++++--------- docs/iris/src/whatsnew/3.0.rst | 643 ++++++++--------- docs/iris/src/whatsnew/latest.rst | 35 +- docs/iris/src/whatsnew/latest.rst.template | 16 +- 5 files changed, 687 insertions(+), 683 deletions(-) diff --git a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst index d6f805c511..4bd9021333 100644 --- a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst +++ b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst @@ -59,16 +59,15 @@ what's new document. The appropriate contribution for a pull request might in fact be an addition or change to an existing "What's New" entry. -Each contribution will ideally be written as a single concise bullet point -in a reStructuredText format. Where possible do not exceed **column 80** and -ensure that any subsequent lines of the same bullet point are aligned with the -first. The content should target an Iris user as the audience. The required -content, in order, is as follows: +Each contribution will ideally be written as a single concise entry using a +reStructuredText auto-enumerated list ``#.`` directive. Where possible do not +exceed **column 80** and ensure that any subsequent lines of the same entry are +aligned with the first. The content should target an Iris user as the audience. +The required content, in order, is as follows: * Names of those who contributed the change. These should be their GitHub user name. Link the name to their GitHub profile. E.g. - ```@bjlittle `_ and - `@tkknight `_ changed...`` + ```@tkknight `_ changed...`` * The new/changed behaviour @@ -79,15 +78,14 @@ content, in order, is as follows: * Pull request references, bracketed, following the final period. E.g. ``(:pull:`1111`, :pull:`9999`)`` -* A trailing blank line (standard reStructuredText bullet format) +* A trailing blank line (standard reStructuredText list format) For example:: - * `@bjlittle `_ and - `@tkknight `_ changed changed argument ``x`` - to be optional in :class:`~iris.module.class` and - :meth:`iris.module.method`. This allows greater flexibility as requested in - :issue:`9999`. (:pull:`1111`, :pull:`9999`) + #. `@tkknight `_ changed changed argument ``x`` + to be optional in :class:`~iris.module.class` and + :meth:`iris.module.method`. This allows greater flexibility as requested in + :issue:`9999`. (:pull:`1111`, :pull:`9999`) The above example also demonstrates some of the possible syntax for including diff --git a/docs/iris/src/whatsnew/3.0.1.rst b/docs/iris/src/whatsnew/3.0.1.rst index 597e235ebe..163fe4ff3e 100644 --- a/docs/iris/src/whatsnew/3.0.1.rst +++ b/docs/iris/src/whatsnew/3.0.1.rst @@ -18,10 +18,10 @@ This document explains the changes made to Iris for this release 💼 **Internal** - * `@bjlittle`_ gracefully promote formula terms within :mod:`~iris.aux_factory` that have ``units`` of ``unknown`` - to ``units`` of ``1`` (dimensionless), where the formula term **must** have dimensionless ``units``. Without this - graceful treatment of ``units`` the resulting :class:`~iris.cube.Cube` will **not** contain the expected auxiliary - factory, and the associated derived coordinate will be missing. (:pull:`3965`) + #. `@bjlittle`_ gracefully promote formula terms within :mod:`~iris.aux_factory` that have ``units`` of ``unknown`` + to ``units`` of ``1`` (dimensionless), where the formula term **must** have dimensionless ``units``. Without this + graceful treatment of ``units`` the resulting :class:`~iris.cube.Cube` will **not** contain the expected auxiliary + factory, and the associated derived coordinate will be missing. (:pull:`3965`) .. dropdown:: :opticon:`report` Release Highlights @@ -60,204 +60,204 @@ This document explains the changes made to Iris for this release 📢 Announcements ================ -* Congratulations to `@bouweandela`_, `@jvegasbsc`_, and `@zklaus`_ who - recently became Iris core developers. They bring a wealth of expertise to the - team, and are using Iris to underpin `ESMValTool`_ - "*A community diagnostic - and performance metrics tool for routine evaluation of Earth system models - in CMIP*". Welcome aboard! 🎉 +#. Congratulations to `@bouweandela`_, `@jvegasbsc`_, and `@zklaus`_ who + recently became Iris core developers. They bring a wealth of expertise to the + team, and are using Iris to underpin `ESMValTool`_ - "*A community diagnostic + and performance metrics tool for routine evaluation of Earth system models + in CMIP*". Welcome aboard! 🎉 -* Congratulations also goes to `@jonseddon`_ who recently became an Iris core - developer. We look forward to seeing more of your awesome contributions! 🎉 +#. Congratulations also goes to `@jonseddon`_ who recently became an Iris core + developer. We look forward to seeing more of your awesome contributions! 🎉 ✨ Features =========== -* `@MoseleyS`_ greatly enhanced the :mod:`~iris.fileformats.nimrod` - module to provide richer meta-data translation when loading ``Nimrod`` data - into cubes. This covers most known operational use-cases. (:pull:`3647`) - -* `@stephenworsley`_ improved the handling of - :class:`iris.coords.CellMeasure`\ s in the :class:`~iris.cube.Cube` - statistical operations :meth:`~iris.cube.Cube.collapsed`, - :meth:`~iris.cube.Cube.aggregated_by` and - :meth:`~iris.cube.Cube.rolling_window`. These previously removed every - :class:`~iris.coords.CellMeasure` attached to the cube. Now, a - :class:`~iris.coords.CellMeasure` will only be removed if it is associated - with an axis over which the statistic is being run. (:pull:`3549`) - -* `@stephenworsley`_, `@pp-mo`_ and `@abooton`_ added support for - `CF Ancillary Data`_ variables. These are created as - :class:`iris.coords.AncillaryVariable`, and appear as components of cubes - much like :class:`~iris.coords.AuxCoord`\ s, with the new - :class:`~iris.cube.Cube` methods - :meth:`~iris.cube.Cube.add_ancillary_variable`, - :meth:`~iris.cube.Cube.remove_ancillary_variable`, - :meth:`~iris.cube.Cube.ancillary_variable`, - :meth:`~iris.cube.Cube.ancillary_variables` and - :meth:`~iris.cube.Cube.ancillary_variable_dims`. - They are loaded from and saved to NetCDF-CF files. Special support for - `Quality Flags`_ is also provided, to ensure they load and save with - appropriate units. (:pull:`3800`) - -* `@bouweandela`_ implemented lazy regridding for the - :class:`~iris.analysis.Linear`, :class:`~iris.analysis.Nearest`, and - :class:`~iris.analysis.AreaWeighted` regridding schemes. (:pull:`3701`) - -* `@bjlittle`_ added `logging`_ support within :mod:`iris.analysis.maths`, - :mod:`iris.common.metadata`, and :mod:`iris.common.resolve`. Each module - defines a :class:`logging.Logger` instance called ``logger`` with a default - ``level`` of ``INFO``. To enable ``DEBUG`` logging use - ``logger.setLevel("DEBUG")``. (:pull:`3785`) - -* `@bjlittle`_ added the :mod:`iris.common.resolve` module, which provides - infrastructure to support the analysis, identification and combination - of metadata common between two :class:`~iris.cube.Cube` operands into a - single resultant :class:`~iris.cube.Cube` that will be auto-transposed, - and with the appropriate broadcast shape. (:pull:`3785`) - -* `@bjlittle`_ added the :ref:`common metadata API `, which provides - a unified treatment of metadata across Iris, and allows users to easily - manage and manipulate their metadata in a consistent way. (:pull:`3785`) - -* `@bjlittle`_ added :ref:`lenient metadata ` support, to - allow users to control **strict** or **lenient** metadata equivalence, - difference and combination. (:pull:`3785`) - -* `@bjlittle`_ added :ref:`lenient cube maths ` support and - resolved several long standing major issues with cube arithmetic regarding - a more robust treatment of cube broadcasting, cube dimension auto-transposition, - and preservation of common metadata and coordinates during cube math operations. - Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) - -* `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when - collapsing over a single dimension. - Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. - The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works - with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) +#. `@MoseleyS`_ greatly enhanced the :mod:`~iris.fileformats.nimrod` + module to provide richer meta-data translation when loading ``Nimrod`` data + into cubes. This covers most known operational use-cases. (:pull:`3647`) + +#. `@stephenworsley`_ improved the handling of + :class:`iris.coords.CellMeasure`\ s in the :class:`~iris.cube.Cube` + statistical operations :meth:`~iris.cube.Cube.collapsed`, + :meth:`~iris.cube.Cube.aggregated_by` and + :meth:`~iris.cube.Cube.rolling_window`. These previously removed every + :class:`~iris.coords.CellMeasure` attached to the cube. Now, a + :class:`~iris.coords.CellMeasure` will only be removed if it is associated + with an axis over which the statistic is being run. (:pull:`3549`) + +#. `@stephenworsley`_, `@pp-mo`_ and `@abooton`_ added support for + `CF Ancillary Data`_ variables. These are created as + :class:`iris.coords.AncillaryVariable`, and appear as components of cubes + much like :class:`~iris.coords.AuxCoord`\ s, with the new + :class:`~iris.cube.Cube` methods + :meth:`~iris.cube.Cube.add_ancillary_variable`, + :meth:`~iris.cube.Cube.remove_ancillary_variable`, + :meth:`~iris.cube.Cube.ancillary_variable`, + :meth:`~iris.cube.Cube.ancillary_variables` and + :meth:`~iris.cube.Cube.ancillary_variable_dims`. + They are loaded from and saved to NetCDF-CF files. Special support for + `Quality Flags`_ is also provided, to ensure they load and save with + appropriate units. (:pull:`3800`) + +#. `@bouweandela`_ implemented lazy regridding for the + :class:`~iris.analysis.Linear`, :class:`~iris.analysis.Nearest`, and + :class:`~iris.analysis.AreaWeighted` regridding schemes. (:pull:`3701`) + +#. `@bjlittle`_ added `logging`_ support within :mod:`iris.analysis.maths`, + :mod:`iris.common.metadata`, and :mod:`iris.common.resolve`. Each module + defines a :class:`logging.Logger` instance called ``logger`` with a default + ``level`` of ``INFO``. To enable ``DEBUG`` logging use + ``logger.setLevel("DEBUG")``. (:pull:`3785`) + +#. `@bjlittle`_ added the :mod:`iris.common.resolve` module, which provides + infrastructure to support the analysis, identification and combination + of metadata common between two :class:`~iris.cube.Cube` operands into a + single resultant :class:`~iris.cube.Cube` that will be auto-transposed, + and with the appropriate broadcast shape. (:pull:`3785`) + +#. `@bjlittle`_ added the :ref:`common metadata API `, which provides + a unified treatment of metadata across Iris, and allows users to easily + manage and manipulate their metadata in a consistent way. (:pull:`3785`) + +#. `@bjlittle`_ added :ref:`lenient metadata ` support, to + allow users to control **strict** or **lenient** metadata equivalence, + difference and combination. (:pull:`3785`) + +#. `@bjlittle`_ added :ref:`lenient cube maths ` support and + resolved several long standing major issues with cube arithmetic regarding + a more robust treatment of cube broadcasting, cube dimension auto-transposition, + and preservation of common metadata and coordinates during cube math operations. + Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) + +#. `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when + collapsing over a single dimension. + Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. + The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works + with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) 🐛 Bugs Fixed ============= -* `@stephenworsley`_ fixed :meth:`~iris.cube.Cube.remove_coord` to now also - remove derived coordinates by removing aux_factories. (:pull:`3641`) - -* `@jonseddon`_ fixed ``isinstance(cube, collections.Iterable)`` to now behave - as expected if a :class:`~iris.cube.Cube` is iterated over, while also - ensuring that ``TypeError`` is still raised. (Fixed by setting the - ``__iter__()`` method in :class:`~iris.cube.Cube` to ``None``). - (:pull:`3656`) - -* `@stephenworsley`_ enabled cube concatenation along an axis shared by cell - measures; these cell measures are now concatenated together in the resulting - cube. Such a scenario would previously cause concatenation to inappropriately - fail. (:pull:`3566`) - -* `@stephenworsley`_ newly included :class:`~iris.coords.CellMeasure`\ s in - :class:`~iris.cube.Cube` copy operations. Previously copying a - :class:`~iris.cube.Cube` would ignore any attached - :class:`~iris.coords.CellMeasure`. (:pull:`3546`) - -* `@bjlittle`_ set a :class:`~iris.coords.CellMeasure`'s - ``measure`` attribute to have a default value of ``area``. - Previously, the ``measure`` was provided as a keyword argument to - :class:`~iris.coords.CellMeasure` with a default value of ``None``, which - caused a ``TypeError`` when no ``measure`` was provided, since ``area`` or - ``volume`` are the only accepted values. (:pull:`3533`) - -* `@trexfeathers`_ set **all** plot types in :mod:`iris.plot` to now use - `matplotlib.dates.date2num`_ to format date/time coordinates for use on a plot - axis (previously :meth:`~iris.plot.pcolor` and :meth:`~iris.plot.pcolormesh` - did not include this behaviour). (:pull:`3762`) - -* `@trexfeathers`_ changed date/time axis labels in :mod:`iris.quickplot` to - now **always** be based on the ``epoch`` used in `matplotlib.dates.date2num`_ - (previously would take the unit from a time coordinate, if present, even - though the coordinate's value had been changed via ``date2num``). - (:pull:`3762`) - -* `@pp-mo`_ newly included attributes of cell measures in NETCDF-CF - file loading; they were previously being discarded. They are now available on - the :class:`~iris.coords.CellMeasure` in the loaded :class:`~iris.cube.Cube`. - (:pull:`3800`) - -* `@pp-mo`_ fixed the netcdf loader to now handle any grid-mapping - variables with missing ``false_easting`` and ``false_northing`` properties, - which was previously failing for some coordinate systems. See :issue:`3629`. - (:pull:`3804`) - -* `@stephenworsley`_ changed the way tick labels are assigned from string coords. - Previously, the first tick label would occasionally be duplicated. This also - removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) - -* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check - ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) - -* `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's - coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) +#. `@stephenworsley`_ fixed :meth:`~iris.cube.Cube.remove_coord` to now also + remove derived coordinates by removing aux_factories. (:pull:`3641`) + +#. `@jonseddon`_ fixed ``isinstance(cube, collections.Iterable)`` to now behave + as expected if a :class:`~iris.cube.Cube` is iterated over, while also + ensuring that ``TypeError`` is still raised. (Fixed by setting the + ``__iter__()`` method in :class:`~iris.cube.Cube` to ``None``). + (:pull:`3656`) + +#. `@stephenworsley`_ enabled cube concatenation along an axis shared by cell + measures; these cell measures are now concatenated together in the resulting + cube. Such a scenario would previously cause concatenation to inappropriately + fail. (:pull:`3566`) + +#. `@stephenworsley`_ newly included :class:`~iris.coords.CellMeasure`\ s in + :class:`~iris.cube.Cube` copy operations. Previously copying a + :class:`~iris.cube.Cube` would ignore any attached + :class:`~iris.coords.CellMeasure`. (:pull:`3546`) + +#. `@bjlittle`_ set a :class:`~iris.coords.CellMeasure`'s + ``measure`` attribute to have a default value of ``area``. + Previously, the ``measure`` was provided as a keyword argument to + :class:`~iris.coords.CellMeasure` with a default value of ``None``, which + caused a ``TypeError`` when no ``measure`` was provided, since ``area`` or + ``volume`` are the only accepted values. (:pull:`3533`) + +#. `@trexfeathers`_ set **all** plot types in :mod:`iris.plot` to now use + `matplotlib.dates.date2num`_ to format date/time coordinates for use on a plot + axis (previously :meth:`~iris.plot.pcolor` and :meth:`~iris.plot.pcolormesh` + did not include this behaviour). (:pull:`3762`) + +#. `@trexfeathers`_ changed date/time axis labels in :mod:`iris.quickplot` to + now **always** be based on the ``epoch`` used in `matplotlib.dates.date2num`_ + (previously would take the unit from a time coordinate, if present, even + though the coordinate's value had been changed via ``date2num``). + (:pull:`3762`) + +#. `@pp-mo`_ newly included attributes of cell measures in NETCDF-CF + file loading; they were previously being discarded. They are now available on + the :class:`~iris.coords.CellMeasure` in the loaded :class:`~iris.cube.Cube`. + (:pull:`3800`) + +#. `@pp-mo`_ fixed the netcdf loader to now handle any grid-mapping + variables with missing ``false_easting`` and ``false_northing`` properties, + which was previously failing for some coordinate systems. See :issue:`3629`. + (:pull:`3804`) + +#. `@stephenworsley`_ changed the way tick labels are assigned from string coords. + Previously, the first tick label would occasionally be duplicated. This also + removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) + +#. `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check + ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) + +#. `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's + coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) .. _whatsnew 3.0.1 changes: 💣 Incompatible Changes ======================= -* `@pp-mo`_ rationalised :class:`~iris.cube.CubeList` extraction - methods: - - The former method ``iris.cube.CubeList.extract_strict``, and the ``strict`` - keyword of the :meth:`~iris.cube.CubeList.extract` method have been removed, - and are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` - and :meth:`~iris.cube.CubeList.extract_cubes`. - The new routines perform the same operation, but in a style more like other - ``Iris`` functions such as :meth:`~iris.load_cube` and :meth:`~iris.load_cubes`. - Unlike ``strict`` extraction, the type of return value is now completely - consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a - :class:`~iris.cube.Cube`, and :meth:`~iris.cube.CubeList.extract_cubes` - always returns an :class:`iris.cube.CubeList` of a length equal to the - number of constraints. (:pull:`3715`) - -* `@pp-mo`_ removed the former function - ``iris.analysis.coord_comparison``. (:pull:`3562`) - -* `@bjlittle`_ moved the - :func:`iris.experimental.equalise_cubes.equalise_attributes` function from - the :mod:`iris.experimental` module into the :mod:`iris.util` module. Please - use the :func:`iris.util.equalise_attributes` function instead. - (:pull:`3527`) - -* `@bjlittle`_ removed the module ``iris.experimental.concatenate``. In - ``v1.6.0`` the experimental ``concatenate`` functionality was moved to the - :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the - :func:`iris.experimental.concatenate.concatenate` function raised an - exception. (:pull:`3523`) - -* `@stephenworsley`_ changed the default units of :class:`~iris.coords.DimCoord` - and :class:`~iris.coords.AuxCoord` from `"1"` to `"unknown"`. (:pull:`3795`) - -* `@stephenworsley`_ changed Iris objects loaded from NetCDF-CF files to have - ``units='unknown'`` where the corresponding NetCDF variable has no ``units`` - property. Previously these cases defaulted to ``units='1'``. - This affects loading of coordinates whose file variable has no "units" - attribute (not valid, under `CF units rules`_): These will now have units - of `"unknown"`, rather than `"1"`, which **may prevent the creation of - a hybrid vertical coordinate**. While these cases used to "work", this was - never really correct behaviour. (:pull:`3795`) - -* `@SimonPeatman`_ added attribute ``var_name`` to coordinates created by the - :func:`iris.analysis.trajectory.interpolate` function. This prevents - duplicate coordinate errors in certain circumstances. (:pull:`3718`) - -* `@bjlittle`_ aligned the :func:`iris.analysis.maths.apply_ufunc` with the - rest of the :mod:`iris.analysis.maths` API by changing its keyword argument - from ``other_cube`` to ``other``. (:pull:`3785`) - -* `@bjlittle`_ changed the :meth:`iris.analysis.maths.IFunc.__call__` to ignore - any surplus ``other`` keyword argument for a ``data_func`` that requires - **only one** argument. This aligns the behaviour of - :meth:`iris.analysis.maths.IFunc.__call__` with - :func:`~iris.analysis.maths.apply_ufunc`. Previously a ``ValueError`` - exception was raised. (:pull:`3785`) +#. `@pp-mo`_ rationalised :class:`~iris.cube.CubeList` extraction + methods: + + The former method ``iris.cube.CubeList.extract_strict``, and the ``strict`` + keyword of the :meth:`~iris.cube.CubeList.extract` method have been removed, + and are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` + and :meth:`~iris.cube.CubeList.extract_cubes`. + The new routines perform the same operation, but in a style more like other + ``Iris`` functions such as :meth:`~iris.load_cube` and :meth:`~iris.load_cubes`. + Unlike ``strict`` extraction, the type of return value is now completely + consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a + :class:`~iris.cube.Cube`, and :meth:`~iris.cube.CubeList.extract_cubes` + always returns an :class:`iris.cube.CubeList` of a length equal to the + number of constraints. (:pull:`3715`) + +#. `@pp-mo`_ removed the former function + ``iris.analysis.coord_comparison``. (:pull:`3562`) + +#. `@bjlittle`_ moved the + :func:`iris.experimental.equalise_cubes.equalise_attributes` function from + the :mod:`iris.experimental` module into the :mod:`iris.util` module. Please + use the :func:`iris.util.equalise_attributes` function instead. + (:pull:`3527`) + +#. `@bjlittle`_ removed the module ``iris.experimental.concatenate``. In + ``v1.6.0`` the experimental ``concatenate`` functionality was moved to the + :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the + :func:`iris.experimental.concatenate.concatenate` function raised an + exception. (:pull:`3523`) + +#. `@stephenworsley`_ changed the default units of :class:`~iris.coords.DimCoord` + and :class:`~iris.coords.AuxCoord` from `"1"` to `"unknown"`. (:pull:`3795`) + +#. `@stephenworsley`_ changed Iris objects loaded from NetCDF-CF files to have + ``units='unknown'`` where the corresponding NetCDF variable has no ``units`` + property. Previously these cases defaulted to ``units='1'``. + This affects loading of coordinates whose file variable has no "units" + attribute (not valid, under `CF units rules`_): These will now have units + of `"unknown"`, rather than `"1"`, which **may prevent the creation of + a hybrid vertical coordinate**. While these cases used to "work", this was + never really correct behaviour. (:pull:`3795`) + +#. `@SimonPeatman`_ added attribute ``var_name`` to coordinates created by the + :func:`iris.analysis.trajectory.interpolate` function. This prevents + duplicate coordinate errors in certain circumstances. (:pull:`3718`) + +#. `@bjlittle`_ aligned the :func:`iris.analysis.maths.apply_ufunc` with the + rest of the :mod:`iris.analysis.maths` API by changing its keyword argument + from ``other_cube`` to ``other``. (:pull:`3785`) + +#. `@bjlittle`_ changed the :meth:`iris.analysis.maths.IFunc.__call__` to ignore + any surplus ``other`` keyword argument for a ``data_func`` that requires + **only one** argument. This aligns the behaviour of + :meth:`iris.analysis.maths.IFunc.__call__` with + :func:`~iris.analysis.maths.apply_ufunc`. Previously a ``ValueError`` + exception was raised. (:pull:`3785`) .. _whatsnew 3.0.1 deprecations: @@ -265,48 +265,48 @@ This document explains the changes made to Iris for this release 🔥 Deprecations =============== -* `@stephenworsley`_ removed the deprecated :class:`iris.Future` flags - ``cell_date_time_objects``, ``netcdf_promote``, ``netcdf_no_unlimited`` and - ``clip_latitudes``. (:pull:`3459`) +#. `@stephenworsley`_ removed the deprecated :class:`iris.Future` flags + ``cell_date_time_objects``, ``netcdf_promote``, ``netcdf_no_unlimited`` and + ``clip_latitudes``. (:pull:`3459`) -* `@stephenworsley`_ changed :attr:`iris.fileformats.pp.PPField.lbproc` to be an - ``int``. The deprecated attributes ``flag1``, ``flag2`` etc. have been - removed from it. (:pull:`3461`) +#. `@stephenworsley`_ changed :attr:`iris.fileformats.pp.PPField.lbproc` to be an + ``int``. The deprecated attributes ``flag1``, ``flag2`` etc. have been + removed from it. (:pull:`3461`) -* `@bjlittle`_ deprecated :func:`~iris.util.as_compatible_shape` in preference - for :class:`~iris.common.resolve.Resolve` e.g., ``Resolve(src, tgt)(tgt.core_data())``. - The :func:`~iris.util.as_compatible_shape` function will be removed in a future - release of Iris. (:pull:`3892`) +#. `@bjlittle`_ deprecated :func:`~iris.util.as_compatible_shape` in preference + for :class:`~iris.common.resolve.Resolve` e.g., ``Resolve(src, tgt)(tgt.core_data())``. + The :func:`~iris.util.as_compatible_shape` function will be removed in a future + release of Iris. (:pull:`3892`) 🔗 Dependencies =============== -* `@stephenworsley`_, `@trexfeathers`_ and `@bjlittle`_ removed ``Python2`` - support, modernising the codebase by switching to exclusive ``Python3`` - support. (:pull:`3513`) +#. `@stephenworsley`_, `@trexfeathers`_ and `@bjlittle`_ removed ``Python2`` + support, modernising the codebase by switching to exclusive ``Python3`` + support. (:pull:`3513`) -* `@bjlittle`_ improved the developer set up process. Configuring Iris and - :ref:`installing_from_source` as a developer with all the required package - dependencies is now easier with our curated conda environment YAML files. - (:pull:`3812`) +#. `@bjlittle`_ improved the developer set up process. Configuring Iris and + :ref:`installing_from_source` as a developer with all the required package + dependencies is now easier with our curated conda environment YAML files. + (:pull:`3812`) -* `@stephenworsley`_ pinned Iris to require `Dask`_ ``>=2.0``. (:pull:`3460`) +#. `@stephenworsley`_ pinned Iris to require `Dask`_ ``>=2.0``. (:pull:`3460`) -* `@stephenworsley`_ and `@trexfeathers`_ pinned Iris to require - `Cartopy`_ ``>=0.18``, in order to remain compatible with the latest version - of `Matplotlib`_. (:pull:`3762`) +#. `@stephenworsley`_ and `@trexfeathers`_ pinned Iris to require + `Cartopy`_ ``>=0.18``, in order to remain compatible with the latest version + of `Matplotlib`_. (:pull:`3762`) -* `@bjlittle`_ unpinned Iris to use the latest version of `Matplotlib`_. - Supporting ``Iris`` for both ``Python2`` and ``Python3`` had resulted in - pinning our dependency on `Matplotlib`_ at ``v2.x``. But this is no longer - necessary now that ``Python2`` support has been dropped. (:pull:`3468`) +#. `@bjlittle`_ unpinned Iris to use the latest version of `Matplotlib`_. + Supporting ``Iris`` for both ``Python2`` and ``Python3`` had resulted in + pinning our dependency on `Matplotlib`_ at ``v2.x``. But this is no longer + necessary now that ``Python2`` support has been dropped. (:pull:`3468`) -* `@stephenworsley`_ and `@trexfeathers`_ unpinned Iris to use the latest version - of `Proj`_. (:pull:`3762`) +#. `@stephenworsley`_ and `@trexfeathers`_ unpinned Iris to use the latest version + of `Proj`_. (:pull:`3762`) -* `@stephenworsley`_ and `@trexfeathers`_ removed GDAL from the extensions - dependency group. We no longer consider it to be an extension. (:pull:`3762`) +#. `@stephenworsley`_ and `@trexfeathers`_ removed GDAL from the extensions + dependency group. We no longer consider it to be an extension. (:pull:`3762`) .. _whatsnew 3.0.1 docs: @@ -314,158 +314,160 @@ This document explains the changes made to Iris for this release 📚 Documentation ================ -* `@tkknight`_ moved the - :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` - from the general part of the gallery to oceanography. (:pull:`3761`) +#. `@tkknight`_ moved the + :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` + from the general part of the gallery to oceanography. (:pull:`3761`) -* `@tkknight`_ updated documentation to use a modern sphinx theme and be - served from https://scitools-iris.readthedocs.io/en/latest/. (:pull:`3752`) +#. `@tkknight`_ updated documentation to use a modern sphinx theme and be + served from https://scitools-iris.readthedocs.io/en/latest/. (:pull:`3752`) -* `@bjlittle`_ added support for the `black`_ code formatter. This is - now automatically checked on GitHub PRs, replacing the older, unittest-based - ``iris.tests.test_coding_standards.TestCodeFormat``. Black provides automatic - code format correction for most IDEs. See the new developer guide section on - :ref:`code_formatting`. (:pull:`3518`) +#. `@bjlittle`_ added support for the `black`_ code formatter. This is + now automatically checked on GitHub PRs, replacing the older, unittest-based + ``iris.tests.test_coding_standards.TestCodeFormat``. Black provides automatic + code format correction for most IDEs. See the new developer guide section on + :ref:`code_formatting`. (:pull:`3518`) -* `@tkknight`_ and `@trexfeathers`_ refreshed the :ref:`whats_new_contributions` - for the :ref:`iris_whatsnew`. This includes always creating the ``latest`` - what's new page so it appears on the latest documentation at - https://scitools-iris.readthedocs.io/en/latest/whatsnew. This resolves - :issue:`2104`, :issue:`3451`, :issue:`3818`, :issue:`3837`. Also updated the - :ref:`iris_development_releases_steps` to follow when making a release. - (:pull:`3769`, :pull:`3838`, :pull:`3843`) +#. `@tkknight`_ and `@trexfeathers`_ refreshed the :ref:`whats_new_contributions` + for the :ref:`iris_whatsnew`. This includes always creating the ``latest`` + what's new page so it appears on the latest documentation at + https://scitools-iris.readthedocs.io/en/latest/whatsnew. This resolves + :issue:`2104`, :issue:`3451`, :issue:`3818`, :issue:`3837`. Also updated the + :ref:`iris_development_releases_steps` to follow when making a release. + (:pull:`3769`, :pull:`3838`, :pull:`3843`) -* `@tkknight`_ enabled the PDF creation of the documentation on the - `Read the Docs`_ service. The PDF may be accessed by clicking on the version - at the bottom of the side bar, then selecting ``PDF`` from the ``Downloads`` - section. (:pull:`3765`) +#. `@tkknight`_ enabled the PDF creation of the documentation on the + `Read the Docs`_ service. The PDF may be accessed by clicking on the version + at the bottom of the side bar, then selecting ``PDF`` from the ``Downloads`` + section. (:pull:`3765`) -* `@stephenworsley`_ added a warning to the - :func:`iris.analysis.cartography.project` function regarding its behaviour on - projections with non-rectangular boundaries. (:pull:`3762`) +#. `@stephenworsley`_ added a warning to the + :func:`iris.analysis.cartography.project` function regarding its behaviour on + projections with non-rectangular boundaries. (:pull:`3762`) -* `@stephenworsley`_ added the :ref:`cube_maths_combining_units` section to the - user guide to clarify how ``Units`` are handled during cube arithmetic. - (:pull:`3803`) +#. `@stephenworsley`_ added the :ref:`cube_maths_combining_units` section to the + user guide to clarify how ``Units`` are handled during cube arithmetic. + (:pull:`3803`) -* `@tkknight`_ overhauled the :ref:`developers_guide` including information on - getting involved in becoming a contributor and general structure of the - guide. This resolves :issue:`2170`, :issue:`2331`, :issue:`3453`, - :issue:`314`, :issue:`2902`. (:pull:`3852`) +#. `@tkknight`_ overhauled the :ref:`developers_guide` including information on + getting involved in becoming a contributor and general structure of the + guide. This resolves :issue:`2170`, :issue:`2331`, :issue:`3453`, + :issue:`314`, :issue:`2902`. (:pull:`3852`) -* `@rcomer`_ added argument descriptions to the :class:`~iris.coords.DimCoord` - docstring. (:pull:`3681`) +#. `@rcomer`_ added argument descriptions to the :class:`~iris.coords.DimCoord` + docstring. (:pull:`3681`) -* `@tkknight`_ added two url's to be ignored for the ``make linkcheck``. This - will ensure the Iris github project is not repeatedly hit during the - linkcheck for issues and pull requests as it can result in connection - refused and thus travis-ci_ job failures. For more information on linkcheck, - see :ref:`contributing.documentation.testing`. (:pull:`3873`) +#. `@tkknight`_ added two url's to be ignored for the ``make linkcheck``. This + will ensure the Iris github project is not repeatedly hit during the + linkcheck for issues and pull requests as it can result in connection + refused and thus travis-ci_ job failures. For more information on linkcheck, + see :ref:`contributing.documentation.testing`. (:pull:`3873`) -* `@tkknight`_ enabled the napolean_ package that is used by sphinx_ to cater - for the existing google style docstrings and to also allow for `numpy`_ - docstrings. This resolves :issue:`3841`. (:pull:`3871`) +#. `@tkknight`_ enabled the napolean_ package that is used by sphinx_ to cater + for the existing google style docstrings and to also allow for `numpy`_ + docstrings. This resolves :issue:`3841`. (:pull:`3871`) -* `@tkknight`_ configured ``sphinx-build`` to promote warnings to errors when - building the documentation via ``make html``. This will minimise technical - debt accruing for the documentation. (:pull:`3877`) +#. `@tkknight`_ configured ``sphinx-build`` to promote warnings to errors when + building the documentation via ``make html``. This will minimise technical + debt accruing for the documentation. (:pull:`3877`) -* `@tkknight`_ updated :ref:`installing_iris` to include a reference to - Windows Subsystem for Linux. (:pull:`3885`) +#. `@tkknight`_ updated :ref:`installing_iris` to include a reference to + Windows Subsystem for Linux. (:pull:`3885`) -* `@tkknight`_ updated the :ref:`iris_docs` homepage to include panels so the - links are more visible to users. This uses the sphinx-panels_ extension. - (:pull:`3884`) +#. `@tkknight`_ updated the :ref:`iris_docs` homepage to include panels so the + links are more visible to users. This uses the sphinx-panels_ extension. + (:pull:`3884`) -* `@bjlittle`_ created the :ref:`Further topics ` section and - included documentation for :ref:`metadata`, :ref:`lenient metadata`, and - :ref:`lenient maths`. (:pull:`3890`) +#. `@bjlittle`_ created the :ref:`Further topics ` section and + included documentation for :ref:`metadata`, :ref:`lenient metadata`, and + :ref:`lenient maths`. (:pull:`3890`) -* `@jonseddon`_ updated the CF version of the netCDF saver in the - :ref:`saving_iris_cubes` section and in the equivalent function docstring. - (:pull:`3925`) +#. `@jonseddon`_ updated the CF version of the netCDF saver in the + :ref:`saving_iris_cubes` section and in the equivalent function docstring. + (:pull:`3925`) -* `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. - (:pull:`3940`) +#. `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. + (:pull:`3940`) 💼 Internal =========== -* `@pp-mo`_ and `@lbdreyer`_ removed all Iris test dependencies on `iris-grib`_ - by transferring all relevant content to the `iris-grib`_ repository. (:pull:`3662`, - :pull:`3663`, :pull:`3664`, :pull:`3665`, :pull:`3666`, :pull:`3669`, - :pull:`3670`, :pull:`3671`, :pull:`3672`, :pull:`3742`, :pull:`3746`) +#. `@pp-mo`_ and `@lbdreyer`_ removed all Iris test dependencies on `iris-grib`_ + by transferring all relevant content to the `iris-grib`_ repository. (:pull:`3662`, + :pull:`3663`, :pull:`3664`, :pull:`3665`, :pull:`3666`, :pull:`3669`, + :pull:`3670`, :pull:`3671`, :pull:`3672`, :pull:`3742`, :pull:`3746`) -* `@lbdreyer`_ and `@pp-mo`_ overhauled the handling of dimensional - metadata to remove duplication. (:pull:`3422`, :pull:`3551`) +#. `@lbdreyer`_ and `@pp-mo`_ overhauled the handling of dimensional + metadata to remove duplication. (:pull:`3422`, :pull:`3551`) -* `@trexfeathers`_ simplified the standard license header for all files, which - removes the need to repeatedly update year numbers in the header. - (:pull:`3489`) +#. `@trexfeathers`_ simplified the standard license header for all files, which + removes the need to repeatedly update year numbers in the header. + (:pull:`3489`) -* `@stephenworsley`_ changed the numerical values in tests involving the - Robinson projection due to improvements made in - `Proj`_. (:pull:`3762`) (see also `Proj#1292`_ and `Proj#2151`_) +#. `@stephenworsley`_ changed the numerical values in tests involving the + Robinson projection due to improvements made in + `Proj`_. (:pull:`3762`) (see also `Proj#1292`_ and `Proj#2151`_) -* `@stephenworsley`_ changed tests to account for more detailed descriptions of - projections in `GDAL`_. (:pull:`3762`) (see also `GDAL#1185`_) +#. `@stephenworsley`_ changed tests to account for more detailed descriptions of + projections in `GDAL`_. (:pull:`3762`) (see also `GDAL#1185`_) -* `@stephenworsley`_ changed tests to account for `GDAL`_ now saving fill values - for data without masked points. (:pull:`3762`) +#. `@stephenworsley`_ changed tests to account for `GDAL`_ now saving fill values + for data without masked points. (:pull:`3762`) -* `@trexfeathers`_ changed every graphics test that includes `Cartopy's coastlines`_ - to account for new adaptive coastline scaling. (:pull:`3762`) - (see also `Cartopy#1105`_) +#. `@trexfeathers`_ changed every graphics test that includes `Cartopy's coastlines`_ + to account for new adaptive coastline scaling. (:pull:`3762`) + (see also `Cartopy#1105`_) -* `@trexfeathers`_ changed graphics tests to account for some new default - grid-line spacing in `Cartopy`_. (:pull:`3762`) (see also `Cartopy#1117`_) +#. `@trexfeathers`_ changed graphics tests to account for some new default + grid-line spacing in `Cartopy`_. (:pull:`3762`) (see also `Cartopy#1117`_) -* `@trexfeathers`_ added additional acceptable graphics test targets to account - for very minor changes in `Matplotlib`_ version ``3.3`` (colormaps, fonts and - axes borders). (:pull:`3762`) +#. `@trexfeathers`_ added additional acceptable graphics test targets to account + for very minor changes in `Matplotlib`_ version ``3.3`` (colormaps, fonts and + axes borders). (:pull:`3762`) -* `@rcomer`_ corrected the Matplotlib backend in Iris tests to ignore - `matplotlib.rcdefaults`_, instead the tests will **always** use ``agg``. - (:pull:`3846`) +#. `@rcomer`_ corrected the Matplotlib backend in Iris tests to ignore + `matplotlib.rcdefaults`_, instead the tests will **always** use ``agg``. + (:pull:`3846`) -* `@bjlittle`_ migrated the `black`_ support from ``19.10b0`` to ``20.8b1``. - (:pull:`3866`) +#. `@bjlittle`_ migrated the `black`_ support from ``19.10b0`` to ``20.8b1``. + (:pull:`3866`) -* `@lbdreyer`_ updated the CF standard name table to the latest version: `v75`_. - (:pull:`3867`) +#. `@lbdreyer`_ updated the CF standard name table to the latest version: `v75`_. + (:pull:`3867`) -* `@bjlittle`_ added :pep:`517` and :pep:`518` support for building and - installing Iris, in particular to handle the `PyKE`_ package dependency. - (:pull:`3812`) +#. `@bjlittle`_ added :pep:`517` and :pep:`518` support for building and + installing Iris, in particular to handle the `PyKE`_ package dependency. + (:pull:`3812`) -* `@bjlittle`_ added metadata support for comparing :attr:`~iris.cube.Cube.attributes` - dictionaries that contain `numpy`_ arrays using `xxHash`_, an extremely fast - non-cryptographic hash algorithm, running at RAM speed limits. +#. `@bjlittle`_ added metadata support for comparing :attr:`~iris.cube.Cube.attributes` + dictionaries that contain `numpy`_ arrays using `xxHash`_, an extremely fast + non-cryptographic hash algorithm, running at RAM speed limits. -* `@bjlittle`_ added the ``iris.tests.assertDictEqual`` method to override - :meth:`unittest.TestCase.assertDictEqual` in order to cope with testing - metadata :attr:`~iris.cube.Cube.attributes` dictionary comparison where - the value of a key may be a `numpy`_ array. (:pull:`3785`) +#. `@bjlittle`_ added the ``iris.tests.assertDictEqual`` method to override + :meth:`unittest.TestCase.assertDictEqual` in order to cope with testing + metadata :attr:`~iris.cube.Cube.attributes` dictionary comparison where + the value of a key may be a `numpy`_ array. (:pull:`3785`) -* `@bjlittle`_ added the :func:`~iris.config.get_logger` function for creating - a generic :class:`logging.Logger` with a :class:`logging.StreamHandler` and - custom :class:`logging.Formatter`. (:pull:`3785`) +#. `@bjlittle`_ added the :func:`~iris.config.get_logger` function for creating + a generic :class:`logging.Logger` with a :class:`logging.StreamHandler` and + custom :class:`logging.Formatter`. (:pull:`3785`) -* `@owena11`_ identified and optimised a bottleneck in ``FieldsFile`` header - loading due to the use of :func:`numpy.fromfile`. (:pull:`3791`) +#. `@owena11`_ identified and optimised a bottleneck in ``FieldsFile`` header + loading due to the use of :func:`numpy.fromfile`. (:pull:`3791`) -* `@znicholls`_ added a test for plotting with the label being taken from the unit's symbol, see :meth:`~iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol` (:pull:`3902`). +#. `@znicholls`_ added a test for plotting with the label being taken from the unit's symbol, + see :meth:`~iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol` (:pull:`3902`). -* `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). +#. `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in + the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). -* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ support. (:pull:`3928`) - -* `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. - It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to - run the Iris tests, the doc-tests, the gallery doc-tests, and lint Iris - with `flake8`_ and `black`_. (:pull:`3928`) +#. `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ + support. (:pull:`3928`) +#. `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. + It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to + run the Iris tests, the doc-tests, the gallery doc-tests, and lint Iris + with `flake8`_ and `black`_. (:pull:`3928`) .. _Read the Docs: https://scitools-iris.readthedocs.io/en/latest/ .. _Matplotlib: https://matplotlib.org/ diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 399325add5..0f61d62033 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -43,204 +43,204 @@ This document explains the changes made to Iris for this release 📢 Announcements ================ -* Congratulations to `@bouweandela`_, `@jvegasbsc`_, and `@zklaus`_ who - recently became Iris core developers. They bring a wealth of expertise to the - team, and are using Iris to underpin `ESMValTool`_ - "*A community diagnostic - and performance metrics tool for routine evaluation of Earth system models - in CMIP*". Welcome aboard! 🎉 +#. Congratulations to `@bouweandela`_, `@jvegasbsc`_, and `@zklaus`_ who + recently became Iris core developers. They bring a wealth of expertise to the + team, and are using Iris to underpin `ESMValTool`_ - "*A community diagnostic + and performance metrics tool for routine evaluation of Earth system models + in CMIP*". Welcome aboard! 🎉 -* Congratulations also goes to `@jonseddon`_ who recently became an Iris core - developer. We look forward to seeing more of your awesome contributions! 🎉 +#. Congratulations also goes to `@jonseddon`_ who recently became an Iris core + developer. We look forward to seeing more of your awesome contributions! 🎉 ✨ Features =========== -* `@MoseleyS`_ greatly enhanced the :mod:`~iris.fileformats.nimrod` - module to provide richer meta-data translation when loading ``Nimrod`` data - into cubes. This covers most known operational use-cases. (:pull:`3647`) - -* `@stephenworsley`_ improved the handling of - :class:`iris.coords.CellMeasure`\ s in the :class:`~iris.cube.Cube` - statistical operations :meth:`~iris.cube.Cube.collapsed`, - :meth:`~iris.cube.Cube.aggregated_by` and - :meth:`~iris.cube.Cube.rolling_window`. These previously removed every - :class:`~iris.coords.CellMeasure` attached to the cube. Now, a - :class:`~iris.coords.CellMeasure` will only be removed if it is associated - with an axis over which the statistic is being run. (:pull:`3549`) - -* `@stephenworsley`_, `@pp-mo`_ and `@abooton`_ added support for - `CF Ancillary Data`_ variables. These are created as - :class:`iris.coords.AncillaryVariable`, and appear as components of cubes - much like :class:`~iris.coords.AuxCoord`\ s, with the new - :class:`~iris.cube.Cube` methods - :meth:`~iris.cube.Cube.add_ancillary_variable`, - :meth:`~iris.cube.Cube.remove_ancillary_variable`, - :meth:`~iris.cube.Cube.ancillary_variable`, - :meth:`~iris.cube.Cube.ancillary_variables` and - :meth:`~iris.cube.Cube.ancillary_variable_dims`. - They are loaded from and saved to NetCDF-CF files. Special support for - `Quality Flags`_ is also provided, to ensure they load and save with - appropriate units. (:pull:`3800`) - -* `@bouweandela`_ implemented lazy regridding for the - :class:`~iris.analysis.Linear`, :class:`~iris.analysis.Nearest`, and - :class:`~iris.analysis.AreaWeighted` regridding schemes. (:pull:`3701`) - -* `@bjlittle`_ added `logging`_ support within :mod:`iris.analysis.maths`, - :mod:`iris.common.metadata`, and :mod:`iris.common.resolve`. Each module - defines a :class:`logging.Logger` instance called ``logger`` with a default - ``level`` of ``INFO``. To enable ``DEBUG`` logging use - ``logger.setLevel("DEBUG")``. (:pull:`3785`) - -* `@bjlittle`_ added the :mod:`iris.common.resolve` module, which provides - infrastructure to support the analysis, identification and combination - of metadata common between two :class:`~iris.cube.Cube` operands into a - single resultant :class:`~iris.cube.Cube` that will be auto-transposed, - and with the appropriate broadcast shape. (:pull:`3785`) - -* `@bjlittle`_ added the :ref:`common metadata API `, which provides - a unified treatment of metadata across Iris, and allows users to easily - manage and manipulate their metadata in a consistent way. (:pull:`3785`) - -* `@bjlittle`_ added :ref:`lenient metadata ` support, to - allow users to control **strict** or **lenient** metadata equivalence, - difference and combination. (:pull:`3785`) - -* `@bjlittle`_ added :ref:`lenient cube maths ` support and - resolved several long standing major issues with cube arithmetic regarding - a more robust treatment of cube broadcasting, cube dimension auto-transposition, - and preservation of common metadata and coordinates during cube math operations. - Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) - -* `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when - collapsing over a single dimension. - Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. - The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works - with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) +#. `@MoseleyS`_ greatly enhanced the :mod:`~iris.fileformats.nimrod` + module to provide richer meta-data translation when loading ``Nimrod`` data + into cubes. This covers most known operational use-cases. (:pull:`3647`) + +#. `@stephenworsley`_ improved the handling of + :class:`iris.coords.CellMeasure`\ s in the :class:`~iris.cube.Cube` + statistical operations :meth:`~iris.cube.Cube.collapsed`, + :meth:`~iris.cube.Cube.aggregated_by` and + :meth:`~iris.cube.Cube.rolling_window`. These previously removed every + :class:`~iris.coords.CellMeasure` attached to the cube. Now, a + :class:`~iris.coords.CellMeasure` will only be removed if it is associated + with an axis over which the statistic is being run. (:pull:`3549`) + +#. `@stephenworsley`_, `@pp-mo`_ and `@abooton`_ added support for + `CF Ancillary Data`_ variables. These are created as + :class:`iris.coords.AncillaryVariable`, and appear as components of cubes + much like :class:`~iris.coords.AuxCoord`\ s, with the new + :class:`~iris.cube.Cube` methods + :meth:`~iris.cube.Cube.add_ancillary_variable`, + :meth:`~iris.cube.Cube.remove_ancillary_variable`, + :meth:`~iris.cube.Cube.ancillary_variable`, + :meth:`~iris.cube.Cube.ancillary_variables` and + :meth:`~iris.cube.Cube.ancillary_variable_dims`. + They are loaded from and saved to NetCDF-CF files. Special support for + `Quality Flags`_ is also provided, to ensure they load and save with + appropriate units. (:pull:`3800`) + +#. `@bouweandela`_ implemented lazy regridding for the + :class:`~iris.analysis.Linear`, :class:`~iris.analysis.Nearest`, and + :class:`~iris.analysis.AreaWeighted` regridding schemes. (:pull:`3701`) + +#. `@bjlittle`_ added `logging`_ support within :mod:`iris.analysis.maths`, + :mod:`iris.common.metadata`, and :mod:`iris.common.resolve`. Each module + defines a :class:`logging.Logger` instance called ``logger`` with a default + ``level`` of ``INFO``. To enable ``DEBUG`` logging use + ``logger.setLevel("DEBUG")``. (:pull:`3785`) + +#. `@bjlittle`_ added the :mod:`iris.common.resolve` module, which provides + infrastructure to support the analysis, identification and combination + of metadata common between two :class:`~iris.cube.Cube` operands into a + single resultant :class:`~iris.cube.Cube` that will be auto-transposed, + and with the appropriate broadcast shape. (:pull:`3785`) + +#. `@bjlittle`_ added the :ref:`common metadata API `, which provides + a unified treatment of metadata across Iris, and allows users to easily + manage and manipulate their metadata in a consistent way. (:pull:`3785`) + +#. `@bjlittle`_ added :ref:`lenient metadata ` support, to + allow users to control **strict** or **lenient** metadata equivalence, + difference and combination. (:pull:`3785`) + +#. `@bjlittle`_ added :ref:`lenient cube maths ` support and + resolved several long standing major issues with cube arithmetic regarding + a more robust treatment of cube broadcasting, cube dimension auto-transposition, + and preservation of common metadata and coordinates during cube math operations. + Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) + +#. `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when + collapsing over a single dimension. + Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. + The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works + with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) 🐛 Bugs Fixed ============= -* `@stephenworsley`_ fixed :meth:`~iris.cube.Cube.remove_coord` to now also - remove derived coordinates by removing aux_factories. (:pull:`3641`) - -* `@jonseddon`_ fixed ``isinstance(cube, collections.Iterable)`` to now behave - as expected if a :class:`~iris.cube.Cube` is iterated over, while also - ensuring that ``TypeError`` is still raised. (Fixed by setting the - ``__iter__()`` method in :class:`~iris.cube.Cube` to ``None``). - (:pull:`3656`) - -* `@stephenworsley`_ enabled cube concatenation along an axis shared by cell - measures; these cell measures are now concatenated together in the resulting - cube. Such a scenario would previously cause concatenation to inappropriately - fail. (:pull:`3566`) - -* `@stephenworsley`_ newly included :class:`~iris.coords.CellMeasure`\ s in - :class:`~iris.cube.Cube` copy operations. Previously copying a - :class:`~iris.cube.Cube` would ignore any attached - :class:`~iris.coords.CellMeasure`. (:pull:`3546`) - -* `@bjlittle`_ set a :class:`~iris.coords.CellMeasure`'s - ``measure`` attribute to have a default value of ``area``. - Previously, the ``measure`` was provided as a keyword argument to - :class:`~iris.coords.CellMeasure` with a default value of ``None``, which - caused a ``TypeError`` when no ``measure`` was provided, since ``area`` or - ``volume`` are the only accepted values. (:pull:`3533`) - -* `@trexfeathers`_ set **all** plot types in :mod:`iris.plot` to now use - `matplotlib.dates.date2num`_ to format date/time coordinates for use on a plot - axis (previously :meth:`~iris.plot.pcolor` and :meth:`~iris.plot.pcolormesh` - did not include this behaviour). (:pull:`3762`) - -* `@trexfeathers`_ changed date/time axis labels in :mod:`iris.quickplot` to - now **always** be based on the ``epoch`` used in `matplotlib.dates.date2num`_ - (previously would take the unit from a time coordinate, if present, even - though the coordinate's value had been changed via ``date2num``). - (:pull:`3762`) - -* `@pp-mo`_ newly included attributes of cell measures in NETCDF-CF - file loading; they were previously being discarded. They are now available on - the :class:`~iris.coords.CellMeasure` in the loaded :class:`~iris.cube.Cube`. - (:pull:`3800`) - -* `@pp-mo`_ fixed the netcdf loader to now handle any grid-mapping - variables with missing ``false_easting`` and ``false_northing`` properties, - which was previously failing for some coordinate systems. See :issue:`3629`. - (:pull:`3804`) - -* `@stephenworsley`_ changed the way tick labels are assigned from string coords. - Previously, the first tick label would occasionally be duplicated. This also - removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) - -* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check - ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) - -* `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's - coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) +#. `@stephenworsley`_ fixed :meth:`~iris.cube.Cube.remove_coord` to now also + remove derived coordinates by removing aux_factories. (:pull:`3641`) + +#. `@jonseddon`_ fixed ``isinstance(cube, collections.Iterable)`` to now behave + as expected if a :class:`~iris.cube.Cube` is iterated over, while also + ensuring that ``TypeError`` is still raised. (Fixed by setting the + ``__iter__()`` method in :class:`~iris.cube.Cube` to ``None``). + (:pull:`3656`) + +#. `@stephenworsley`_ enabled cube concatenation along an axis shared by cell + measures; these cell measures are now concatenated together in the resulting + cube. Such a scenario would previously cause concatenation to inappropriately + fail. (:pull:`3566`) + +#. `@stephenworsley`_ newly included :class:`~iris.coords.CellMeasure`\ s in + :class:`~iris.cube.Cube` copy operations. Previously copying a + :class:`~iris.cube.Cube` would ignore any attached + :class:`~iris.coords.CellMeasure`. (:pull:`3546`) + +#. `@bjlittle`_ set a :class:`~iris.coords.CellMeasure`'s + ``measure`` attribute to have a default value of ``area``. + Previously, the ``measure`` was provided as a keyword argument to + :class:`~iris.coords.CellMeasure` with a default value of ``None``, which + caused a ``TypeError`` when no ``measure`` was provided, since ``area`` or + ``volume`` are the only accepted values. (:pull:`3533`) + +#. `@trexfeathers`_ set **all** plot types in :mod:`iris.plot` to now use + `matplotlib.dates.date2num`_ to format date/time coordinates for use on a plot + axis (previously :meth:`~iris.plot.pcolor` and :meth:`~iris.plot.pcolormesh` + did not include this behaviour). (:pull:`3762`) + +#. `@trexfeathers`_ changed date/time axis labels in :mod:`iris.quickplot` to + now **always** be based on the ``epoch`` used in `matplotlib.dates.date2num`_ + (previously would take the unit from a time coordinate, if present, even + though the coordinate's value had been changed via ``date2num``). + (:pull:`3762`) + +#. `@pp-mo`_ newly included attributes of cell measures in NETCDF-CF + file loading; they were previously being discarded. They are now available on + the :class:`~iris.coords.CellMeasure` in the loaded :class:`~iris.cube.Cube`. + (:pull:`3800`) + +#. `@pp-mo`_ fixed the netcdf loader to now handle any grid-mapping + variables with missing ``false_easting`` and ``false_northing`` properties, + which was previously failing for some coordinate systems. See :issue:`3629`. + (:pull:`3804`) + +#. `@stephenworsley`_ changed the way tick labels are assigned from string coords. + Previously, the first tick label would occasionally be duplicated. This also + removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) + +#. `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check + ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) + +#. `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's + coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) .. _whatsnew 3.0 changes: 💣 Incompatible Changes ======================= -* `@pp-mo`_ rationalised :class:`~iris.cube.CubeList` extraction - methods: - - The former method ``iris.cube.CubeList.extract_strict``, and the ``strict`` - keyword of the :meth:`~iris.cube.CubeList.extract` method have been removed, - and are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` - and :meth:`~iris.cube.CubeList.extract_cubes`. - The new routines perform the same operation, but in a style more like other - ``Iris`` functions such as :meth:`~iris.load_cube` and :meth:`~iris.load_cubes`. - Unlike ``strict`` extraction, the type of return value is now completely - consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a - :class:`~iris.cube.Cube`, and :meth:`~iris.cube.CubeList.extract_cubes` - always returns an :class:`iris.cube.CubeList` of a length equal to the - number of constraints. (:pull:`3715`) - -* `@pp-mo`_ removed the former function - ``iris.analysis.coord_comparison``. (:pull:`3562`) - -* `@bjlittle`_ moved the - :func:`iris.experimental.equalise_cubes.equalise_attributes` function from - the :mod:`iris.experimental` module into the :mod:`iris.util` module. Please - use the :func:`iris.util.equalise_attributes` function instead. - (:pull:`3527`) - -* `@bjlittle`_ removed the module ``iris.experimental.concatenate``. In - ``v1.6.0`` the experimental ``concatenate`` functionality was moved to the - :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the - :func:`iris.experimental.concatenate.concatenate` function raised an - exception. (:pull:`3523`) - -* `@stephenworsley`_ changed the default units of :class:`~iris.coords.DimCoord` - and :class:`~iris.coords.AuxCoord` from `"1"` to `"unknown"`. (:pull:`3795`) - -* `@stephenworsley`_ changed Iris objects loaded from NetCDF-CF files to have - ``units='unknown'`` where the corresponding NetCDF variable has no ``units`` - property. Previously these cases defaulted to ``units='1'``. - This affects loading of coordinates whose file variable has no "units" - attribute (not valid, under `CF units rules`_): These will now have units - of `"unknown"`, rather than `"1"`, which **may prevent the creation of - a hybrid vertical coordinate**. While these cases used to "work", this was - never really correct behaviour. (:pull:`3795`) - -* `@SimonPeatman`_ added attribute ``var_name`` to coordinates created by the - :func:`iris.analysis.trajectory.interpolate` function. This prevents - duplicate coordinate errors in certain circumstances. (:pull:`3718`) - -* `@bjlittle`_ aligned the :func:`iris.analysis.maths.apply_ufunc` with the - rest of the :mod:`iris.analysis.maths` API by changing its keyword argument - from ``other_cube`` to ``other``. (:pull:`3785`) - -* `@bjlittle`_ changed the :meth:`iris.analysis.maths.IFunc.__call__` to ignore - any surplus ``other`` keyword argument for a ``data_func`` that requires - **only one** argument. This aligns the behaviour of - :meth:`iris.analysis.maths.IFunc.__call__` with - :func:`~iris.analysis.maths.apply_ufunc`. Previously a ``ValueError`` - exception was raised. (:pull:`3785`) +#. `@pp-mo`_ rationalised :class:`~iris.cube.CubeList` extraction + methods: + + The former method ``iris.cube.CubeList.extract_strict``, and the ``strict`` + keyword of the :meth:`~iris.cube.CubeList.extract` method have been removed, + and are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` + and :meth:`~iris.cube.CubeList.extract_cubes`. + The new routines perform the same operation, but in a style more like other + ``Iris`` functions such as :meth:`~iris.load_cube` and :meth:`~iris.load_cubes`. + Unlike ``strict`` extraction, the type of return value is now completely + consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a + :class:`~iris.cube.Cube`, and :meth:`~iris.cube.CubeList.extract_cubes` + always returns an :class:`iris.cube.CubeList` of a length equal to the + number of constraints. (:pull:`3715`) + +#. `@pp-mo`_ removed the former function + ``iris.analysis.coord_comparison``. (:pull:`3562`) + +#. `@bjlittle`_ moved the + :func:`iris.experimental.equalise_cubes.equalise_attributes` function from + the :mod:`iris.experimental` module into the :mod:`iris.util` module. Please + use the :func:`iris.util.equalise_attributes` function instead. + (:pull:`3527`) + +#. `@bjlittle`_ removed the module ``iris.experimental.concatenate``. In + ``v1.6.0`` the experimental ``concatenate`` functionality was moved to the + :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the + :func:`iris.experimental.concatenate.concatenate` function raised an + exception. (:pull:`3523`) + +#. `@stephenworsley`_ changed the default units of :class:`~iris.coords.DimCoord` + and :class:`~iris.coords.AuxCoord` from `"1"` to `"unknown"`. (:pull:`3795`) + +#. `@stephenworsley`_ changed Iris objects loaded from NetCDF-CF files to have + ``units='unknown'`` where the corresponding NetCDF variable has no ``units`` + property. Previously these cases defaulted to ``units='1'``. + This affects loading of coordinates whose file variable has no "units" + attribute (not valid, under `CF units rules`_): These will now have units + of `"unknown"`, rather than `"1"`, which **may prevent the creation of + a hybrid vertical coordinate**. While these cases used to "work", this was + never really correct behaviour. (:pull:`3795`) + +#. `@SimonPeatman`_ added attribute ``var_name`` to coordinates created by the + :func:`iris.analysis.trajectory.interpolate` function. This prevents + duplicate coordinate errors in certain circumstances. (:pull:`3718`) + +#. `@bjlittle`_ aligned the :func:`iris.analysis.maths.apply_ufunc` with the + rest of the :mod:`iris.analysis.maths` API by changing its keyword argument + from ``other_cube`` to ``other``. (:pull:`3785`) + +#. `@bjlittle`_ changed the :meth:`iris.analysis.maths.IFunc.__call__` to ignore + any surplus ``other`` keyword argument for a ``data_func`` that requires + **only one** argument. This aligns the behaviour of + :meth:`iris.analysis.maths.IFunc.__call__` with + :func:`~iris.analysis.maths.apply_ufunc`. Previously a ``ValueError`` + exception was raised. (:pull:`3785`) .. _whatsnew 3.0 deprecations: @@ -248,48 +248,48 @@ This document explains the changes made to Iris for this release 🔥 Deprecations =============== -* `@stephenworsley`_ removed the deprecated :class:`iris.Future` flags - ``cell_date_time_objects``, ``netcdf_promote``, ``netcdf_no_unlimited`` and - ``clip_latitudes``. (:pull:`3459`) +#. `@stephenworsley`_ removed the deprecated :class:`iris.Future` flags + ``cell_date_time_objects``, ``netcdf_promote``, ``netcdf_no_unlimited`` and + ``clip_latitudes``. (:pull:`3459`) -* `@stephenworsley`_ changed :attr:`iris.fileformats.pp.PPField.lbproc` to be an - ``int``. The deprecated attributes ``flag1``, ``flag2`` etc. have been - removed from it. (:pull:`3461`) +#. `@stephenworsley`_ changed :attr:`iris.fileformats.pp.PPField.lbproc` to be an + ``int``. The deprecated attributes ``flag1``, ``flag2`` etc. have been + removed from it. (:pull:`3461`) -* `@bjlittle`_ deprecated :func:`~iris.util.as_compatible_shape` in preference - for :class:`~iris.common.resolve.Resolve` e.g., ``Resolve(src, tgt)(tgt.core_data())``. - The :func:`~iris.util.as_compatible_shape` function will be removed in a future - release of Iris. (:pull:`3892`) +#. `@bjlittle`_ deprecated :func:`~iris.util.as_compatible_shape` in preference + for :class:`~iris.common.resolve.Resolve` e.g., ``Resolve(src, tgt)(tgt.core_data())``. + The :func:`~iris.util.as_compatible_shape` function will be removed in a future + release of Iris. (:pull:`3892`) 🔗 Dependencies =============== -* `@stephenworsley`_, `@trexfeathers`_ and `@bjlittle`_ removed ``Python2`` - support, modernising the codebase by switching to exclusive ``Python3`` - support. (:pull:`3513`) +#. `@stephenworsley`_, `@trexfeathers`_ and `@bjlittle`_ removed ``Python2`` + support, modernising the codebase by switching to exclusive ``Python3`` + support. (:pull:`3513`) -* `@bjlittle`_ improved the developer set up process. Configuring Iris and - :ref:`installing_from_source` as a developer with all the required package - dependencies is now easier with our curated conda environment YAML files. - (:pull:`3812`) +#. `@bjlittle`_ improved the developer set up process. Configuring Iris and + :ref:`installing_from_source` as a developer with all the required package + dependencies is now easier with our curated conda environment YAML files. + (:pull:`3812`) -* `@stephenworsley`_ pinned Iris to require `Dask`_ ``>=2.0``. (:pull:`3460`) +#. `@stephenworsley`_ pinned Iris to require `Dask`_ ``>=2.0``. (:pull:`3460`) -* `@stephenworsley`_ and `@trexfeathers`_ pinned Iris to require - `Cartopy`_ ``>=0.18``, in order to remain compatible with the latest version - of `Matplotlib`_. (:pull:`3762`) +#. `@stephenworsley`_ and `@trexfeathers`_ pinned Iris to require + `Cartopy`_ ``>=0.18``, in order to remain compatible with the latest version + of `Matplotlib`_. (:pull:`3762`) -* `@bjlittle`_ unpinned Iris to use the latest version of `Matplotlib`_. - Supporting ``Iris`` for both ``Python2`` and ``Python3`` had resulted in - pinning our dependency on `Matplotlib`_ at ``v2.x``. But this is no longer - necessary now that ``Python2`` support has been dropped. (:pull:`3468`) +#. `@bjlittle`_ unpinned Iris to use the latest version of `Matplotlib`_. + Supporting ``Iris`` for both ``Python2`` and ``Python3`` had resulted in + pinning our dependency on `Matplotlib`_ at ``v2.x``. But this is no longer + necessary now that ``Python2`` support has been dropped. (:pull:`3468`) -* `@stephenworsley`_ and `@trexfeathers`_ unpinned Iris to use the latest version - of `Proj`_. (:pull:`3762`) +#. `@stephenworsley`_ and `@trexfeathers`_ unpinned Iris to use the latest version + of `Proj`_. (:pull:`3762`) -* `@stephenworsley`_ and `@trexfeathers`_ removed GDAL from the extensions - dependency group. We no longer consider it to be an extension. (:pull:`3762`) +#. `@stephenworsley`_ and `@trexfeathers`_ removed GDAL from the extensions + dependency group. We no longer consider it to be an extension. (:pull:`3762`) .. _whatsnew 3.0 docs: @@ -297,157 +297,160 @@ This document explains the changes made to Iris for this release 📚 Documentation ================ -* `@tkknight`_ moved the - :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` - from the general part of the gallery to oceanography. (:pull:`3761`) +#. `@tkknight`_ moved the + :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` + from the general part of the gallery to oceanography. (:pull:`3761`) -* `@tkknight`_ updated documentation to use a modern sphinx theme and be - served from https://scitools-iris.readthedocs.io/en/latest/. (:pull:`3752`) +#. `@tkknight`_ updated documentation to use a modern sphinx theme and be + served from https://scitools-iris.readthedocs.io/en/latest/. (:pull:`3752`) -* `@bjlittle`_ added support for the `black`_ code formatter. This is - now automatically checked on GitHub PRs, replacing the older, unittest-based - ``iris.tests.test_coding_standards.TestCodeFormat``. Black provides automatic - code format correction for most IDEs. See the new developer guide section on - :ref:`code_formatting`. (:pull:`3518`) +#. `@bjlittle`_ added support for the `black`_ code formatter. This is + now automatically checked on GitHub PRs, replacing the older, unittest-based + ``iris.tests.test_coding_standards.TestCodeFormat``. Black provides automatic + code format correction for most IDEs. See the new developer guide section on + :ref:`code_formatting`. (:pull:`3518`) -* `@tkknight`_ and `@trexfeathers`_ refreshed the :ref:`whats_new_contributions` - for the :ref:`iris_whatsnew`. This includes always creating the ``latest`` - what's new page so it appears on the latest documentation at - https://scitools-iris.readthedocs.io/en/latest/whatsnew. This resolves - :issue:`2104`, :issue:`3451`, :issue:`3818`, :issue:`3837`. Also updated the - :ref:`iris_development_releases_steps` to follow when making a release. - (:pull:`3769`, :pull:`3838`, :pull:`3843`) +#. `@tkknight`_ and `@trexfeathers`_ refreshed the :ref:`whats_new_contributions` + for the :ref:`iris_whatsnew`. This includes always creating the ``latest`` + what's new page so it appears on the latest documentation at + https://scitools-iris.readthedocs.io/en/latest/whatsnew. This resolves + :issue:`2104`, :issue:`3451`, :issue:`3818`, :issue:`3837`. Also updated the + :ref:`iris_development_releases_steps` to follow when making a release. + (:pull:`3769`, :pull:`3838`, :pull:`3843`) -* `@tkknight`_ enabled the PDF creation of the documentation on the - `Read the Docs`_ service. The PDF may be accessed by clicking on the version - at the bottom of the side bar, then selecting ``PDF`` from the ``Downloads`` - section. (:pull:`3765`) +#. `@tkknight`_ enabled the PDF creation of the documentation on the + `Read the Docs`_ service. The PDF may be accessed by clicking on the version + at the bottom of the side bar, then selecting ``PDF`` from the ``Downloads`` + section. (:pull:`3765`) -* `@stephenworsley`_ added a warning to the - :func:`iris.analysis.cartography.project` function regarding its behaviour on - projections with non-rectangular boundaries. (:pull:`3762`) +#. `@stephenworsley`_ added a warning to the + :func:`iris.analysis.cartography.project` function regarding its behaviour on + projections with non-rectangular boundaries. (:pull:`3762`) -* `@stephenworsley`_ added the :ref:`cube_maths_combining_units` section to the - user guide to clarify how ``Units`` are handled during cube arithmetic. - (:pull:`3803`) +#. `@stephenworsley`_ added the :ref:`cube_maths_combining_units` section to the + user guide to clarify how ``Units`` are handled during cube arithmetic. + (:pull:`3803`) -* `@tkknight`_ overhauled the :ref:`developers_guide` including information on - getting involved in becoming a contributor and general structure of the - guide. This resolves :issue:`2170`, :issue:`2331`, :issue:`3453`, - :issue:`314`, :issue:`2902`. (:pull:`3852`) +#. `@tkknight`_ overhauled the :ref:`developers_guide` including information on + getting involved in becoming a contributor and general structure of the + guide. This resolves :issue:`2170`, :issue:`2331`, :issue:`3453`, + :issue:`314`, :issue:`2902`. (:pull:`3852`) -* `@rcomer`_ added argument descriptions to the :class:`~iris.coords.DimCoord` - docstring. (:pull:`3681`) +#. `@rcomer`_ added argument descriptions to the :class:`~iris.coords.DimCoord` + docstring. (:pull:`3681`) -* `@tkknight`_ added two url's to be ignored for the ``make linkcheck``. This - will ensure the Iris github project is not repeatedly hit during the - linkcheck for issues and pull requests as it can result in connection - refused and thus travis-ci_ job failures. For more information on linkcheck, - see :ref:`contributing.documentation.testing`. (:pull:`3873`) +#. `@tkknight`_ added two url's to be ignored for the ``make linkcheck``. This + will ensure the Iris github project is not repeatedly hit during the + linkcheck for issues and pull requests as it can result in connection + refused and thus travis-ci_ job failures. For more information on linkcheck, + see :ref:`contributing.documentation.testing`. (:pull:`3873`) -* `@tkknight`_ enabled the napolean_ package that is used by sphinx_ to cater - for the existing google style docstrings and to also allow for `numpy`_ - docstrings. This resolves :issue:`3841`. (:pull:`3871`) +#. `@tkknight`_ enabled the napolean_ package that is used by sphinx_ to cater + for the existing google style docstrings and to also allow for `numpy`_ + docstrings. This resolves :issue:`3841`. (:pull:`3871`) -* `@tkknight`_ configured ``sphinx-build`` to promote warnings to errors when - building the documentation via ``make html``. This will minimise technical - debt accruing for the documentation. (:pull:`3877`) +#. `@tkknight`_ configured ``sphinx-build`` to promote warnings to errors when + building the documentation via ``make html``. This will minimise technical + debt accruing for the documentation. (:pull:`3877`) -* `@tkknight`_ updated :ref:`installing_iris` to include a reference to - Windows Subsystem for Linux. (:pull:`3885`) +#. `@tkknight`_ updated :ref:`installing_iris` to include a reference to + Windows Subsystem for Linux. (:pull:`3885`) -* `@tkknight`_ updated the :ref:`iris_docs` homepage to include panels so the - links are more visible to users. This uses the sphinx-panels_ extension. - (:pull:`3884`) +#. `@tkknight`_ updated the :ref:`iris_docs` homepage to include panels so the + links are more visible to users. This uses the sphinx-panels_ extension. + (:pull:`3884`) -* `@bjlittle`_ created the :ref:`Further topics ` section and - included documentation for :ref:`metadata`, :ref:`lenient metadata`, and - :ref:`lenient maths`. (:pull:`3890`) +#. `@bjlittle`_ created the :ref:`Further topics ` section and + included documentation for :ref:`metadata`, :ref:`lenient metadata`, and + :ref:`lenient maths`. (:pull:`3890`) -* `@jonseddon`_ updated the CF version of the netCDF saver in the - :ref:`saving_iris_cubes` section and in the equivalent function docstring. - (:pull:`3925`) +#. `@jonseddon`_ updated the CF version of the netCDF saver in the + :ref:`saving_iris_cubes` section and in the equivalent function docstring. + (:pull:`3925`) -* `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. - (:pull:`3940`) +#. `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. + (:pull:`3940`) 💼 Internal =========== -* `@pp-mo`_ and `@lbdreyer`_ removed all Iris test dependencies on `iris-grib`_ - by transferring all relevant content to the `iris-grib`_ repository. (:pull:`3662`, - :pull:`3663`, :pull:`3664`, :pull:`3665`, :pull:`3666`, :pull:`3669`, - :pull:`3670`, :pull:`3671`, :pull:`3672`, :pull:`3742`, :pull:`3746`) +#. `@pp-mo`_ and `@lbdreyer`_ removed all Iris test dependencies on `iris-grib`_ + by transferring all relevant content to the `iris-grib`_ repository. (:pull:`3662`, + :pull:`3663`, :pull:`3664`, :pull:`3665`, :pull:`3666`, :pull:`3669`, + :pull:`3670`, :pull:`3671`, :pull:`3672`, :pull:`3742`, :pull:`3746`) -* `@lbdreyer`_ and `@pp-mo`_ overhauled the handling of dimensional - metadata to remove duplication. (:pull:`3422`, :pull:`3551`) +#. `@lbdreyer`_ and `@pp-mo`_ overhauled the handling of dimensional + metadata to remove duplication. (:pull:`3422`, :pull:`3551`) -* `@trexfeathers`_ simplified the standard license header for all files, which - removes the need to repeatedly update year numbers in the header. - (:pull:`3489`) +#. `@trexfeathers`_ simplified the standard license header for all files, which + removes the need to repeatedly update year numbers in the header. + (:pull:`3489`) -* `@stephenworsley`_ changed the numerical values in tests involving the - Robinson projection due to improvements made in - `Proj`_. (:pull:`3762`) (see also `Proj#1292`_ and `Proj#2151`_) +#. `@stephenworsley`_ changed the numerical values in tests involving the + Robinson projection due to improvements made in + `Proj`_. (:pull:`3762`) (see also `Proj#1292`_ and `Proj#2151`_) -* `@stephenworsley`_ changed tests to account for more detailed descriptions of - projections in `GDAL`_. (:pull:`3762`) (see also `GDAL#1185`_) +#. `@stephenworsley`_ changed tests to account for more detailed descriptions of + projections in `GDAL`_. (:pull:`3762`) (see also `GDAL#1185`_) -* `@stephenworsley`_ changed tests to account for `GDAL`_ now saving fill values - for data without masked points. (:pull:`3762`) +#. `@stephenworsley`_ changed tests to account for `GDAL`_ now saving fill values + for data without masked points. (:pull:`3762`) -* `@trexfeathers`_ changed every graphics test that includes `Cartopy's coastlines`_ - to account for new adaptive coastline scaling. (:pull:`3762`) - (see also `Cartopy#1105`_) +#. `@trexfeathers`_ changed every graphics test that includes `Cartopy's coastlines`_ + to account for new adaptive coastline scaling. (:pull:`3762`) + (see also `Cartopy#1105`_) -* `@trexfeathers`_ changed graphics tests to account for some new default - grid-line spacing in `Cartopy`_. (:pull:`3762`) (see also `Cartopy#1117`_) +#. `@trexfeathers`_ changed graphics tests to account for some new default + grid-line spacing in `Cartopy`_. (:pull:`3762`) (see also `Cartopy#1117`_) -* `@trexfeathers`_ added additional acceptable graphics test targets to account - for very minor changes in `Matplotlib`_ version ``3.3`` (colormaps, fonts and - axes borders). (:pull:`3762`) +#. `@trexfeathers`_ added additional acceptable graphics test targets to account + for very minor changes in `Matplotlib`_ version ``3.3`` (colormaps, fonts and + axes borders). (:pull:`3762`) -* `@rcomer`_ corrected the Matplotlib backend in Iris tests to ignore - `matplotlib.rcdefaults`_, instead the tests will **always** use ``agg``. - (:pull:`3846`) +#. `@rcomer`_ corrected the Matplotlib backend in Iris tests to ignore + `matplotlib.rcdefaults`_, instead the tests will **always** use ``agg``. + (:pull:`3846`) -* `@bjlittle`_ migrated the `black`_ support from ``19.10b0`` to ``20.8b1``. - (:pull:`3866`) +#. `@bjlittle`_ migrated the `black`_ support from ``19.10b0`` to ``20.8b1``. + (:pull:`3866`) -* `@lbdreyer`_ updated the CF standard name table to the latest version: `v75`_. - (:pull:`3867`) +#. `@lbdreyer`_ updated the CF standard name table to the latest version: `v75`_. + (:pull:`3867`) -* `@bjlittle`_ added :pep:`517` and :pep:`518` support for building and - installing Iris, in particular to handle the `PyKE`_ package dependency. - (:pull:`3812`) +#. `@bjlittle`_ added :pep:`517` and :pep:`518` support for building and + installing Iris, in particular to handle the `PyKE`_ package dependency. + (:pull:`3812`) -* `@bjlittle`_ added metadata support for comparing :attr:`~iris.cube.Cube.attributes` - dictionaries that contain `numpy`_ arrays using `xxHash`_, an extremely fast - non-cryptographic hash algorithm, running at RAM speed limits. +#. `@bjlittle`_ added metadata support for comparing :attr:`~iris.cube.Cube.attributes` + dictionaries that contain `numpy`_ arrays using `xxHash`_, an extremely fast + non-cryptographic hash algorithm, running at RAM speed limits. -* `@bjlittle`_ added the ``iris.tests.assertDictEqual`` method to override - :meth:`unittest.TestCase.assertDictEqual` in order to cope with testing - metadata :attr:`~iris.cube.Cube.attributes` dictionary comparison where - the value of a key may be a `numpy`_ array. (:pull:`3785`) +#. `@bjlittle`_ added the ``iris.tests.assertDictEqual`` method to override + :meth:`unittest.TestCase.assertDictEqual` in order to cope with testing + metadata :attr:`~iris.cube.Cube.attributes` dictionary comparison where + the value of a key may be a `numpy`_ array. (:pull:`3785`) -* `@bjlittle`_ added the :func:`~iris.config.get_logger` function for creating - a generic :class:`logging.Logger` with a :class:`logging.StreamHandler` and - custom :class:`logging.Formatter`. (:pull:`3785`) +#. `@bjlittle`_ added the :func:`~iris.config.get_logger` function for creating + a generic :class:`logging.Logger` with a :class:`logging.StreamHandler` and + custom :class:`logging.Formatter`. (:pull:`3785`) -* `@owena11`_ identified and optimised a bottleneck in ``FieldsFile`` header - loading due to the use of :func:`numpy.fromfile`. (:pull:`3791`) +#. `@owena11`_ identified and optimised a bottleneck in ``FieldsFile`` header + loading due to the use of :func:`numpy.fromfile`. (:pull:`3791`) -* `@znicholls`_ added a test for plotting with the label being taken from the unit's symbol, see :meth:`~iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol` (:pull:`3902`). +#. `@znicholls`_ added a test for plotting with the label being taken from the unit's symbol, + see :meth:`~iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol` (:pull:`3902`). -* `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). +#. `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in + the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). -* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ support. (:pull:`3928`) +#. `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ + support. (:pull:`3928`) -* `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. - It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to - run the Iris tests, the doc-tests, the gallery doc-tests, and lint Iris - with `flake8`_ and `black`_. (:pull:`3928`) +#. `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. + It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to + run the Iris tests, the doc-tests, the gallery doc-tests, and lint Iris + with `flake8`_ and `black`_. (:pull:`3928`) .. _Read the Docs: https://scitools-iris.readthedocs.io/en/latest/ .. _Matplotlib: https://matplotlib.org/ diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 6205dc6bfa..5809b3cf2e 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -10,58 +10,59 @@ This document explains the changes made to Iris for this release 📢 Announcements ================ -* N/A +#. N/A ✨ Features =========== -* `@pelson`_ and `@trexfeathers`_ enhanced :meth:iris.plot.plot and - :meth:iris.quickplot.plot to automatically place the cube on the x axis if - the primary coordinate being plotted against is a vertical coordinate. E.g. - ``iris.plot.plot(z_cube)`` will produce a z-vs-phenomenon plot, where before - it would have produced a phenomenon-vs-z plot. (:pull:`3906`) +#. `@pelson`_ and `@trexfeathers`_ enhanced :meth:iris.plot.plot and + :meth:iris.quickplot.plot to automatically place the cube on the x axis if + the primary coordinate being plotted against is a vertical coordinate. E.g. + ``iris.plot.plot(z_cube)`` will produce a z-vs-phenomenon plot, where before + it would have produced a phenomenon-vs-z plot. (:pull:`3906`) 🐛 Bugs Fixed ============= -* `@gcaria`_ fixed :meth:`~iris.cube.Cube.cell_measure_dims` to also accept the string name of a :class:`~iris.coords.CellMeasure`. (:pull:`3931`) -* `@gcaria`_ fixed :meth:`~iris.cube.Cube.ancillary_variable_dims` to also accept the string name of a :class:`~iris.coords.AncillaryVariable`. (:pull:`3931`) +#. `@gcaria`_ fixed :meth:`~iris.cube.Cube.cell_measure_dims` to also accept the + string name of a :class:`~iris.coords.CellMeasure`. (:pull:`3931`) +#. `@gcaria`_ fixed :meth:`~iris.cube.Cube.ancillary_variable_dims` to also accept + the string name of a :class:`~iris.coords.AncillaryVariable`. (:pull:`3931`) 💣 Incompatible Changes ======================= -* N/A +#. N/A 🔥 Deprecations =============== -* N/A +#. N/A 🔗 Dependencies =============== -* N/A +#. N/A 📚 Documentation ================ -* `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. - (:pull:`3933`) -* `@MHBalsmeier`_ Described non-conda installation on Debian-based distros. - (:pull:`3958`) +#. `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. + (:pull:`3933`) +#. `@MHBalsmeier`_ Described non-conda installation on Debian-based distros. + (:pull:`3958`) 💼 Internal =========== -* `@rcomer`_ removed an old unused test file. (:pull:`3913`) - +#. `@rcomer`_ removed an old unused test file. (:pull:`3913`) .. _@pelson: https://github.com/pelson diff --git a/docs/iris/src/whatsnew/latest.rst.template b/docs/iris/src/whatsnew/latest.rst.template index 67518e539a..06c04f264f 100644 --- a/docs/iris/src/whatsnew/latest.rst.template +++ b/docs/iris/src/whatsnew/latest.rst.template @@ -10,46 +10,46 @@ This document explains the changes made to Iris for this release 📢 Announcements ================ -* N/A +#. N/A ✨ Features =========== -* N/A +#. N/A 🐛 Bugs Fixed ============= -* N/A +#. N/A 💣 Incompatible Changes ======================= -* N/A +#. N/A 🔥 Deprecations =============== -* N/A +#. N/A 🔗 Dependencies =============== -* N/A +#. N/A 📚 Documentation ================ -* N/A +#. N/A 💼 Internal =========== -* N/A +#. N/A From 28b494d963f447d51cd9e85a28bd85a61cb254cf Mon Sep 17 00:00:00 2001 From: Bill Little Date: Sun, 31 Jan 2021 18:53:02 +0000 Subject: [PATCH 6/9] Docs whatsnew add dropdowns to the template (#3969) * add release highlights dropdown to latest and template * add the patches dropdown to latest template * make the patch release pulldown v3 generic --- docs/iris/src/whatsnew/latest.rst | 17 +++++++++++ docs/iris/src/whatsnew/latest.rst.template | 34 +++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 5809b3cf2e..f33e546dc3 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -7,6 +7,21 @@ This document explains the changes made to Iris for this release (:doc:`View all changes `.) +.. dropdown:: :opticon:`report` 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! + + 📢 Announcements ================ @@ -65,6 +80,8 @@ This document explains the changes made to Iris for this release #. `@rcomer`_ removed an old unused test file. (:pull:`3913`) + +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose .. _@pelson: https://github.com/pelson .. _@trexfeathers: https://github.com/trexfeathers .. _@gcaria: https://github.com/gcaria diff --git a/docs/iris/src/whatsnew/latest.rst.template b/docs/iris/src/whatsnew/latest.rst.template index 06c04f264f..0dd7cc788b 100644 --- a/docs/iris/src/whatsnew/latest.rst.template +++ b/docs/iris/src/whatsnew/latest.rst.template @@ -7,6 +7,33 @@ This document explains the changes made to Iris for this release (:doc:`View all changes `.) +.. dropdown:: :opticon:`alert` v3.X.X Patches + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + :open: + + The patches in this release of Iris include: + + #. N/A + + +.. dropdown:: :opticon:`report` 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! + + 📢 Announcements ================ @@ -52,4 +79,9 @@ This document explains the changes made to Iris for this release 💼 Internal =========== -#. N/A +* N/A + + + + +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose From 15bcd700807c747f2a856deb911b2abeed3f5ff5 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 1 Feb 2021 11:50:12 +0000 Subject: [PATCH 7/9] reorganise docs common links + add core devs (#3972) * reorganise docs common links + add core devs * add common links comment --- docs/iris/src/common_links.inc | 66 ++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/docs/iris/src/common_links.inc b/docs/iris/src/common_links.inc index 3941bfaff2..050752a483 100644 --- a/docs/iris/src/common_links.inc +++ b/docs/iris/src/common_links.inc @@ -1,27 +1,57 @@ -.. _SciTools: https://github.com/SciTools +.. comment + Common resources in alphabetical order: + +.. _.cirrus.yml: https://github.com/SciTools/iris/blob/master/.cirrus.yml +.. _.flake8.yml: https://github.com/SciTools/iris/blob/master/.flake8 +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris +.. _conda: https://docs.conda.io/en/latest/ +.. _contributor: https://github.com/SciTools/scitools.org.uk/blob/master/contributors.json +.. _core developers: https://github.com/SciTools/scitools.org.uk/blob/master/contributors.json +.. _generating sss keys for GitHub: https://docs.github.com/en/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account +.. _GitHub Help Documentation: https://docs.github.com/en/github .. _Iris: https://github.com/SciTools/iris .. _Iris GitHub: https://github.com/SciTools/iris .. _iris mailing list: https://groups.google.com/forum/#!forum/scitools-iris +.. _iris-sample-data: https://github.com/SciTools/iris-sample-data +.. _iris-test-data: https://github.com/SciTools/iris-test-data .. _issue: https://github.com/SciTools/iris/issues .. _issues: https://github.com/SciTools/iris/issues +.. _legacy documentation: https://scitools.org.uk/iris/docs/v2.4.0/ +.. _matplotlib: https://matplotlib.org/ +.. _napolean: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/sphinxcontrib.napoleon.html +.. _New Issue: https://github.com/scitools/iris/issues/new/choose .. _pull request: https://github.com/SciTools/iris/pulls .. _pull requests: https://github.com/SciTools/iris/pulls -.. _contributor: https://github.com/SciTools/scitools.org.uk/blob/master/contributors.json -.. _core developers: https://github.com/SciTools/scitools.org.uk/blob/master/contributors.json -.. _iris-test-data: https://github.com/SciTools/iris-test-data -.. _iris-sample-data: https://github.com/SciTools/iris-sample-data -.. _test-iris-imagehash: https://github.com/SciTools/test-iris-imagehash .. _readthedocs.yml: https://github.com/SciTools/iris/blob/master/requirements/ci/readthedocs.yml -.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris -.. _.cirrus.yml: https://github.com/SciTools/iris/blob/master/.cirrus.yml -.. _.flake8.yml: https://github.com/SciTools/iris/blob/master/.flake8 -.. _GitHub Help Documentation: https://docs.github.com/en/github -.. _using git: https://docs.github.com/en/github/using-git -.. _generating sss keys for GitHub: https://docs.github.com/en/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account -.. _New Issue: https://github.com/scitools/iris/issues/new/choose -.. _matplotlib: https://matplotlib.org/ -.. _conda: https://docs.conda.io/en/latest/ +.. _SciTools: https://github.com/SciTools .. _sphinx: https://www.sphinx-doc.org/en/master/ -.. _napolean: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/sphinxcontrib.napoleon.html -.. _legacy documentation: https://scitools.org.uk/iris/docs/v2.4.0/ -.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris +.. _test-iris-imagehash: https://github.com/SciTools/test-iris-imagehash +.. _using git: https://docs.github.com/en/github/using-git + + +.. comment + Core developers (@github names) in alphabetical order: + +.. _@abooton: https://github.com/abooton +.. _@alastair-gemmell: https://github.com/alastair-gemmell +.. _@ajdawson: https://github.com/ajdawson +.. _@bjlittle: https://github.com/bjlittle +.. _@bouweandela: https://github.com/bouweandela +.. _@corinnebosley: https://github.com/corinnebosley +.. _@cpelley: https://github.com/cpelley +.. _@djkirkham: https://github.com/djkirkham +.. _@DPeterK: https://github.com/DPeterK +.. _@esc24: https://github.com/esc24 +.. _@jonseddon: https://github.com/jonseddon +.. _@jvegasbsc: https://github.com/jvegasbsc +.. _@lbdreyer: https://github.com/lbdreyer +.. _@marqh: https://github.com/marqh +.. _@pelson: https://github.com/pelson +.. _@pp-mo: https://github.com/pp-mo +.. _@QuLogic: https://github.com/QuLogic +.. _@rcomer: https://github.com/rcomer +.. _@rhattersley: https://github.com/rhattersley +.. _@stephenworsley: https://github.com/stephenworsley +.. _@tkknight: https://github.com/tkknight +.. _@trexfeathers: https://github.com/trexfeathers +.. _@zklaus: https://github.com/zklaus From 1e8ccdfc037f412b4ddc4b19d1fda75a13fb223b Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 1 Feb 2021 12:01:08 +0000 Subject: [PATCH 8/9] document that iris.coords.Coord is an ABC (#3971) * document that iris.coords.Coord is an ABC * use rst comment directive instead of only directive --- docs/iris/src/whatsnew/latest.rst | 28 +++++--- docs/iris/src/whatsnew/latest.rst.template | 9 ++- lib/iris/coords.py | 76 +++++++++++++++++----- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index f33e546dc3..0b1b8db1b6 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -43,6 +43,7 @@ This document explains the changes made to Iris for this release #. `@gcaria`_ fixed :meth:`~iris.cube.Cube.cell_measure_dims` to also accept the string name of a :class:`~iris.coords.CellMeasure`. (:pull:`3931`) + #. `@gcaria`_ fixed :meth:`~iris.cube.Cube.ancillary_variable_dims` to also accept the string name of a :class:`~iris.coords.AncillaryVariable`. (:pull:`3931`) @@ -68,10 +69,12 @@ This document explains the changes made to Iris for this release 📚 Documentation ================ -#. `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. - (:pull:`3933`) -#. `@MHBalsmeier`_ Described non-conda installation on Debian-based distros. - (:pull:`3958`) +#. `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. (:pull:`3933`) + +#. `@MHBalsmeier`_ described non-conda installation on Debian-based distros. (:pull:`3958`) + +#. `@bjlittle`_ clarified in the doc-string that :class:`~iris.coords.Coord` is now an `abstract base class`_ of + coordinates since ``v3.0.0``, and it is **not** possible to create an instance of it. (:pull:`3971`) 💼 Internal @@ -80,10 +83,19 @@ This document explains the changes made to Iris for this release #. `@rcomer`_ removed an old unused test file. (:pull:`3913`) +.. comment + What's new author names (@github name) in alphabetical order: -.. _GitHub: https://github.com/SciTools/iris/issues/new/choose -.. _@pelson: https://github.com/pelson -.. _@trexfeathers: https://github.com/trexfeathers +.. _@bjlittle: https://github.com/bjlittle .. _@gcaria: https://github.com/gcaria -.. _@rcomer: https://github.com/rcomer .. _@MHBalsmeier: https://github.com/MHBalsmeier +.. _@pelson: https://github.com/pelson +.. _@rcomer: https://github.com/rcomer +.. _@trexfeathers: https://github.com/trexfeathers + + +.. comment + What's new resources in alphabetical order: + +.. _abstract base class: https://docs.python.org/3/library/abc.html +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose diff --git a/docs/iris/src/whatsnew/latest.rst.template b/docs/iris/src/whatsnew/latest.rst.template index 0dd7cc788b..75a1f8cd76 100644 --- a/docs/iris/src/whatsnew/latest.rst.template +++ b/docs/iris/src/whatsnew/latest.rst.template @@ -79,9 +79,16 @@ This document explains the changes made to Iris for this release 💼 Internal =========== -* N/A +#. N/A + + +.. comment + What's new author names (@github name) in alphabetical order: + +.. comment + What's new resources in alphabetical order: .. _GitHub: https://github.com/SciTools/iris/issues/new/choose diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 76ca83cd96..cfeb24cdcb 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -12,7 +12,6 @@ from collections import namedtuple from collections.abc import Iterator import copy -from functools import wraps from itertools import chain, zip_longest import operator import warnings @@ -1272,7 +1271,7 @@ def contains_point(self, point): class Coord(_DimensionalMetadata): """ - Superclass for coordinates. + Abstract base class for coordinates. """ @@ -1291,7 +1290,7 @@ def __init__( ): """ - Constructs a single coordinate. + Coordinate abstract base class. As of ``v3.0.0`` you **cannot** create an instance of :class:`Coord`. Args: @@ -1313,17 +1312,17 @@ def __init__( * bounds An array of values describing the bounds of each cell. Given n bounds for each cell, the shape of the bounds array should be - points.shape + (n,). For example, a 1d coordinate with 100 points + points.shape + (n,). For example, a 1D coordinate with 100 points and two bounds per cell would have a bounds array of shape (100, 2) Note if the data is a climatology, `climatological` should be set. * attributes - A dictionary containing other cf and user-defined attributes. + A dictionary containing other CF and user-defined attributes. * coord_system A :class:`~iris.coord_systems.CoordSystem` representing the coordinate system of the coordinate, - e.g. a :class:`~iris.coord_systems.GeogCS` for a longitude Coord. + e.g., a :class:`~iris.coord_systems.GeogCS` for a longitude coordinate. * climatological (bool): When True: the coordinate is a NetCDF climatological time axis. When True: saving in NetCDF will give the coordinate variable a @@ -2250,7 +2249,8 @@ def _xml_id_extra(self, unique_value): class DimCoord(Coord): """ - A coordinate that is 1D, numeric, and strictly monotonic. + A coordinate that is 1D, and numeric, with values that have a strict monotonic ordering. Missing values are not + permitted in a :class:`DimCoord`. """ @@ -2275,7 +2275,7 @@ def from_regular( optionally bounds. The majority of the arguments are defined as for - :meth:`Coord.__init__`, but those which differ are defined below. + :class:`Coord`, but those which differ are defined below. Args: @@ -2336,8 +2336,9 @@ def __init__( climatological=False, ): """ - Create a 1D, numeric, and strictly monotonic :class:`Coord` with - read-only points and bounds. + Create a 1D, numeric, and strictly monotonic coordinate with **immutable** points and bounds. + + Missing values are not permitted. Args: @@ -2369,11 +2370,11 @@ def __init__( Note if the data is a climatology, `climatological` should be set. * attributes: - A dictionary containing other cf and user-defined attributes. + A dictionary containing other CF and user-defined attributes. * coord_system: A :class:`~iris.coord_systems.CoordSystem` representing the coordinate system of the coordinate, - e.g. a :class:`~iris.coord_systems.GeogCS` for a longitude Coord. + e.g., a :class:`~iris.coord_systems.GeogCS` for a longitude coordinate. * circular (bool): Whether the coordinate wraps by the :attr:`~iris.coords.DimCoord.units.modulus` i.e., the longitude coordinate wraps around the full great circle. @@ -2624,15 +2625,54 @@ class AuxCoord(Coord): """ A CF auxiliary coordinate. - .. note:: - - There are currently no specific properties of :class:`AuxCoord`, - everything is inherited from :class:`Coord`. - """ - @wraps(Coord.__init__, assigned=("__doc__",), updated=()) def __init__(self, *args, **kwargs): + """ + Create a coordinate with **mutable** points and bounds. + + Args: + + * points: + The values (or value in the case of a scalar coordinate) for each + cell of the coordinate. + + Kwargs: + + * standard_name: + CF standard name of the coordinate. + * long_name: + Descriptive name of the coordinate. + * var_name: + The netCDF variable name for the coordinate. + * units + The :class:`~cf_units.Unit` of the coordinate's values. + Can be a string, which will be converted to a Unit object. + * bounds + An array of values describing the bounds of each cell. Given n + bounds for each cell, the shape of the bounds array should be + points.shape + (n,). For example, a 1D coordinate with 100 points + and two bounds per cell would have a bounds array of shape + (100, 2) + Note if the data is a climatology, `climatological` + should be set. + * attributes + A dictionary containing other CF and user-defined attributes. + * coord_system + A :class:`~iris.coord_systems.CoordSystem` representing the + coordinate system of the coordinate, + e.g., a :class:`~iris.coord_systems.GeogCS` for a longitude coordinate. + * climatological (bool): + When True: the coordinate is a NetCDF climatological time axis. + When True: saving in NetCDF will give the coordinate variable a + 'climatology' attribute and will create a boundary variable called + '_climatology' in place of a standard bounds + attribute and bounds variable. + Will set to True when a climatological time axis is loaded + from NetCDF. + Always False if no bounds exist. + + """ super().__init__(*args, **kwargs) # Logically, :class:`Coord` is an abstract class and all actual coords must From 636b97b58f21bbc4d91fe6796a82a1a03e1b5b45 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 1 Feb 2021 13:58:19 +0000 Subject: [PATCH 9/9] remove explicit URLs for core dev names from latest.rst (#3973) --- docs/iris/src/whatsnew/latest.rst | 9 +++------ docs/iris/src/whatsnew/latest.rst.template | 5 +++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 0b1b8db1b6..3cdf5fe691 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -84,18 +84,15 @@ This document explains the changes made to Iris for this release .. comment - What's new author names (@github name) in alphabetical order: + Whatsnew author names (@github name) in alphabetical order. Note that, + core dev names are automatically included by the common_links.inc: -.. _@bjlittle: https://github.com/bjlittle .. _@gcaria: https://github.com/gcaria .. _@MHBalsmeier: https://github.com/MHBalsmeier -.. _@pelson: https://github.com/pelson -.. _@rcomer: https://github.com/rcomer -.. _@trexfeathers: https://github.com/trexfeathers .. comment - What's new resources in alphabetical order: + Whatsnew resources in alphabetical order: .. _abstract base class: https://docs.python.org/3/library/abc.html .. _GitHub: https://github.com/SciTools/iris/issues/new/choose diff --git a/docs/iris/src/whatsnew/latest.rst.template b/docs/iris/src/whatsnew/latest.rst.template index 75a1f8cd76..0992a5c9bc 100644 --- a/docs/iris/src/whatsnew/latest.rst.template +++ b/docs/iris/src/whatsnew/latest.rst.template @@ -83,12 +83,13 @@ This document explains the changes made to Iris for this release .. comment - What's new author names (@github name) in alphabetical order: + Whatsnew author names (@github name) in alphabetical order. Note that, + core dev names are automatically included by the common_links.inc: .. comment - What's new resources in alphabetical order: + Whatsnew resources in alphabetical order: .. _GitHub: https://github.com/SciTools/iris/issues/new/choose