Repository: ESMValGroup/ESMValCore

Issue:
`climate_statistics` with period `hourly`
It would be great if `climate_statistics` supported the period `hourly`. This would be very useful to create diurnal cycle plots.

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/ESMValGroup/ESMValCore/pull/2068?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ESMValGroup) Report
> Merging [#2068](https://app.codecov.io/gh/ESMValGroup/ESMValCore/pull/2068?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ESMValGroup) (ede5329) into [main](https://app.codecov.io/gh/ESMValGroup/ESMValCore/commit/83b12a233b9bd3ae3dc37540db9ac07d14e3b613?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ESMValGroup) (83b12a2) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##             main    #2068   +/-   ##
=======================================
  Coverage   92.79%   92.80%           
=======================================
  Files         236      236           
  Lines       12485    12489    +4     
=======================================
+ Hits        11586    11590    +4     
  Misses        899      899           
```


| [Impacted Files](https://app.codecov.io/gh/ESMValGroup/ESMValCore/pull/2068?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ESMValGroup) | Coverage Δ | |
|---|---|---|
| [esmvalcore/preprocessor/\_time.py](https://app.codecov.io/gh/ESMValGroup/ESMValCore/pull/2068?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ESMValGroup#diff-ZXNtdmFsY29yZS9wcmVwcm9jZXNzb3IvX3RpbWUucHk=) | `97.72% <100.00%> (+0.02%)` | :arrow_up: |



Patch:
diff --git a/conda-linux-64.lock b/conda-linux-64.lock
index f5d65d84a..b14aa7cfe 100644
--- a/conda-linux-64.lock
+++ b/conda-linux-64.lock
@@ -1,6 +1,6 @@
 # Generated by conda-lock.
 # platform: linux-64
-# input_hash: a129f6503ab0709f6a6d72bb9ec4a539c6fbb437842ba32fbed8b7f43f1ebc61
+# input_hash: 90193d2d9bc96fcccd18272eb4cdc8829f7bb39fe2a6bed47d51d07778135585
 @EXPLICIT
 https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81
 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2023.5.7-hbcca054_0.conda#f5c65075fc34438d5b456c7f3f5ab695
@@ -27,9 +27,9 @@ https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#
 https://conda.anaconda.org/conda-forge/linux-64/binutils-2.39-hdd6e379_1.conda#1276c18b0a562739185dbf5bd14b57b2
 https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.39-h5fc0e48_13.conda#7f25a524665e4e2f8a5f86522f8d0e31
 https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-12.2.0-h65d4601_19.tar.bz2#e4c94f80aef025c17ab0828cd85ef535
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.8.17-hd590300_0.conda#ce14366bd45135ab26ee66f8f422dbf2
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.8.19-hd590300_0.conda#81bd50906818d08c2f98d6d9f94cbd02
 https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h7f98852_4.tar.bz2#a1fd65c7ccbf10880423d82bca54eb54
-https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.19.0-hd590300_0.conda#a1955e8668061427d9e8fbc1be979f4e
+https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.19.1-hd590300_0.conda#e8c18d865be43e2fb3f7a145b6adf1f5
 https://conda.anaconda.org/conda-forge/linux-64/freexl-1.0.6-h166bdaf_1.tar.bz2#897e772a157faf3330d72dd291486f62
 https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2#ac7bc6a654f8f41b352b38f4051135f8
 https://conda.anaconda.org/conda-forge/linux-64/geos-3.11.2-hcb278e6_0.conda#3b8e364995e3575e57960d29c1e5ab14
@@ -41,7 +41,7 @@ https://conda.anaconda.org/conda-forge/linux-64/icu-72.1-hcb278e6_0.conda#7c8d20
 https://conda.anaconda.org/conda-forge/linux-64/json-c-0.16-hc379101_0.tar.bz2#0e2bca6857cb73acec30387fef7c3142
 https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2#30186d27e2c9fa62b45fb1476b7200e3
 https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h27087fc_0.tar.bz2#76bbff344f0134279f225174e9064c8f
-https://conda.anaconda.org/conda-forge/linux-64/libabseil-20230125.0-cxx17_hcb278e6_1.conda#a1c65f83198fd8d0328c0a6482c6f31b
+https://conda.anaconda.org/conda-forge/linux-64/libabseil-20230125.2-cxx17_h59595ed_2.conda#f67106643beadfc737b94ca0bfd6d8e3
 https://conda.anaconda.org/conda-forge/linux-64/libaec-1.0.6-hcb278e6_1.conda#0f683578378cddb223e7fd24f785ab2a
 https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.0.9-h166bdaf_8.tar.bz2#9194c9bf9428035a05352d031462eae4
 https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2#c965a5aa0d5c1c37ffc62dff36e28400
@@ -65,14 +65,15 @@ https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.9.4-hcb278e6_0.conda#318
 https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-h516909a_1000.tar.bz2#bb14fcb13341b81d5eb386423b9d2bac
 https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.3-h27087fc_1.tar.bz2#4acfc691e64342b9dae57cf2adc63238
 https://conda.anaconda.org/conda-forge/linux-64/nspr-4.35-h27087fc_0.conda#da0ec11a6454ae19bff5b02ed881a2b1
-https://conda.anaconda.org/conda-forge/linux-64/openssl-3.1.0-hd590300_3.conda#8f24d371ed9efb3f0b0de383fb81d51c
+https://conda.anaconda.org/conda-forge/linux-64/openssl-3.1.1-hd590300_0.conda#92c4e93948f2d7ba3e82ded3f6cbff31
 https://conda.anaconda.org/conda-forge/linux-64/pixman-0.40.0-h36c2ea0_0.tar.bz2#660e72c82f2e75a6b3fe6a6e75c79f19
 https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-h36c2ea0_1001.tar.bz2#22dad4df6e8630e8dff2428f6f6a7036
-https://conda.anaconda.org/conda-forge/linux-64/re2-2023.02.02-hcb278e6_0.conda#ecfc7890f1bd597ba55fbda1396f46fe
+https://conda.anaconda.org/conda-forge/linux-64/rdma-core-28.9-h59595ed_1.conda#aeffb7c06b5f65e55e6c637408dc4100
+https://conda.anaconda.org/conda-forge/linux-64/re2-2023.03.02-h8c504da_0.conda#206f8fa808748f6e90599c3368a1114e
 https://conda.anaconda.org/conda-forge/linux-64/snappy-1.1.10-h9fff704_0.conda#e6d228cd0bb74a51dd18f5bfce0b4115
 https://conda.anaconda.org/conda-forge/linux-64/tzcode-2023c-h0b41bf4_0.conda#0c0533894f21c3d35697cb8378d390e2
 https://conda.anaconda.org/conda-forge/linux-64/xorg-kbproto-1.0.7-h7f98852_1002.tar.bz2#4b230e8381279d76131116660f5a241a
-https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.0.10-h7f98852_0.tar.bz2#d6b0b50b49eccfe0be0373be628be0f3
+https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.1-hd590300_0.conda#b462a33c0be1421532f28bfe8f4a7514
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.11-hd590300_0.conda#2c80dc38fface310c9bd81b17037fee5
 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.3-h7f98852_0.tar.bz2#be93aabceefa2fac576e971aef407908
 https://conda.anaconda.org/conda-forge/linux-64/xorg-renderproto-0.11.1-h7f98852_1002.tar.bz2#06feff3d2634e3097ce2fe681474b534
@@ -81,10 +82,10 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-xproto-7.0.31-h7f98852_1007
 https://conda.anaconda.org/conda-forge/linux-64/xxhash-0.8.1-h0b41bf4_0.conda#e9c3bcf0e0c719431abec8ca447eee27
 https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.6-h166bdaf_0.tar.bz2#2161070d867d1b1204ea749c8eec4ef0
 https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2#4cb3ad778ec2d5a7acbdf254eb1c42ae
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.5.26-h71eb795_0.conda#70278f50e7ba57780d11c109990ccf43
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.16-h4f47f36_6.conda#c7ae9d66db65e9b4d2711fcd1d299d2a
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.9-h4f47f36_1.conda#1441036145a468558c1d8a43549c54ff
-https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.14-h4f47f36_6.conda#bfe5eb2ea272ee67531af7e99db7aa19
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.5.26-hf677bf3_1.conda#7d00f2b22493e28400fdbea8dc110790
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.16-hbad4bc6_7.conda#d58359b64c6d1256c07eeaee753159e3
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.9-hbad4bc6_2.conda#eee3831810e132b6caf45f03c3428363
+https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.14-hbad4bc6_7.conda#916c2a86bf786aab811a60990f7538ed
 https://conda.anaconda.org/conda-forge/linux-64/expat-2.5.0-hcb278e6_1.conda#8b9b5aca60558d02ddaa09d599e55920
 https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-11.3.0-hab1b70f_19.tar.bz2#89ac16d36e66ccb9ca5d34c9217e5799
 https://conda.anaconda.org/conda-forge/linux-64/glog-0.6.0-h6f12383_0.tar.bz2#b31f3565cb84435407594e548a2fb7b2
@@ -100,7 +101,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-3.21.12-h3eb15da_0.c
 https://conda.anaconda.org/conda-forge/linux-64/librttopo-1.1.0-h0d5128d_13.conda#e1d6139ff0500977a760567a4bec1ce9
 https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.42.0-h2797004_0.conda#fdaae20a1cf7cd62130a0973190a31b7
 https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.10.0-hf14f497_3.tar.bz2#d85acad4b47dff4e3def14a769a97906
-https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.13-h7f98852_1004.tar.bz2#b3653fdc58d03face9724f602218a904
+https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda#33277193f5b92bad9fdd230eb700929c
 https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.10.4-hfdac1af_0.conda#241845899caff54ac1d2b3102ad988cf
 https://conda.anaconda.org/conda-forge/linux-64/libzip-1.9.2-hc929e4a_1.tar.bz2#5b122b50e738c4be5c3f2899f010d7cf
 https://conda.anaconda.org/conda-forge/linux-64/pandoc-2.19.2-h32600fe_2.conda#326f46f36d15c44cff5f81d505cb717f
@@ -108,12 +109,12 @@ https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.40-hc3806b6_0.tar.bz2#6
 https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda#47d31b792659ce70f470b5c82fdfb7a4
 https://conda.anaconda.org/conda-forge/linux-64/s2n-1.3.44-h06160fa_0.conda#968cb0fca1249fe9778876201dd2b828
 https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.12-h27826a3_0.tar.bz2#5b8c42eb62e9fc961af70bdd6a26e168
-https://conda.anaconda.org/conda-forge/linux-64/ucx-1.14.0-h3484d09_2.conda#0e5585e36c9c2698ed0c833852d25bdf
-https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.3-hd9c2040_1000.tar.bz2#9e856f78d5c80d5a78f61e72d1d473a3
+https://conda.anaconda.org/conda-forge/linux-64/ucx-1.14.1-hf587318_1.conda#bf57ddc06bb57b616b4b612a99be8bd9
+https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.conda#93ee23f12bc2e684548181256edd2cf6
 https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.4-h9c3ff4c_1.tar.bz2#21743a8d2ea0c8cfbbf8fe489b0347df
 https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-h166bdaf_4.tar.bz2#4b11e365c0275b808be78b30f904e295
 https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.2-h3eb15da_6.conda#6b63daed8feeca47be78f323e793d555
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.13.21-h2c99d58_4.conda#23acfe3a190cb2ae4f450946e42693b7
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.13.21-h9fef7b8_5.conda#0e64949e8f740ceeb9f1d6255f314ab2
 https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.4-h0f2a231_0.conda#876286b5941933a0f558777e57d883cc
 https://conda.anaconda.org/conda-forge/linux-64/boost-cpp-1.78.0-h6582d0a_3.conda#d3c3c7698d0b878aab1b86db95407c8e
 https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.0.9-h166bdaf_8.tar.bz2#e5613f2bc717e9945840ff474419b8e4
@@ -125,8 +126,8 @@ https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-11.3.0-hab1b70
 https://conda.anaconda.org/conda-forge/linux-64/krb5-1.20.1-h81ceb04_0.conda#89a41adce7106749573d883b2f657d78
 https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.6.2-h3d51595_0.conda#9f915b4adeb9dcfd450b9ad238e2db4c
 https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-16_linux64_openblas.tar.bz2#20bae26d0a1db73f758fc3754cab4719
-https://conda.anaconda.org/conda-forge/linux-64/libglib-2.76.2-hebfc3b9_0.conda#db1d4a1dfc04f3eab50d97551850759a
-https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.54.2-hcf146ea_0.conda#c28b07a1a15d4d238e7d744877038e39
+https://conda.anaconda.org/conda-forge/linux-64/libglib-2.76.3-hebfc3b9_0.conda#a64f11b244b2c112cd3fa1cbe9493999
+https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.54.2-hb20ce57_2.conda#2d6c2c90dd7805816bd78d80977b61d6
 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-16_linux64_openblas.tar.bz2#955d993f41f9354bf753d29864ea20ad
 https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.18.1-h8fd135c_1.conda#a62fdab22023982131b5f21afdb9a6c8
 https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.5.0-ha587672_6.conda#4e5ee4b062c21519efbee7e2ae608748
@@ -136,13 +137,13 @@ https://conda.anaconda.org/conda-forge/linux-64/orc-1.8.3-hfdbbad2_0.conda#8aafd
 https://conda.anaconda.org/conda-forge/linux-64/python-3.11.3-h2755cc3_0_cpython.conda#37005ea5f68df6a8a381b70cf4d4a160
 https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.42.0-h2c6b66d_0.conda#1192f6ec654a5bc4ee1d64bdc4a3e5cc
 https://conda.anaconda.org/conda-forge/linux-64/udunits2-2.2.28-hc3e0081_0.tar.bz2#d4c341e0379c31e9e781d4f204726867
-https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.4-h0b41bf4_0.conda#ea8fbfeb976ac49cbeb594e985393514
+https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.4-h8ee46fc_1.conda#52d09ea80a42c0466214609ef0a2d62d
 https://conda.anaconda.org/conda-forge/noarch/alabaster-0.7.13-pyhd8ed1ab_0.conda#06006184e203b61d3525f90de394471e
 https://conda.anaconda.org/conda-forge/linux-64/antlr-python-runtime-4.7.2-py311h38be061_1003.tar.bz2#0ab8f8f0cae99343907fe68cda11baea
 https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-hd4edc92_1.tar.bz2#6c72ec3e660a51736913ef6ea68c454b
 https://conda.anaconda.org/conda-forge/noarch/attrs-23.1.0-pyh71513ae_1.conda#3edfead7cedd1ab4400a6c588f3e75f8
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.2.20-h69ce273_6.conda#3d385ad19987badc21279c2db0be9bc0
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.7.7-h7b8353a_3.conda#54406a17b5343a228e8ccc8f3ea85e6a
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.2.20-hb4b372c_7.conda#7eb8e72640ac21ce4e7d26e873a21cbe
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.7.7-h2632f9a_4.conda#f4cd59b8e2ac740faded0f75aa965a71
 https://conda.anaconda.org/conda-forge/noarch/backcall-0.2.0-pyh9f0ad1d_0.tar.bz2#6006a6d08a3fa99268a2681c7fb55213
 https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_3.conda#54ca2e08b3220c148a1d8329c2678e02
 https://conda.anaconda.org/conda-forge/linux-64/backports.zoneinfo-0.2.1-py311h38be061_7.tar.bz2#ec62b3c5b953cb610f5e2b09cd776caf
@@ -156,7 +157,7 @@ https://conda.anaconda.org/conda-forge/noarch/cloudpickle-2.2.1-pyhd8ed1ab_0.con
 https://conda.anaconda.org/conda-forge/noarch/codespell-2.2.4-pyhd8ed1ab_0.conda#27996543252c93207e54bb35daf80998
 https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2#3faab06a954c2a04039983f2c4a50d99
 https://conda.anaconda.org/conda-forge/noarch/cycler-0.11.0-pyhd8ed1ab_0.tar.bz2#a50559fad0affdbb33729a68669ca1cb
-https://conda.anaconda.org/conda-forge/linux-64/cython-0.29.34-py311hcafe171_0.conda#40f0f00037a8d660de9744fc274b85b5
+https://conda.anaconda.org/conda-forge/linux-64/cython-0.29.35-py311hb755f60_0.conda#17f4738a1ca6155a63d2a0cbd3e4a8b1
 https://conda.anaconda.org/conda-forge/noarch/decorator-5.1.1-pyhd8ed1ab_0.tar.bz2#43afe5ab04e35e17ba28649471dd7364
 https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2#961b3a227b437d82ad7054484cfa71b2
 https://conda.anaconda.org/conda-forge/noarch/dill-0.3.6-pyhd8ed1ab_1.tar.bz2#88c82ca702197fff8a5e87619707556b
@@ -185,7 +186,7 @@ https://conda.anaconda.org/conda-forge/noarch/itsdangerous-2.1.2-pyhd8ed1ab_0.ta
 https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.4-py311h4dd048b_1.tar.bz2#46d451f575392c01dc193069bd89766d
 https://conda.anaconda.org/conda-forge/linux-64/lazy-object-proxy-1.9.0-py311h2582759_0.conda#07745544b144855ed4514a4cf0aadd74
 https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.15-haa2dc70_1.conda#980d8aca0bc23ca73fa8caa3e7c84c28
-https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.1.0-h409715c_0.conda#179baac82cb5f97e036a26e70cc6647e
+https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.1.2-h409715c_0.conda#50c873c9660ed116707ae15b663928d8
 https://conda.anaconda.org/conda-forge/linux-64/libkml-1.3.0-h37653c0_1015.tar.bz2#37d3747dd24d604f63d2610910576e63
 https://conda.anaconda.org/conda-forge/linux-64/libpq-15.3-hbcd7760_0.conda#e945f0fd2471f9b51b32819c1ea83577
 https://conda.anaconda.org/conda-forge/linux-64/libwebp-1.3.0-hb47c5f0_0.conda#9cfd7ad6e1539ca1ad172083586b3301
@@ -220,7 +221,7 @@ https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.0.9-pyhd8ed1ab_0.tar.b
 https://conda.anaconda.org/conda-forge/linux-64/pyrsistent-0.19.3-py311h2582759_0.conda#e53876b66dcc4ba8a0afa63cd8502ac3
 https://conda.anaconda.org/conda-forge/noarch/pyshp-2.3.1-pyhd8ed1ab_0.tar.bz2#92a889dc236a5197612bc85bee6d7174
 https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2#2a7de29fb590ca14b5243c4c812c8025
-https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.16.3-pyhd8ed1ab_0.conda#7aa330a4d88b7ab891a42c39d5d2e742
+https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.17.1-pyhd8ed1ab_0.conda#dd4f393d857e9283eef2442234bd05e3
 https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2023.3-pyhd8ed1ab_0.conda#2590495f608a63625e165915fb4e2e34
 https://conda.anaconda.org/conda-forge/linux-64/python-xxhash-3.2.0-py311h2582759_0.conda#dfcc3e6e30d6ec2b2bb416fcd8ff4dc1
 https://conda.anaconda.org/conda-forge/noarch/pytz-2023.3-pyhd8ed1ab_0.conda#d3076b483092a435832603243567bc31
@@ -250,9 +251,9 @@ https://conda.anaconda.org/conda-forge/noarch/toolz-0.12.0-pyhd8ed1ab_0.tar.bz2#
 https://conda.anaconda.org/conda-forge/linux-64/tornado-6.3.2-py311h459d7ec_0.conda#12b1c374ee90a1aa11ea921858394dc8
 https://conda.anaconda.org/conda-forge/noarch/traitlets-5.9.0-pyhd8ed1ab_0.conda#d0b4f5c87cd35ac3fb3d47b223263a64
 https://conda.anaconda.org/conda-forge/noarch/types-pkg_resources-0.1.3-pyhd8ed1ab_0.tar.bz2#82e2a50752d5a512ab88e66778f9a7a8
-https://conda.anaconda.org/conda-forge/noarch/types-pyyaml-6.0.12.9-pyhd8ed1ab_0.conda#0c0c5edec27d8284bf75023737f74823
+https://conda.anaconda.org/conda-forge/noarch/types-pyyaml-6.0.12.10-pyhd8ed1ab_0.conda#21ab63073cea60bc584a889ae8d765d8
 https://conda.anaconda.org/conda-forge/noarch/types-urllib3-1.26.25.13-pyhd8ed1ab_0.conda#9a73576dfe2f764c431347b9dc35a3fc
-https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.5.0-pyha770c72_0.conda#43e7d9e50261fb11deb76e17d8431aac
+https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.6.2-pyha770c72_0.conda#5a4a270e5a3f93846d6bade2f71fa440
 https://conda.anaconda.org/conda-forge/linux-64/ujson-5.7.0-py311hcafe171_0.conda#ec3960b6d13bb60aad9c67f42a801720
 https://conda.anaconda.org/conda-forge/noarch/untokenize-0.1.1-py_0.tar.bz2#1447ead40f2a01733a9c8dfc32988375
 https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-py_1.tar.bz2#3563be4c5611a44210d9ba0c16113136
@@ -265,25 +266,25 @@ https://conda.anaconda.org/conda-forge/noarch/xyzservices-2023.5.0-pyhd8ed1ab_1.
 https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_0.conda#cf30c2c15b82aacb07f9c09e28ff2275
 https://conda.anaconda.org/conda-forge/noarch/zipp-3.15.0-pyhd8ed1ab_0.conda#13018819ca8f5b7cc675a8faf1f5fedf
 https://conda.anaconda.org/conda-forge/noarch/accessible-pygments-0.0.4-pyhd8ed1ab_0.conda#46a2e6e3dfa718ce3492018d5a110dd6
-https://conda.anaconda.org/conda-forge/noarch/asgiref-3.6.0-pyhd8ed1ab_0.conda#4437fc8d76835df622027fe9ae7da997
+https://conda.anaconda.org/conda-forge/noarch/asgiref-3.7.2-pyhd8ed1ab_0.conda#596932155bf88bb6837141550cb721b0
 https://conda.anaconda.org/conda-forge/linux-64/astroid-2.15.5-py311h38be061_0.conda#bc99014b1cb98221bc4a0f4dc889d26f
 https://conda.anaconda.org/conda-forge/noarch/asttokens-2.2.1-pyhd8ed1ab_0.conda#bf7f54dd0f25c3f06ecb82a07341841a
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.6.26-h2c7c9e7_6.conda#2b6d931ac31ded1e20e86e7940dd507e
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.8.6-h3a1964a_15.conda#68761b2007b4e94cc85be77440becbb8
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.6.27-he072965_1.conda#2c7406414796748e53bd7d7c6349711d
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.8.11-h2282364_1.conda#11a4a996699d883ebda0894faa71bbfc
 https://conda.anaconda.org/conda-forge/noarch/babel-2.12.1-pyhd8ed1ab_1.conda#ac432e732804a81ddcf29c92ead57cde
 https://conda.anaconda.org/conda-forge/noarch/backports.functools_lru_cache-1.6.4-pyhd8ed1ab_0.tar.bz2#c5b3edc62d6309088f4970b3eaaa65a6
 https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.12.2-pyha770c72_0.conda#a362ff7d976217f8fa78c0f1c4f59717
 https://conda.anaconda.org/conda-forge/noarch/bleach-6.0.0-pyhd8ed1ab_0.conda#d48b143d01385872a88ef8417e96c30e
-https://conda.anaconda.org/conda-forge/linux-64/cairo-1.16.0-h35add3b_1015.conda#0c944213e40c9e4aa32292776b9c6903
-https://conda.anaconda.org/conda-forge/noarch/cattrs-22.2.0-pyhd8ed1ab_0.tar.bz2#5dacf4d924ae284579288e378b1f5943
+https://conda.anaconda.org/conda-forge/linux-64/cairo-1.16.0-hbbf8b49_1016.conda#c1dd96500b9b1a75e9e511931f415cbc
+https://conda.anaconda.org/conda-forge/noarch/cattrs-23.1.1-pyhd8ed1ab_0.conda#48e964fec505c7a4d9d131be9e048b40
 https://conda.anaconda.org/conda-forge/linux-64/cffi-1.15.1-py311h409f033_3.conda#9025d0786dbbe4bc91fd8e85502decce
 https://conda.anaconda.org/conda-forge/linux-64/cfitsio-4.2.0-hd9d235c_0.conda#8c57a9adbafd87f5eff842abde599cb4
 https://conda.anaconda.org/conda-forge/linux-64/cftime-1.6.2-py311h4c7f6c3_1.tar.bz2#c7e54004ffd03f8db0a58ab949f2a00b
 https://conda.anaconda.org/conda-forge/noarch/click-plugins-1.1.1-py_0.tar.bz2#4fd2c6b53934bd7d96d1f3fdaf99b79f
 https://conda.anaconda.org/conda-forge/noarch/cligj-0.7.2-pyhd8ed1ab_1.tar.bz2#a29b7c141d6b2de4bb67788a5f107734
 https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.0.7-py311ha3edf6b_0.conda#e7548e7f58965a2fe97a95950a5fedc6
-https://conda.anaconda.org/conda-forge/linux-64/coverage-7.2.5-py311h459d7ec_0.conda#bdcfd13b348de652ca9ac3458575ad9e
-https://conda.anaconda.org/conda-forge/linux-64/curl-8.1.0-h409715c_0.conda#1b20d441e4ca11629f6409d31a597d2e
+https://conda.anaconda.org/conda-forge/linux-64/coverage-7.2.7-py311h459d7ec_0.conda#3c2c65575c28b23afc5e4ff721a2fc9f
+https://conda.anaconda.org/conda-forge/linux-64/curl-8.1.2-h409715c_0.conda#9f88cfb15b7d08b25880b138f91e0eb4
 https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.5.2-hf52228f_0.conda#6b3b19e359824b97df7145c8c878c8be
 https://conda.anaconda.org/conda-forge/linux-64/cytoolz-0.12.0-py311hd4cff14_1.tar.bz2#21523141b35484b1edafba962c6ea883
 https://conda.anaconda.org/conda-forge/noarch/docformatter-1.7.1-pyhd8ed1ab_0.conda#0a896b51ac939fd6c19f106c55509cd6
@@ -302,7 +303,7 @@ https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.2-pyhd8ed1ab_1.tar.bz2#
 https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.2.2-pyhd8ed1ab_0.tar.bz2#243f63592c8e449f40cd42eb5cf32f40
 https://conda.anaconda.org/conda-forge/noarch/latexcodec-2.0.1-pyh9f0ad1d_0.tar.bz2#8d67904973263afd2985ba56aa2d6bb4
 https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-hfa28ad5_6.conda#ef06bee47510a7f5db3c2297a51d6ce2
-https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.10.0-hac9eb74_0.conda#87b80c1f708cabd4c876735529f47852
+https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.10.1-hac9eb74_1.conda#582726eb344fe32d6de102b748067f8d
 https://conda.anaconda.org/conda-forge/noarch/logilab-common-1.7.3-py_0.tar.bz2#6eafcdf39a7eb90b6d951cfff59e8d3b
 https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.6-pyhd8ed1ab_0.tar.bz2#b21613793fcc81d944c76c9f2864a7de
 https://conda.anaconda.org/conda-forge/noarch/munch-3.0.0-pyhd8ed1ab_0.conda#3d5fa8396d78c916d51fb1c6cda24945
@@ -311,7 +312,7 @@ https://conda.anaconda.org/conda-forge/noarch/nested-lookup-0.2.25-pyhd8ed1ab_1.
 https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.8.0-pyhd8ed1ab_0.conda#2a75b296096adabbabadd5e9782e5fcc
 https://conda.anaconda.org/conda-forge/noarch/partd-1.4.0-pyhd8ed1ab_0.conda#721dab5803ea92ce02ddc4ee50aa0c48
 https://conda.anaconda.org/conda-forge/noarch/pexpect-4.8.0-pyh1a96a4e_2.tar.bz2#330448ce4403cc74990ac07c555942a1
-https://conda.anaconda.org/conda-forge/linux-64/pillow-9.5.0-py311h573f0d3_0.conda#fd7a5518f850b660c526bac9da0f7b81
+https://conda.anaconda.org/conda-forge/linux-64/pillow-9.5.0-py311h0b84326_1.conda#6be2190fdbf26a6c1d3356a54d955237
 https://conda.anaconda.org/conda-forge/noarch/pip-23.1.2-pyhd8ed1ab_0.conda#7288da0d36821349cf1126e8670292df
 https://conda.anaconda.org/conda-forge/linux-64/postgresql-15.3-h814edd5_0.conda#c72622dbd4193522a0b568886b63048d
 https://conda.anaconda.org/conda-forge/linux-64/proj-9.2.0-h8ffa02c_0.conda#8b9dcfabec5c6bcac98e89889fffa64e
@@ -319,16 +320,16 @@ https://conda.anaconda.org/conda-forge/noarch/pydocstyle-6.3.0-pyhd8ed1ab_0.cond
 https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2#dd999d1cc9f79e67dbb855c8924c7984
 https://conda.anaconda.org/conda-forge/linux-64/shapely-2.0.1-py311h54d622a_1.conda#a894c65b48676c4973e9ee8b59bceb9e
 https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.2.1-pyhd8ed1ab_0.tar.bz2#7234c9eefff659501cd2fe0d2ede4d48
-https://conda.anaconda.org/conda-forge/noarch/types-requests-2.30.0.0-pyhd8ed1ab_0.conda#1ab2e9a47f24fac257f88956828f1956
-https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.5.0-hd8ed1ab_0.conda#b3c594fde1a80a1fc3eb9cc4a5dfe392
+https://conda.anaconda.org/conda-forge/noarch/types-requests-2.31.0.1-pyhd8ed1ab_0.conda#d4edae6cf0af5332243c2d995f5e8745
+https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.6.2-hd8ed1ab_0.conda#f676553904bb8f7c1dfe71c9db0d9ba7
 https://conda.anaconda.org/conda-forge/noarch/url-normalize-1.4.3-pyhd8ed1ab_0.tar.bz2#7c4076e494f0efe76705154ac9302ba6
+https://conda.anaconda.org/conda-forge/noarch/urllib3-2.0.2-pyhd8ed1ab_0.conda#81a763f3c64fe6d5f32e033b0325265d
 https://conda.anaconda.org/conda-forge/linux-64/xerces-c-3.2.4-h8d71039_2.conda#6d5edbe22b07abae2ea0a9065ef6be12
 https://conda.anaconda.org/conda-forge/noarch/yamale-4.0.4-pyh6c4a22f_0.tar.bz2#cc9f59f147740d88679bf1bd94dbe588
-https://conda.anaconda.org/conda-forge/noarch/yamllint-1.31.0-pyhd8ed1ab_0.conda#89dd502af7f0506db73403f1d9827458
+https://conda.anaconda.org/conda-forge/noarch/yamllint-1.32.0-pyhd8ed1ab_0.conda#6d2425548b0293a225ca4febd80feaa3
 https://conda.anaconda.org/conda-forge/noarch/yapf-0.33.0-pyhd8ed1ab_1.conda#ea4867f364b3f7f48c67643028c7f4c6
-https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.2.8-h0933b68_4.conda#faaf3ceca7a487d6a3257233cfcf31aa
-https://conda.anaconda.org/conda-forge/linux-64/brotlipy-0.7.0-py311hd4cff14_1005.tar.bz2#9bdac7084ecfc08338bae1b976535724
-https://conda.anaconda.org/conda-forge/linux-64/cf-units-3.2.0-py311hcb2cf0a_0.conda#063a136fefba5ea0df36f9de0a9d0e30
+https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.3.0-hcb5a9b2_2.conda#e32991aa713aafc13ae31869d44e04ad
+https://conda.anaconda.org/conda-forge/linux-64/cf-units-3.2.0-py311h1f0f07a_0.conda#43a71a823583d75308eaf3a06c8f150b
 https://conda.anaconda.org/conda-forge/linux-64/compilers-1.5.2-ha770c72_0.conda#f95226244ee1c487cf53272f971323f4
 https://conda.anaconda.org/conda-forge/linux-64/cryptography-40.0.2-py311h9b4c7bb_0.conda#4df4df92db0b9168c11b72460baec870
 https://conda.anaconda.org/conda-forge/noarch/django-4.2.1-pyhd8ed1ab_0.conda#4ab40953038f831d7ab1884b794990e6
@@ -342,42 +343,48 @@ https://conda.anaconda.org/conda-forge/linux-64/kealib-1.5.1-h3845be2_3.conda#f3
 https://conda.anaconda.org/conda-forge/linux-64/libnetcdf-4.9.2-nompi_hdf9a29f_104.conda#283aeeef04e2a01445156c9c2d5c4fa0
 https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.0.1-h7d1ca68_25.conda#c5ff4b64ee24804cad5ddb4239267b09
 https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.7.1-py311h8597a09_0.conda#70c3b734ffe82c16b6d121aaa11929a8
-https://conda.anaconda.org/conda-forge/linux-64/pandas-2.0.1-py311h320fe9a_1.conda#37f841a3140999c4735f7d8091072bea
+https://conda.anaconda.org/conda-forge/linux-64/pandas-2.0.2-py311h320fe9a_0.conda#509769b430266dc5c2f6a3eab0f23164
 https://conda.anaconda.org/conda-forge/noarch/platformdirs-3.5.1-pyhd8ed1ab_0.conda#e2be672aece1f060adf7154f76531a35
 https://conda.anaconda.org/conda-forge/linux-64/poppler-23.05.0-hd18248d_1.conda#09e0de1aa7330fe697eed76eaeef666d
 https://conda.anaconda.org/conda-forge/noarch/pybtex-0.24.0-pyhd8ed1ab_2.tar.bz2#2099b86a7399c44c0c61cdb6de6915ba
 https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.5.0-py311h1850bce_1.conda#572159a946b809df471b11db4995c708
 https://conda.anaconda.org/conda-forge/noarch/pytest-7.3.1-pyhd8ed1ab_0.conda#547c7de697ec99b494a28ddde185b5a4
 https://conda.anaconda.org/conda-forge/noarch/rdflib-6.3.2-pyhd8ed1ab_0.conda#ef37f754e65328229ecf4488b5909b8d
-https://conda.anaconda.org/conda-forge/noarch/requirements-detector-1.2.1-pyhd8ed1ab_0.conda#d8a474b03b51adf0389cef33295e7164
+https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda#a30144e4156cdbb236f99ebb49828f8b
+https://conda.anaconda.org/conda-forge/noarch/requirements-detector-1.2.2-pyhd8ed1ab_0.conda#6626918380d99292df110f3c91b6e5ec
 https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda#e7df0fdd404616638df5ece6e69ba7af
 https://conda.anaconda.org/conda-forge/linux-64/tiledb-2.13.2-hd532e3d_0.conda#6d97164f19dbd27575ef1899b02dc1e0
 https://conda.anaconda.org/conda-forge/linux-64/ukkonen-1.0.1-py311h4dd048b_3.tar.bz2#dbfea4376856bf7bd2121e719cf816e5
 https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.6-pyhd8ed1ab_0.conda#078979d33523cb477bd1916ce41aacc9
-https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.19.9-h85076f6_5.conda#ae2291fd00be4e8c6e1f37a53c6c2a63
+https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.20.2-he0fdcb3_0.conda#3d9577a30f0e61331216b381925aa3e3
 https://conda.anaconda.org/conda-forge/noarch/bokeh-3.1.1-pyhd8ed1ab_0.conda#07401431ba1c7fae695814ae3528312a
-https://conda.anaconda.org/conda-forge/noarch/dask-core-2023.5.0-pyhd8ed1ab_0.conda#03ed2d040648a5ba1063bf1cb0d87b78
+https://conda.anaconda.org/conda-forge/noarch/dask-core-2023.5.1-pyhd8ed1ab_0.conda#b90a2dec6d308d71649dbe58dc32c337
 https://conda.anaconda.org/conda-forge/noarch/flake8-polyfill-1.0.2-py_0.tar.bz2#a53db35e3d07f0af2eccd59c2a00bffe
 https://conda.anaconda.org/conda-forge/noarch/identify-2.5.24-pyhd8ed1ab_0.conda#a4085ab0562d5081a9333435837b538a
 https://conda.anaconda.org/conda-forge/linux-64/jupyter_core-5.3.0-py311h38be061_0.conda#1dd43a18a75d59206019e2a2a28555e5
 https://conda.anaconda.org/conda-forge/linux-64/libgdal-3.7.0-he76be6c_0.conda#2e2c887d9a55b287982c1bf3d7013fb1
 https://conda.anaconda.org/conda-forge/noarch/nc-time-axis-1.4.1-pyhd8ed1ab_0.tar.bz2#281b58948bf60a2582de9e548bcc5369
-https://conda.anaconda.org/conda-forge/linux-64/netcdf-fortran-4.6.0-nompi_h3142581_105.conda#2c716d4127a8386b3c8946170ac75aca
+https://conda.anaconda.org/conda-forge/linux-64/netcdf-fortran-4.6.1-nompi_h4f3791c_100.conda#405c5b3ad4ef53eb0d93043b54206dd7
 https://conda.anaconda.org/conda-forge/linux-64/netcdf4-1.6.3-nompi_py311h1717473_102.conda#d3b4d3ed2f3188d27d43e2c95d0dc2ab
 https://conda.anaconda.org/conda-forge/linux-64/pango-1.50.14-heaa33ce_1.conda#cde553e0e32389e26595db4eacf859eb
+https://conda.anaconda.org/conda-forge/noarch/pooch-1.7.0-pyha770c72_3.conda#5936894aade8240c867d292aa0d980c6
 https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.38-pyha770c72_0.conda#59ba1bf8ea558751a0d391249a248765
 https://conda.anaconda.org/conda-forge/noarch/pylint-2.17.4-pyhd8ed1ab_0.conda#a9d97fe4617aba393d90ea81576b6b46
 https://conda.anaconda.org/conda-forge/noarch/pyopenssl-23.1.1-pyhd8ed1ab_0.conda#0b34aa3ab7e7ccb1765a03dd9ed29938
-https://conda.anaconda.org/conda-forge/noarch/pytest-cov-4.0.0-pyhd8ed1ab_0.tar.bz2#c9e3f8bfdb9bfc34aa1836a6ed4b25d7
+https://conda.anaconda.org/conda-forge/noarch/pytest-cov-4.1.0-pyhd8ed1ab_0.conda#06eb685a3a0b146347a58dda979485da
 https://conda.anaconda.org/conda-forge/noarch/pytest-env-0.8.1-pyhd8ed1ab_0.conda#56466a4061d4c1150f6fe52235019bf8
-https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-2.0.4-pyhd8ed1ab_0.tar.bz2#7ac02a65917993d38ca1bfd7b87208e4
+https://conda.anaconda.org/conda-forge/noarch/pytest-metadata-3.0.0-pyhd8ed1ab_0.conda#00df44ef00d7a49b0815ffbeaf7bfeb2
 https://conda.anaconda.org/conda-forge/noarch/pytest-mock-3.10.0-pyhd8ed1ab_0.tar.bz2#db93caa9fe182f0cd20291aeb22f57ac
 https://conda.anaconda.org/conda-forge/noarch/pytest-mypy-0.8.0-pyhd8ed1ab_0.tar.bz2#4e81c96e5f875c09e5b9f999035b9d8e
 https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.3.1-pyhd8ed1ab_0.conda#816073bb54ef59f33f0f26c14f88311b
+https://conda.anaconda.org/conda-forge/noarch/requests-cache-1.0.1-pyhd8ed1ab_0.conda#43ec7b3627237e5fe23413e314e8ba4c
+https://conda.anaconda.org/conda-forge/noarch/sphinx-7.0.1-pyhd8ed1ab_0.conda#51a8d037b28276b4f68263e890e0f35b
 https://conda.anaconda.org/conda-forge/noarch/virtualenv-20.23.0-pyhd8ed1ab_0.conda#a920e114c4c2ced2280e266da65ab5e6
 https://conda.anaconda.org/conda-forge/noarch/xarray-2023.5.0-pyhd8ed1ab_0.conda#254b5553bed6adf404ac09fa07cb54da
-https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.10.57-hf40e4db_10.conda#4d6f98c3b182f6ef121116055ed211d0
+https://conda.anaconda.org/conda-forge/noarch/autodocsumm-0.2.6-pyhd8ed1ab_0.tar.bz2#4409dd7e06a62c3b2aa9e96782c49c6d
+https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.10.57-h059227d_13.conda#16eac1f53808f188a44cb0dcb59b109b
 https://conda.anaconda.org/conda-forge/noarch/cf_xarray-0.8.1-pyhd8ed1ab_0.conda#3c3cdc59ff9c8e1f1c9d6d3c362ce778
+https://conda.anaconda.org/conda-forge/noarch/distributed-2023.5.1-pyhd8ed1ab_0.conda#517e6d85a48d94b1f5997377df53b896
 https://conda.anaconda.org/conda-forge/linux-64/esmf-8.4.2-nompi_h20110ff_0.conda#11f5169aeff54ad7277476be8ba19ff7
 https://conda.anaconda.org/conda-forge/linux-64/gdal-3.7.0-py311h6122507_0.conda#f45a9655a8fd47b8187ae00972f4b4e7
 https://conda.anaconda.org/conda-forge/linux-64/gtk2-2.24.33-h90689f9_2.tar.bz2#957a0255ab58aaf394a91725d73ab422
@@ -388,40 +395,31 @@ https://conda.anaconda.org/conda-forge/noarch/nbformat-5.8.0-pyhd8ed1ab_0.conda#
 https://conda.anaconda.org/conda-forge/noarch/pep8-naming-0.10.0-pyh9f0ad1d_0.tar.bz2#b3c5536e4f9f58a4b16adb6f1e11732d
 https://conda.anaconda.org/conda-forge/noarch/pre-commit-3.3.2-pyha770c72_0.conda#dbb0111b18ea5c9983fb8db0aef6000b
 https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.38-hd8ed1ab_0.conda#45b74f64d8808eda7e6f6e6b1d641fd2
+https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.13.3-pyhd8ed1ab_0.conda#07aca5f2dea315dcc16680d6891e9056
 https://conda.anaconda.org/conda-forge/noarch/pylint-plugin-utils-0.7-pyhd8ed1ab_0.tar.bz2#1657976383aee04dbb3ae3bdf654bb58
 https://conda.anaconda.org/conda-forge/noarch/pytest-html-3.2.0-pyhd8ed1ab_1.tar.bz2#d5c7a941dfbceaab4b172a56d7918eb0
 https://conda.anaconda.org/conda-forge/linux-64/python-stratify-0.3.0-py311h1f0f07a_0.conda#3a00b1b08d8c01b1a3bfa686b9152df2
-https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.15-pyhd8ed1ab_0.conda#27db656619a55d727eaf5a6ece3d2fd6
-https://conda.anaconda.org/conda-forge/noarch/distributed-2023.5.0-pyhd8ed1ab_0.conda#85a1eddc3a535eca09d9d7e107cc473b
-https://conda.anaconda.org/conda-forge/noarch/esmpy-8.4.2-pyhc1e730c_0.conda#6ae4f054e7d2ceb6720e753d853a6d47
+https://conda.anaconda.org/conda-forge/linux-64/scipy-1.10.1-py311h64a7726_3.conda#a01a3a7428e770db5a0c8c7ab5fce7f7
+https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.21.1-py311hd88b842_1.conda#f19feb9440890ccb806a367ea9ae0654
+https://conda.anaconda.org/conda-forge/noarch/esgf-pyclient-0.3.1-pyh1a96a4e_2.tar.bz2#64068564a9c2932bf30e9b4ec567927d
+https://conda.anaconda.org/conda-forge/noarch/esmpy-8.4.2-pyhc1e730c_1.conda#4067029ad6872d49f6d43c05dd1f51a9
 https://conda.anaconda.org/conda-forge/linux-64/fiona-1.9.4-py311hbac4ec9_0.conda#1d3445f5f7fa002a1c149c405376f012
 https://conda.anaconda.org/conda-forge/linux-64/graphviz-8.0.5-h28d9a01_0.conda#597e2d0e1c6bc2e4457714ff479fe142
 https://conda.anaconda.org/conda-forge/noarch/ipython-8.13.2-pyh41d4057_0.conda#e8563c13eee80a5f1c7bdfc2a1b20077
-https://conda.anaconda.org/conda-forge/linux-64/libarrow-12.0.0-h1cdf7b0_1_cpu.conda#d49cc7294fcc4c037007b0ca3ee8106e
-https://conda.anaconda.org/conda-forge/noarch/nbclient-0.7.4-pyhd8ed1ab_0.conda#f7aa15f77d29b11caa1df1eb15383c59
+https://conda.anaconda.org/conda-forge/linux-64/libarrow-12.0.0-h96638e8_3_cpu.conda#af13034e5449c23c6a6cde65ba501700
+https://conda.anaconda.org/conda-forge/noarch/nbclient-0.8.0-pyhd8ed1ab_0.conda#e78da91cf428faaf05701ce8cc8f2f9b
+https://conda.anaconda.org/conda-forge/noarch/py-cordex-0.5.2-pyhd8ed1ab_0.conda#1de2b64c99d5b4e8413823047c0dbf7c
 https://conda.anaconda.org/conda-forge/noarch/pylint-celery-0.3-py_1.tar.bz2#e29456a611a62d3f26105a2f9c68f759
 https://conda.anaconda.org/conda-forge/noarch/pylint-django-2.5.3-pyhd8ed1ab_0.tar.bz2#00d8853fb1f87195722ea6a582cc9b56
 https://conda.anaconda.org/conda-forge/noarch/pylint-flask-0.6-py_0.tar.bz2#5a9afd3d0a61b08d59eed70fab859c1b
-https://conda.anaconda.org/conda-forge/noarch/requests-2.29.0-pyhd8ed1ab_0.conda#5fa992d972fbccfc069161805122cb8d
-https://conda.anaconda.org/conda-forge/linux-64/arrow-cpp-12.0.0-ha770c72_1_cpu.conda#fa8433ce4405ff14a7fd41d40d8fb4ac
+https://conda.anaconda.org/conda-forge/noarch/iris-3.6.0-pyha770c72_0.conda#a213bee1c2fab6f99c4f66ef5f0b34f9
 https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.4.0-pyhd8ed1ab_0.conda#4456e6030a8309bdad57569b0170b6a3
-https://conda.anaconda.org/conda-forge/noarch/pooch-1.7.0-pyha770c72_3.conda#5936894aade8240c867d292aa0d980c6
-https://conda.anaconda.org/conda-forge/noarch/prospector-1.9.0-pyhd8ed1ab_0.conda#fd44c73df4ca9d47dc9860626b0d1f7b
+https://conda.anaconda.org/conda-forge/noarch/prospector-1.10.2-pyhd8ed1ab_0.conda#2c536985982f7e531df8d640f554008a
+https://conda.anaconda.org/conda-forge/linux-64/pyarrow-12.0.0-py311hdf9aeb4_3_cpu.conda#6f4dc84eb09c954e6e9190b029c88442
 https://conda.anaconda.org/conda-forge/linux-64/pydot-1.4.2-py311h38be061_3.tar.bz2#64a77de29fde80aef5013ddf5e62a564
-https://conda.anaconda.org/conda-forge/noarch/requests-cache-1.0.1-pyhd8ed1ab_0.conda#43ec7b3627237e5fe23413e314e8ba4c
-https://conda.anaconda.org/conda-forge/noarch/sphinx-7.0.1-pyhd8ed1ab_0.conda#51a8d037b28276b4f68263e890e0f35b
-https://conda.anaconda.org/conda-forge/noarch/autodocsumm-0.2.6-pyhd8ed1ab_0.tar.bz2#4409dd7e06a62c3b2aa9e96782c49c6d
-https://conda.anaconda.org/conda-forge/noarch/esgf-pyclient-0.3.1-pyh1a96a4e_2.tar.bz2#64068564a9c2932bf30e9b4ec567927d
+https://conda.anaconda.org/conda-forge/noarch/dask-2023.5.1-pyhd8ed1ab_0.conda#0ff65293034744118a4534397686d2e0
 https://conda.anaconda.org/conda-forge/noarch/nbconvert-pandoc-7.4.0-pyhd8ed1ab_0.conda#127c702e1b1eff595be82bc6a78cfce0
-https://conda.anaconda.org/conda-forge/noarch/parquet-cpp-1.5.1-2.tar.bz2#79a5f78c42817594ae016a7896521a97
 https://conda.anaconda.org/conda-forge/noarch/prov-2.0.0-pyhd3deb0d_0.tar.bz2#aa9b3ad140f6c0668c646f32e20ccf82
-https://conda.anaconda.org/conda-forge/noarch/py-cordex-0.5.2-pyhd8ed1ab_0.conda#1de2b64c99d5b4e8413823047c0dbf7c
-https://conda.anaconda.org/conda-forge/noarch/pydata-sphinx-theme-0.13.3-pyhd8ed1ab_0.conda#07aca5f2dea315dcc16680d6891e9056
-https://conda.anaconda.org/conda-forge/linux-64/scipy-1.10.1-py311h64a7726_3.conda#a01a3a7428e770db5a0c8c7ab5fce7f7
-https://conda.anaconda.org/conda-forge/linux-64/cartopy-0.21.1-py311hd88b842_1.conda#f19feb9440890ccb806a367ea9ae0654
+https://conda.anaconda.org/conda-forge/noarch/iris-esmf-regrid-0.7.0-pyhd8ed1ab_0.conda#de82eb8d09362babacafe6b7e27752ac
 https://conda.anaconda.org/conda-forge/noarch/nbconvert-7.4.0-pyhd8ed1ab_0.conda#a86727968b41c20dd3d73b91632e77dc
-https://conda.anaconda.org/conda-forge/linux-64/pyarrow-12.0.0-py311hdf9aeb4_1_cpu.conda#48584404ab97094a2653ff2538694991
-https://conda.anaconda.org/conda-forge/noarch/dask-2023.5.0-pyhd8ed1ab_0.conda#01400176e0f139c44f96d5fa69b1ada7
-https://conda.anaconda.org/conda-forge/noarch/iris-3.6.0-pyhd8ed1ab_0.conda#27668030298891f1494c741d17be2328
-https://conda.anaconda.org/conda-forge/noarch/nbsphinx-0.9.1-pyhd8ed1ab_0.conda#a0b8b3d9eb22da29279a90883dcd5962
-https://conda.anaconda.org/conda-forge/noarch/iris-esmf-regrid-0.6.0-pyhd8ed1ab_0.conda#f5820b0c256099df65864d629ffac2f7
+https://conda.anaconda.org/conda-forge/noarch/nbsphinx-0.9.2-pyhd8ed1ab_0.conda#d1212b423fdd10d2da59601385561ff7
diff --git a/doc/quickstart/find_data.rst b/doc/quickstart/find_data.rst
index fcd27a66a..9f9963928 100644
--- a/doc/quickstart/find_data.rst
+++ b/doc/quickstart/find_data.rst
@@ -370,33 +370,15 @@ This is useful for diagnostics that do not support input data in UGRID format
 if you want to use the built-in :ref:`unstructured_nearest scheme <built-in
 regridding schemes>` regridding scheme.
 
-Similar to any other fix, the ICON fix allows the use of :ref:`extra
-facets<extra_facets>`.
-By default, the file :download:`icon-mappings.yml
-</../esmvalcore/config/extra_facets/icon-mappings.yml>` is used for that
-purpose.
-For some variables, extra facets are necessary; otherwise ESMValTool cannot
-read them properly.
-Supported keys for extra facets are:
-
-============= ============================= =================================
-Key           Description                   Default value if not specified
-============= ============================= =================================
-``latitude``  Standard name of the latitude ``latitude``
-              coordinate in the raw input
-              file
-``longitude`` Standard name of the          ``longitude``
-              longitude coordinate in the
-              raw input file
-``raw_name``  Variable name of the          CMOR variable name of the
-              variable in the raw input     corresponding variable
-              file
-``ugrid``     Automatic UGRIDization of     ``True``
-              the input data
-``var_type``  Variable type of the          No default (needs to be specified
-              variable in the raw input     in extra facets or recipe if
-              file                          default DRS is used)
-============= ============================= =================================
+For 3D ICON variables, ESMValCore tries to add the pressure level information
+(from the variables `pfull` and `phalf`) and/or altitude information (from the
+variables `zg` and `zghalf`) to the preprocessed output files.
+If neither of these variables are available in the input files, it is possible
+to specify the location of files that include the corresponding `zg` or
+`zghalf` variables with the facets ``zg_file`` and/or ``zghalf_file`` in the
+recipe or the extra facets.
+The paths to these files can be specified absolute or relative (to
+``auxiliary_data_dir`` as defined in the :ref:`user configuration file`).
 
 .. hint::
 
@@ -404,6 +386,9 @@ Key           Description                   Default value if not specified
    native ICON data, you need to specify the name of the vertical coordinate
    (e.g., ``coordinate: air_pressure``) since native ICON output usually
    provides a 3D air pressure field instead of a simple 1D vertical coordinate.
+   This also works if your files only contain altitude information (in this
+   case, the US standard atmosphere is used to convert between altitude and
+   pressure levels; see :ref:`Vertical interpolation` for details).
    Example:
 
    .. code-block:: yaml
@@ -415,6 +400,46 @@ Key           Description                   Default value if not specified
           scheme: linear
           coordinate: air_pressure
 
+Similar to any other fix, the ICON fix allows the use of :ref:`extra
+facets<extra_facets>`.
+By default, the file :download:`icon-mappings.yml
+</../esmvalcore/config/extra_facets/icon-mappings.yml>` is used for that
+purpose.
+For some variables, extra facets are necessary; otherwise ESMValTool cannot
+read them properly.
+Supported keys for extra facets are:
+
+=================== ================================ ===================================
+Key                 Description                      Default value if not specified
+=================== ================================ ===================================
+``latitude``        Standard name of the latitude    ``latitude``
+                    coordinate in the raw input
+                    file
+``longitude``       Standard name of the             ``longitude``
+                    longitude coordinate in the
+                    raw input file
+``raw_name``        Variable name of the             CMOR variable name of the
+                    variable in the raw input        corresponding variable
+                    file
+``ugrid``           Automatic UGRIDization of        ``True``
+                    the input data
+``var_type``        Variable type of the             No default (needs to be specified
+                    variable in the raw input        in extra facets or recipe if
+                    file                             default DRS is used)
+``zg_file``         Absolute or relative (to         If possible, use `zg` variable
+                    ``auxiliary_data_dir`` defined   provided by the raw input file
+                    in the
+                    :ref:`user configuration file`)
+                    path to the input file that
+                    contains `zg`
+``zghalf_file``     Absolute or relative (to         If possible, use `zghalf` variable
+                    ``auxiliary_data_dir`` defined   provided by the raw input file
+                    in the
+                    :ref:`user configuration file`)
+                    path to the input file that
+                    contains `zghalf`
+=================== ================================ ===================================
+
 .. hint::
 
    In order to read cell area files (``areacella`` and ``areacello``), one
diff --git a/doc/recipe/preprocessor.rst b/doc/recipe/preprocessor.rst
index 8ebabc40e..3a087bef8 100644
--- a/doc/recipe/preprocessor.rst
+++ b/doc/recipe/preprocessor.rst
@@ -1381,16 +1381,17 @@ See also :func:`esmvalcore.preprocessor.decadal_statistics`.
 ----------------------
 
 This function produces statistics for the whole dataset. It can produce scalars
-(if the full period is chosen) or daily, monthly or seasonal statistics.
+(if the full period is chosen) or hourly, daily, monthly or seasonal
+statistics.
 
 Parameters:
     * operator: operation to apply. Accepted values are 'mean', 'median',
       'std_dev', 'min', 'max', 'sum' and 'rms'. Default is 'mean'
 
     * period: define the granularity of the statistics: get values for the
-      full period, for each month or day of year.
+      full period, for each month, day of year or hour of day.
       Available periods: 'full', 'season', 'seasonal', 'monthly', 'month',
-      'mon', 'daily', 'day'. Default is 'full'
+      'mon', 'daily', 'day', 'hourly', 'hour', 'hr'. Default is 'full'
 
     * seasons: if period 'seasonal' or 'season' allows to set custom seasons.
       Default is '[DJF, MAM, JJA, SON]'
@@ -1502,14 +1503,14 @@ See also :func:`esmvalcore.preprocessor.resample_hours`.
 ----------------------
 
 This function computes the anomalies for the whole dataset. It can compute
-anomalies from the full, seasonal, monthly and daily climatologies. Optionally
-standardized anomalies can be calculated.
+anomalies from the full, seasonal, monthly, daily and hourly climatologies.
+Optionally standardized anomalies can be calculated.
 
 Parameters:
     * period: define the granularity of the climatology to use:
-      full period, seasonal, monthly or daily.
+      full period, seasonal, monthly, daily or hourly.
       Available periods: 'full', 'season', 'seasonal', 'monthly', 'month',
-      'mon', 'daily', 'day'. Default is 'full'
+      'mon', 'daily', 'day', 'hourly', 'hour', 'hr'. Default is 'full'
     * reference: Time slice to use as the reference to compute the climatology
       on. Can be 'null' to use the full cube or a dictionary with the
       parameters from extract_time_. Default is null
@@ -1886,7 +1887,7 @@ that can be:
 * left closed ``[z_min, z_max)``, in which the extracted range includes ``z_min`` but not ``z_max``.
 * right closed ``(z_min, z_max]``, in which the extracted range includes ``z_max`` but not ``z_min``.
 
-The extraction is performed by applying a constraint on the coordinate values, without any kind of interpolation. 
+The extraction is performed by applying a constraint on the coordinate values, without any kind of interpolation.
 
 This function takes four arguments:
 
diff --git a/environment.yml b/environment.yml
index 26809991e..6861f0429 100644
--- a/environment.yml
+++ b/environment.yml
@@ -22,6 +22,7 @@ dependencies:
   - iris-esmf-regrid >=0.6.0  # to work with latest esmpy
   - isodate
   - jinja2
+  - libnetcdf!=4.9.1 # to avoid hdf5 warnings
   - nc-time-axis
   - nested-lookup
   - netcdf4
@@ -35,7 +36,7 @@ dependencies:
   - py-cordex
   - pybtex
   - python>=3.9
-  - python-stratify
+  - python-stratify>=0.3
   - pyyaml
   - requests
   - scipy>=1.6
diff --git a/esmvalcore/cmor/_fixes/icon/_base_fixes.py b/esmvalcore/cmor/_fixes/icon/_base_fixes.py
index f2502ac0a..0590c2026 100644
--- a/esmvalcore/cmor/_fixes/icon/_base_fixes.py
+++ b/esmvalcore/cmor/_fixes/icon/_base_fixes.py
@@ -1,6 +1,7 @@
 """Fix base classes for ICON on-the-fly CMORizer."""
 
 import logging
+import os
 import warnings
 from datetime import datetime
 from pathlib import Path
@@ -178,6 +179,75 @@ class IconFix(NativeDatasetFix):
 
         return (node_lat, node_lon)
 
+    def _get_path_from_facet(self, facet, description=None):
+        """Try to get path from facet."""
+        if description is None:
+            description = 'File'
+        path = Path(os.path.expandvars(self.extra_facets[facet])).expanduser()
+        if not path.is_file():
+            new_path = self.session['auxiliary_data_dir'] / path
+            if not new_path.is_file():
+                raise FileNotFoundError(
+                    f"{description} '{path}' given by facet '{facet}' does "
+                    f"not exist (specify a valid absolute path or a path "
+                    f"relative to the auxiliary_data_dir "
+                    f"'{self.session['auxiliary_data_dir']}')"
+                )
+            path = new_path
+        return path
+
+    def add_additional_cubes(self, cubes):
+        """Add additional user-defined cubes to list of cubes (in-place).
+
+        An example use case is adding a vertical coordinate (e.g., `zg`) to the
+        dataset if the vertical coordinate data is stored in a separate ICON
+        output file.
+
+        Currently, the following cubes can be added:
+
+        - `zg` (`geometric_height_at_full_level_center`) from facet `zg_file`.
+          This can be used as vertical coordinate.
+        - `zghalf` (`geometric_height_at_half_level_center`) from facet
+          `zghalf_file`. This can be used as bounds for the vertical
+          coordinate.
+
+        Note
+        ----
+        Files can be specified as absolute or relative (to
+        ``auxiliary_data_dir`` as defined in the :ref:`user configuration
+        file`) paths.
+
+        Parameters
+        ----------
+        cubes: iris.cube.CubeList
+            Input cubes which will be modified in place.
+
+        Returns
+        -------
+        iris.cube.CubeList
+            Modified cubes. The cubes are modified in place; they are just
+            returned out of convenience for easy access.
+
+        Raises
+        ------
+        InputFilesNotFound
+            A specified file does not exist.
+
+        """
+        facets_to_consider = [
+            'zg_file',
+            'zghalf_file',
+        ]
+        for facet in facets_to_consider:
+            if facet not in self.extra_facets:
+                continue
+            path_to_add = self._get_path_from_facet(facet)
+            logger.debug("Adding cubes from %s", path_to_add)
+            new_cubes = self._load_cubes(path_to_add)
+            cubes.extend(new_cubes)
+
+        return cubes
+
     def get_horizontal_grid(self, cube):
         """Get copy of ICON horizontal grid from global attribute of cube.
 
@@ -318,6 +388,14 @@ class IconFix(NativeDatasetFix):
                 category=UserWarning,
                 module='iris',
             )
+            warnings.filterwarnings(
+                'ignore',
+                message="Failed to create 'height' dimension coordinate: The "
+                        "'height' DimCoord bounds array must be strictly "
+                        "monotonic.",
+                category=UserWarning,
+                module='iris',
+            )
             cubes = iris.load(str(path))
         return cubes
 
diff --git a/esmvalcore/cmor/_fixes/icon/icon.py b/esmvalcore/cmor/_fixes/icon/icon.py
index bef17d5a9..aef9c9c14 100644
--- a/esmvalcore/cmor/_fixes/icon/icon.py
+++ b/esmvalcore/cmor/_fixes/icon/icon.py
@@ -24,6 +24,7 @@ class AllVars(IconFix):
 
     def fix_metadata(self, cubes):
         """Fix metadata."""
+        cubes = self.add_additional_cubes(cubes)
         cube = self.get_cube(cubes)
 
         # Fix time
@@ -152,46 +153,66 @@ class AllVars(IconFix):
             f"'{self.vardef.short_name}', cube and other cubes in file do not "
             f"contain it")
 
+    def _get_z_coord(self, cubes, points_name, bounds_name=None):
+        """Get z-coordinate without metadata (reversed)."""
+        points_cube = iris.util.reverse(
+            cubes.extract_cube(NameConstraint(var_name=points_name)),
+            'height',
+        )
+        points = points_cube.core_data()
+
+        # Get bounds if possible
+        if bounds_name is not None:
+            bounds_cube = iris.util.reverse(
+                cubes.extract_cube(NameConstraint(var_name=bounds_name)),
+                'height',
+            )
+            bounds = bounds_cube.core_data()
+            bounds = da.stack(
+                (bounds[..., :-1, :], bounds[..., 1:, :]), axis=-1
+            )
+        else:
+            bounds = None
+
+        z_coord = AuxCoord(
+            points,
+            bounds=bounds,
+            units=points_cube.units,
+        )
+        return z_coord
+
     def _fix_height(self, cube, cubes):
         """Fix height coordinate of cube."""
         # Reverse entire cube along height axis so that index 0 is surface
         # level
         cube = iris.util.reverse(cube, 'height')
 
-        # Add air_pressure coordinate if possible
-        # (make sure to also reverse pressure cubes)
+        # If possible, extract reversed air_pressure coordinate from list of
+        # cubes and add it to cube
+        # Note: pfull/phalf have dimensions (time, height, spatial_dim)
         if cubes.extract(NameConstraint(var_name='pfull')):
-            plev_points_cube = iris.util.reverse(
-                cubes.extract_cube(NameConstraint(var_name='pfull')),
-                'height',
-            )
-            air_pressure_points = plev_points_cube.core_data()
-
-            # Get bounds from half levels and reshape array
             if cubes.extract(NameConstraint(var_name='phalf')):
-                plev_bounds_cube = iris.util.reverse(
-                    cubes.extract_cube(NameConstraint(var_name='phalf')),
-                    'height',
-                )
-                air_pressure_bounds = plev_bounds_cube.core_data()
-                air_pressure_bounds = da.stack(
-                    (air_pressure_bounds[:, :-1], air_pressure_bounds[:, 1:]),
-                    axis=-1)
+                phalf = 'phalf'
             else:
-                air_pressure_bounds = None
-
-            # Setup air pressure coordinate with correct metadata and add to
-            # cube
-            air_pressure_coord = AuxCoord(
-                air_pressure_points,
-                bounds=air_pressure_bounds,
-                var_name='plev',
-                standard_name='air_pressure',
-                long_name='pressure',
-                units=plev_points_cube.units,
-                attributes={'positive': 'down'},
-            )
-            cube.add_aux_coord(air_pressure_coord, np.arange(cube.ndim))
+                phalf = None
+            plev_coord = self._get_z_coord(cubes, 'pfull', bounds_name=phalf)
+            self.fix_plev_metadata(cube, plev_coord)
+            cube.add_aux_coord(plev_coord, np.arange(cube.ndim))
+
+        # If possible, extract reversed altitude coordinate from list of cubes
+        # and add it to cube
+        # Note: zg/zghalf have dimensions (height, spatial_dim)
+        if cubes.extract(NameConstraint(var_name='zg')):
+            if cubes.extract(NameConstraint(var_name='zghalf')):
+                zghalf = 'zghalf'
+            else:
+                zghalf = None
+            alt_coord = self._get_z_coord(cubes, 'zg', bounds_name=zghalf)
+            self.fix_alt16_metadata(cube, alt_coord)
+
+            # Altitude coordinate only spans height and spatial dimensions (no
+            # time) -> these are always the last two dimensions in the cube
+            cube.add_aux_coord(alt_coord, np.arange(cube.ndim)[-2:])
 
         # Fix metadata
         z_coord = cube.coord('height')
diff --git a/esmvalcore/cmor/_fixes/native_datasets.py b/esmvalcore/cmor/_fixes/native_datasets.py
index 72e9561c6..1e14e5c95 100644
--- a/esmvalcore/cmor/_fixes/native_datasets.py
+++ b/esmvalcore/cmor/_fixes/native_datasets.py
@@ -231,6 +231,36 @@ class NativeDatasetFix(Fix):
         coord.long_name = 'time'
         return coord
 
+    @staticmethod
+    def fix_alt16_metadata(cube, coord=None):
+        """Fix metadata of alt16 coordinate (in-place).
+
+        Parameters
+        ----------
+        cube: iris.cube.Cube
+            Input cube.
+        coord: str or iris.coords.Coord or None, optional (default: None)
+            Coordinate for which metadata will be fixed in-place. If ``None``,
+            assume the coordinate's name is `altitude`.
+
+        Returns
+        -------
+        iris.coords.AuxCoord or iris.coords.DimCoord
+            Fixed altitude coordinate. The coordinate is altered in-place; it
+            is just returned out of convenience for easy access.
+
+        """
+        if coord is None:
+            coord = cube.coord('altitude')
+        elif isinstance(coord, str):
+            coord = cube.coord(coord)
+        coord.var_name = 'alt16'
+        coord.standard_name = 'altitude'
+        coord.long_name = 'altitude'
+        coord.convert_units('m')
+        coord.attributes['positive'] = 'up'
+        return coord
+
     @staticmethod
     def fix_height_metadata(cube, coord=None):
         """Fix metadata of height coordinate (in-place).
diff --git a/esmvalcore/preprocessor/_other.py b/esmvalcore/preprocessor/_other.py
index 36bbccafb..14594ba7c 100644
--- a/esmvalcore/preprocessor/_other.py
+++ b/esmvalcore/preprocessor/_other.py
@@ -4,6 +4,7 @@ import logging
 from collections import defaultdict
 
 import dask.array as da
+import numpy as np
 
 logger = logging.getLogger(__name__)
 
@@ -71,3 +72,16 @@ def _group_products(products, by_key):
 
     grouped = _groupby(products, keyfunc=grouper)
     return grouped.items()
+
+
+def get_array_module(*args):
+    """Return the best matching array module.
+
+    If at least one of the arguments is a :class:`dask.array.Array` object,
+    the :mod:`dask.array` module is returned. In all other cases the
+    :mod:`numpy` module is returned.
+    """
+    for arg in args:
+        if isinstance(arg, da.Array):
+            return da
+    return np
diff --git a/esmvalcore/preprocessor/_regrid.py b/esmvalcore/preprocessor/_regrid.py
index a14abfbe6..4972a7ce8 100644
--- a/esmvalcore/preprocessor/_regrid.py
+++ b/esmvalcore/preprocessor/_regrid.py
@@ -14,13 +14,13 @@ from typing import Dict
 import iris
 import numpy as np
 import stratify
-from dask import array as da
 from geopy.geocoders import Nominatim
 from iris.analysis import AreaWeighted, Linear, Nearest, UnstructuredNearest
 from iris.util import broadcast_to_shape
 
 from ..cmor._fixes.shared import add_altitude_from_plev, add_plev_from_altitude
 from ..cmor.table import CMOR_TABLES
+from ._other import get_array_module
 from ._regrid_esmpy import ESMF_REGRID_METHODS
 from ._regrid_esmpy import regrid as esmpy_regrid
 from ._supplementary_vars import add_ancillary_variable, add_cell_measure
@@ -797,22 +797,19 @@ def _vertical_interpolate(cube, src_levels, levels, interpolation,
                                               cube.coord_dims(src_levels))
 
     # force mask onto data as nan's
-    cube.data = da.ma.filled(cube.core_data(), np.nan)
+    npx = get_array_module(cube.core_data())
+    data = npx.ma.filled(cube.core_data(), np.nan)
 
     # Now perform the actual vertical interpolation.
     new_data = stratify.interpolate(levels,
                                     src_levels_broadcast,
-                                    cube.core_data(),
+                                    data,
                                     axis=z_axis,
                                     interpolation=interpolation,
                                     extrapolation=extrapolation)
 
     # Calculate the mask based on the any NaN values in the interpolated data.
-    mask = np.isnan(new_data)
-
-    if np.any(mask):
-        # Ensure that the data is masked appropriately.
-        new_data = np.ma.array(new_data, mask=mask, fill_value=_MDI)
+    new_data = npx.ma.masked_where(npx.isnan(new_data), new_data)
 
     # Construct the resulting cube with the interpolated data.
     return _create_cube(cube, new_data, src_levels, levels.astype(float))
diff --git a/esmvalcore/preprocessor/_time.py b/esmvalcore/preprocessor/_time.py
index 0ba0a953f..5de60f35a 100644
--- a/esmvalcore/preprocessor/_time.py
+++ b/esmvalcore/preprocessor/_time.py
@@ -704,30 +704,31 @@ def climate_statistics(cube,
     """Compute climate statistics with the specified granularity.
 
     Computes statistics for the whole dataset. It is possible to get them for
-    the full period or with the data grouped by day, month or season
+    the full period or with the data grouped by hour, day, month or season.
 
     Parameters
     ----------
     cube: iris.cube.Cube
-        input cube.
+        Input cube.
 
     operator: str, optional
         Select operator to apply.
         Available operators: 'mean', 'median', 'std_dev', 'sum', 'min',
-        'max', 'rms'
+        'max', 'rms'.
 
     period: str, optional
         Period to compute the statistic over.
         Available periods: 'full', 'season', 'seasonal', 'monthly', 'month',
-        'mon', 'daily', 'day'
+        'mon', 'daily', 'day', 'hourly', 'hour', 'hr'.
 
     seasons: list or tuple of str, optional
-        Seasons to use if needed. Defaults to ('DJF', 'MAM', 'JJA', 'SON')
+        Seasons to use if needed. Defaults to ('DJF', 'MAM', 'JJA', 'SON').
 
     Returns
     -------
     iris.cube.Cube
-        Monthly statistics cube
+        Climate statistics cube.
+
     """
     original_dtype = cube.dtype
     period = period.lower()
@@ -775,34 +776,35 @@ def anomalies(cube,
               seasons=('DJF', 'MAM', 'JJA', 'SON')):
     """Compute anomalies using a mean with the specified granularity.
 
-    Computes anomalies based on daily, monthly, seasonal or yearly means for
-    the full available period
+    Computes anomalies based on hourly, daily, monthly, seasonal or yearly
+    means for the full available period.
 
     Parameters
     ----------
     cube: iris.cube.Cube
-        input cube.
+        Input cube.
 
     period: str
         Period to compute the statistic over.
         Available periods: 'full', 'season', 'seasonal', 'monthly', 'month',
-        'mon', 'daily', 'day'
+        'mon', 'daily', 'day', 'hourly', 'hour', 'hr'.
 
     reference: list int, optional, default: None
         Period of time to use a reference, as needed for the 'extract_time'
-        preprocessor function
-        If None, all available data is used as a reference
+        preprocessor function. If ``None``, all available data is used as a
+        reference.
 
     standardize: bool, optional
-        If True standardized anomalies are calculated
+        If ``True`` standardized anomalies are calculated.
 
     seasons: list or tuple of str, optional
-        Seasons to use if needed. Defaults to ('DJF', 'MAM', 'JJA', 'SON')
+        Seasons to use if needed. Defaults to ('DJF', 'MAM', 'JJA', 'SON').
 
     Returns
     -------
     iris.cube.Cube
-        Anomalies cube
+        Anomalies cube.
+
     """
     if reference is None:
         reference_cube = cube
@@ -868,6 +870,10 @@ def _compute_anomalies(cube, reference, period, seasons):
 
 def _get_period_coord(cube, period, seasons):
     """Get periods."""
+    if period in ['hourly', 'hour', 'hr']:
+        if not cube.coords('hour'):
+            iris.coord_categorisation.add_hour(cube, 'time')
+        return cube.coord('hour')
     if period in ['daily', 'day']:
         if not cube.coords('day_of_year'):
             iris.coord_categorisation.add_day_of_year(cube, 'time')
diff --git a/setup.py b/setup.py
index 05d5a9ac5..5743b3f60 100755
--- a/setup.py
+++ b/setup.py
@@ -58,7 +58,7 @@ REQUIREMENTS = {
         'scipy>=1.6',
         'scitools-iris>=3.4.0',
         'shapely[vectorized]',
-        'stratify',
+        'stratify>=0.3',
         'yamale',
     ],
     # Test dependencies


Test patch:
diff --git a/tests/integration/cmor/_fixes/icon/test_icon.py b/tests/integration/cmor/_fixes/icon/test_icon.py
index ada031f39..686cf80a5 100644
--- a/tests/integration/cmor/_fixes/icon/test_icon.py
+++ b/tests/integration/cmor/_fixes/icon/test_icon.py
@@ -1,5 +1,6 @@
 """Tests for the ICON on-the-fly CMORizer."""
 from datetime import datetime
+from pathlib import Path
 from unittest import mock
 
 import iris
@@ -15,6 +16,7 @@ from esmvalcore.cmor._fixes.icon._base_fixes import IconFix
 from esmvalcore.cmor._fixes.icon.icon import AllVars, Clwvi, Siconc, Siconca
 from esmvalcore.cmor.fix import Fix
 from esmvalcore.cmor.table import CoordinateInfo, get_var_info
+from esmvalcore.config import CFG
 from esmvalcore.config._config import get_extra_facets
 from esmvalcore.dataset import Dataset
 
@@ -90,7 +92,25 @@ def cubes_2d_lat_lon_grid():
     return CubeList([cube])
 
 
-def _get_fix(mip, short_name, fix_name):
+@pytest.fixture
+def simple_unstructured_cube():
+    """Simple cube with unstructured grid."""
+    time_coord = DimCoord([0], var_name='time', standard_name='time',
+                          units='days since 1850-01-01')
+    height_coord = DimCoord([0, 1, 2], var_name='height')
+    lat_coord = AuxCoord([0.0, 1.0], var_name='lat', standard_name='latitude',
+                         long_name='latitude', units='degrees_north')
+    lon_coord = AuxCoord([0.0, 1.0], var_name='lon',
+                         standard_name='longitude', long_name='longitude',
+                         units='degrees_east')
+    cube = Cube([[[0.0, 1.0], [2.0, 3.0], [4.0, 5.0]]], var_name='ta',
+                units='K',
+                dim_coords_and_dims=[(time_coord, 0), (height_coord, 1)],
+                aux_coords_and_dims=[(lat_coord, 2), (lon_coord, 2)])
+    return cube
+
+
+def _get_fix(mip, short_name, fix_name, session=None):
     """Load a fix from esmvalcore.cmor._fixes.icon.icon."""
     dataset = Dataset(
         project='ICON',
@@ -101,26 +121,26 @@ def _get_fix(mip, short_name, fix_name):
     extra_facets = get_extra_facets(dataset, ())
     vardef = get_var_info(project='ICON', mip=mip, short_name=short_name)
     cls = getattr(esmvalcore.cmor._fixes.icon.icon, fix_name)
-    fix = cls(vardef, extra_facets=extra_facets)
+    fix = cls(vardef, extra_facets=extra_facets, session=session)
     return fix
 
 
-def get_fix(mip, short_name):
+def get_fix(mip, short_name, session=None):
     """Load a variable fix from esmvalcore.cmor._fixes.icon.icon."""
     fix_name = short_name[0].upper() + short_name[1:]
-    return _get_fix(mip, short_name, fix_name)
+    return _get_fix(mip, short_name, fix_name, session=session)
 
 
-def get_allvars_fix(mip, short_name):
+def get_allvars_fix(mip, short_name, session=None):
     """Load the AllVars fix from esmvalcore.cmor._fixes.icon.icon."""
-    return _get_fix(mip, short_name, 'AllVars')
+    return _get_fix(mip, short_name, 'AllVars', session=session)
 
 
-def fix_metadata(cubes, mip, short_name):
+def fix_metadata(cubes, mip, short_name, session=None):
     """Fix metadata of cubes."""
-    fix = get_fix(mip, short_name)
+    fix = get_fix(mip, short_name, session=session)
     cubes = fix.fix_metadata(cubes)
-    fix = get_allvars_fix(mip, short_name)
+    fix = get_allvars_fix(mip, short_name, session=session)
     cubes = fix.fix_metadata(cubes)
     return cubes
 
@@ -175,18 +195,20 @@ def check_time(cube):
     assert time.attributes == {}
 
 
-def check_height(cube, plev_has_bounds=True):
-    """Check height coordinate of cube."""
+def check_model_level_metadata(cube):
+    """Check metadata of model_level coordinate."""
     assert cube.coords('model level number', dim_coords=True)
     height = cube.coord('model level number', dim_coords=True)
     assert height.var_name == 'model_level'
     assert height.standard_name is None
     assert height.long_name == 'model level number'
     assert height.units == 'no unit'
-    np.testing.assert_array_equal(height.points, np.arange(47))
-    assert height.bounds is None
     assert height.attributes == {'positive': 'up'}
+    return height
 
+
+def check_air_pressure_metadata(cube):
+    """Check metadata of air_pressure coordinate."""
     assert cube.coords('air_pressure', dim_coords=False)
     plev = cube.coord('air_pressure', dim_coords=False)
     assert plev.var_name == 'plev'
@@ -194,6 +216,16 @@ def check_height(cube, plev_has_bounds=True):
     assert plev.long_name == 'pressure'
     assert plev.units == 'Pa'
     assert plev.attributes == {'positive': 'down'}
+    return plev
+
+
+def check_height(cube, plev_has_bounds=True):
+    """Check height coordinate of cube."""
+    height = check_model_level_metadata(cube)
+    np.testing.assert_array_equal(height.points, np.arange(47))
+    assert height.bounds is None
+
+    plev = check_air_pressure_metadata(cube)
     assert cube.coord_dims('air_pressure') == (0, 1, 2)
 
     np.testing.assert_allclose(
@@ -1553,3 +1585,168 @@ def test_get_mesh_not_cached(monkeypatch):
     monkeypatch.setattr(fix, '_create_mesh', mock.Mock())
     fix.get_mesh(cube)
     fix._create_mesh.assert_called_once_with(cube)
+
+
+# Test _get_path_from_facet
+
+
+@pytest.mark.parametrize(
+    'path,description,output',
+    [
+        ('{tmp_path}/a.nc', None, '{tmp_path}/a.nc'),
+        ('b.nc', 'Grid file', '{tmp_path}/b.nc'),
+    ],
+)
+def test_get_path_from_facet(path, description, output, tmp_path):
+    """Test fix."""
+    session = CFG.start_session('my session')
+    session['auxiliary_data_dir'] = tmp_path
+    path = path.format(tmp_path=tmp_path)
+    fix = get_allvars_fix('Amon', 'tas', session=session)
+    fix.extra_facets['test_path'] = path
+
+    # Create empty dummy file
+    output = output.format(tmp_path=tmp_path)
+    with open(output, 'w'):
+        pass
+
+    out_path = fix._get_path_from_facet('test_path', description=description)
+
+    assert isinstance(out_path, Path)
+    assert out_path == Path(output.format(tmp_path=tmp_path))
+
+
+@pytest.mark.parametrize(
+    'path,description',
+    [
+        ('{tmp_path}/a.nc', None),
+        ('b.nc', 'Grid file'),
+    ],
+)
+def test_get_path_from_facet_fail(path, description, tmp_path):
+    """Test fix."""
+    session = CFG.start_session('my session')
+    session['auxiliary_data_dir'] = tmp_path
+    path = path.format(tmp_path=tmp_path)
+    fix = get_allvars_fix('Amon', 'tas', session=session)
+    fix.extra_facets['test_path'] = path
+
+    with pytest.raises(FileNotFoundError, match=description):
+        fix._get_path_from_facet('test_path', description=description)
+
+
+# Test add_additional_cubes
+
+
+@pytest.mark.parametrize('facet', ['zg_file', 'zghalf_file'])
+@pytest.mark.parametrize('path', ['{tmp_path}/a.nc', 'a.nc'])
+def test_add_additional_cubes(path, facet, tmp_path):
+    """Test fix."""
+    session = CFG.start_session('my session')
+    session['auxiliary_data_dir'] = tmp_path
+    path = path.format(tmp_path=tmp_path)
+    fix = get_allvars_fix('Amon', 'tas', session=session)
+    fix.extra_facets[facet] = path
+
+    # Save temporary cube
+    cube = Cube(0, var_name=facet)
+    iris.save(cube, tmp_path / 'a.nc')
+
+    cubes = CubeList([])
+    new_cubes = fix.add_additional_cubes(cubes)
+
+    assert new_cubes is cubes
+    assert len(cubes) == 1
+    assert cubes[0].var_name == facet
+
+
+@pytest.mark.parametrize('facet', ['zg_file', 'zghalf_file'])
+@pytest.mark.parametrize('path', ['{tmp_path}/a.nc', 'a.nc'])
+def test_add_additional_cubes_fail(path, facet, tmp_path):
+    """Test fix."""
+    session = CFG.start_session('my session')
+    session['auxiliary_data_dir'] = tmp_path
+    path = path.format(tmp_path=tmp_path)
+    fix = get_allvars_fix('Amon', 'tas', session=session)
+    fix.extra_facets[facet] = path
+
+    cubes = CubeList([])
+    with pytest.raises(FileNotFoundError, match='File'):
+        fix.add_additional_cubes(cubes)
+
+
+# Test _fix_height
+
+
+@pytest.mark.parametrize('bounds', [True, False])
+def test_fix_height_plev(bounds, simple_unstructured_cube):
+    """Test fix."""
+    cube = simple_unstructured_cube[:, 1:, :]
+    pfull_cube = simple_unstructured_cube[:, 1:, :]
+    pfull_cube.var_name = 'pfull'
+    pfull_cube.units = 'Pa'
+    cubes = CubeList([cube, pfull_cube])
+    if bounds:
+        phalf_cube = simple_unstructured_cube.copy()
+        phalf_cube.var_name = 'phalf'
+        phalf_cube.units = 'Pa'
+        cubes.append(phalf_cube)
+    fix = get_allvars_fix('Amon', 'ta')
+
+    fixed_cube = fix._fix_height(cube, cubes)
+
+    expected_data = [[[4.0, 5.0], [2.0, 3.0]]]
+    np.testing.assert_allclose(fixed_cube.data, expected_data)
+
+    height = check_model_level_metadata(fixed_cube)
+    np.testing.assert_array_equal(height.points, [0, 1])
+    assert height.bounds is None
+
+    plev = check_air_pressure_metadata(fixed_cube)
+    assert fixed_cube.coord_dims('air_pressure') == (0, 1, 2)
+    np.testing.assert_allclose(plev.points, expected_data)
+    if bounds:
+        expected_bnds = [[[[4.0, 2.0], [5.0, 3.0]], [[2.0, 0.0], [3.0, 1.0]]]]
+        np.testing.assert_allclose(plev.bounds, expected_bnds)
+    else:
+        assert plev.bounds is None
+
+
+@pytest.mark.parametrize('bounds', [True, False])
+def test_fix_height_alt16(bounds, simple_unstructured_cube):
+    """Test fix."""
+    cube = simple_unstructured_cube[:, 1:, :]
+    zg_cube = simple_unstructured_cube[0, 1:, :]
+    zg_cube.var_name = 'zg'
+    zg_cube.units = 'm'
+    cubes = CubeList([cube, zg_cube])
+    if bounds:
+        zghalf_cube = simple_unstructured_cube[0, :, :]
+        zghalf_cube.var_name = 'zghalf'
+        zghalf_cube.units = 'm'
+        cubes.append(zghalf_cube)
+    fix = get_allvars_fix('Amon', 'ta')
+
+    fixed_cube = fix._fix_height(cube, cubes)
+
+    expected_data = [[[4.0, 5.0], [2.0, 3.0]]]
+    np.testing.assert_allclose(fixed_cube.data, expected_data)
+
+    height = check_model_level_metadata(fixed_cube)
+    np.testing.assert_array_equal(height.points, [0, 1])
+    assert height.bounds is None
+
+    assert fixed_cube.coords('altitude', dim_coords=False)
+    alt16 = fixed_cube.coord('altitude', dim_coords=False)
+    assert alt16.var_name == 'alt16'
+    assert alt16.standard_name == 'altitude'
+    assert alt16.long_name == 'altitude'
+    assert alt16.units == 'm'
+    assert alt16.attributes == {'positive': 'up'}
+    assert fixed_cube.coord_dims('altitude') == (1, 2)
+    np.testing.assert_allclose(alt16.points, expected_data[0])
+    if bounds:
+        expected_bnds = [[[4.0, 2.0], [5.0, 3.0]], [[2.0, 0.0], [3.0, 1.0]]]
+        np.testing.assert_allclose(alt16.bounds, expected_bnds)
+    else:
+        assert alt16.bounds is None
diff --git a/tests/integration/cmor/_fixes/test_native_datasets.py b/tests/integration/cmor/_fixes/test_native_datasets.py
index a48b37e95..371c2c5a2 100644
--- a/tests/integration/cmor/_fixes/test_native_datasets.py
+++ b/tests/integration/cmor/_fixes/test_native_datasets.py
@@ -41,6 +41,11 @@ def sample_cube():
         long_name='air_pressure',
         units='hPa',
     )
+    alt16_coord = AuxCoord(
+        [2.0, 4.0],
+        long_name='altitude',
+        units='km',
+    )
     height_coord = AuxCoord(
         [2.0, 4.0],
         long_name='height',
@@ -68,7 +73,8 @@ def sample_cube():
                              (plev_coord, 1),
                              (lat_coord, 2),
                              (lon_coord, 3)],
-        aux_coords_and_dims=[(height_coord, 1),
+        aux_coords_and_dims=[(alt16_coord, 1),
+                             (height_coord, 1),
                              (coord_with_bounds, 1)],
     )
     return cube
@@ -375,6 +381,46 @@ def test_fix_time_metadata_from_coord(sample_cube, fix):
     assert out_coord.bounds is None
 
 
+def test_fix_alt16_metadata(sample_cube, fix):
+    """Test ``fix_alt16_metadata``."""
+    out_coord = fix.fix_alt16_metadata(sample_cube)
+    assert out_coord is sample_cube.coord('altitude')
+    assert out_coord.standard_name == 'altitude'
+    assert out_coord.var_name == 'alt16'
+    assert out_coord.long_name == 'altitude'
+    assert out_coord.units == 'm'
+    assert out_coord.attributes['positive'] == 'up'
+    np.testing.assert_allclose(out_coord.points, [2000.0, 4000.0])
+    assert out_coord.bounds is None
+
+
+def test_fix_alt16_metadata_from_str(sample_cube, fix):
+    """Test ``fix_alt16_metadata`` from string."""
+    out_coord = fix.fix_alt16_metadata(sample_cube, coord='altitude')
+    assert out_coord is sample_cube.coord('altitude')
+    assert out_coord.standard_name == 'altitude'
+    assert out_coord.var_name == 'alt16'
+    assert out_coord.long_name == 'altitude'
+    assert out_coord.units == 'm'
+    assert out_coord.attributes['positive'] == 'up'
+    np.testing.assert_allclose(out_coord.points, [2000.0, 4000.0])
+    assert out_coord.bounds is None
+
+
+def test_fix_alt16_metadata_from_coord(sample_cube, fix):
+    """Test ``fix_alt16_metadata`` from string."""
+    coord = AuxCoord([2.0], units='m')
+    out_coord = fix.fix_alt16_metadata(sample_cube, coord=coord)
+    assert out_coord is coord
+    assert out_coord.standard_name == 'altitude'
+    assert out_coord.var_name == 'alt16'
+    assert out_coord.long_name == 'altitude'
+    assert out_coord.units == 'm'
+    assert out_coord.attributes['positive'] == 'up'
+    np.testing.assert_allclose(out_coord.points, [2.0])
+    assert out_coord.bounds is None
+
+
 def test_fix_height_metadata(sample_cube, fix):
     """Test ``fix_height_metadata``."""
     out_coord = fix.fix_height_metadata(sample_cube)
diff --git a/tests/integration/preprocessor/_regrid/test_extract_levels.py b/tests/integration/preprocessor/_regrid/test_extract_levels.py
index 8687f1922..b0869244a 100644
--- a/tests/integration/preprocessor/_regrid/test_extract_levels.py
+++ b/tests/integration/preprocessor/_regrid/test_extract_levels.py
@@ -15,6 +15,7 @@ from tests.unit.preprocessor._regrid import _make_cube, _make_vcoord
 
 
 class Test(tests.Test):
+
     def setUp(self):
         """Prepare tests."""
         shape = (3, 2, 2)
@@ -57,20 +58,52 @@ class Test(tests.Test):
         levels = [0.5, 1.5]
         scheme = 'linear'
         result = extract_levels(self.cube, levels, scheme)
-        expected = np.array([[[[2., 3.], [4., 5.]], [[6., 7.], [8., 9.]]],
-                             [[[14., 15.], [16., 17.]], [[18., 19.],
-                                                         [20., 21.]]]])
+        expected = np.ma.array([
+            [
+                [[2., 3.], [4., 5.]],
+                [[6., 7.], [8., 9.]],
+            ],
+            [
+                [[14., 15.], [16., 17.]],
+                [[18., 19.], [20., 21.]],
+            ],
+        ])
         self.assert_array_equal(result.data, expected)
         self.shape[self.z_dim] = len(levels)
         self.assertEqual(result.shape, tuple(self.shape))
 
+    def test_interpolation__linear_lazy(self):
+        levels = [0.5, 1.5]
+        scheme = 'linear'
+        cube = self.cube.copy(self.cube.lazy_data())
+        result = extract_levels(cube, levels, scheme)
+        self.assertTrue(result.has_lazy_data())
+        expected = np.ma.array([
+            [
+                [[2., 3.], [4., 5.]],
+                [[6., 7.], [8., 9.]],
+            ],
+            [
+                [[14., 15.], [16., 17.]],
+                [[18., 19.], [20., 21.]],
+            ],
+        ])
+        self.assert_array_equal(result.data, expected)
+
     def test_interpolation__nearest(self):
         levels = [0.49, 1.51]
         scheme = 'nearest'
         result = extract_levels(self.cube, levels, scheme)
-        expected = np.array([[[[0., 1.], [2., 3.]], [[8., 9.], [10., 11.]]],
-                             [[[12., 13.], [14., 15.]], [[20., 21.],
-                                                         [22., 23.]]]])
+        expected = np.ma.array([
+            [
+                [[0., 1.], [2., 3.]],
+                [[8., 9.], [10., 11.]],
+            ],
+            [
+                [[12., 13.], [14., 15.]],
+                [[20., 21.], [22., 23.]],
+            ],
+        ])
         self.assert_array_equal(result.data, expected)
         self.shape[self.z_dim] = len(levels)
         self.assertEqual(result.shape, tuple(self.shape))
diff --git a/tests/unit/preprocessor/_other/test_other.py b/tests/unit/preprocessor/_other/test_other.py
index ab98895c0..803b66d99 100644
--- a/tests/unit/preprocessor/_other/test_other.py
+++ b/tests/unit/preprocessor/_other/test_other.py
@@ -2,6 +2,7 @@
 
 import unittest
 
+import dask.array as da
 import iris.coord_categorisation
 import iris.coords
 import numpy as np
@@ -10,11 +11,16 @@ from iris.cube import Cube
 from numpy.testing import assert_array_equal
 
 from esmvalcore.preprocessor import PreprocessorFile
-from esmvalcore.preprocessor._other import _group_products, clip
+from esmvalcore.preprocessor._other import (
+    _group_products,
+    clip,
+    get_array_module,
+)
 
 
 class TestOther(unittest.TestCase):
     """Test class for _other."""
+
     def test_clip(self):
         """Test clip function."""
         cube = Cube(np.array([-10, 0, 10]))
@@ -67,5 +73,23 @@ def test_group_products_string_list():
     assert grouped_by_list == grouped_by_string
 
 
+def test_get_array_module_da():
+
+    npx = get_array_module(da.array([1, 2]))
+    assert npx is da
+
+
+def test_get_array_module_np():
+
+    npx = get_array_module(np.array([1, 2]))
+    assert npx is np
+
+
+def test_get_array_module_mixed():
+
+    npx = get_array_module(da.array([1]), np.array([1]))
+    assert npx is da
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/unit/preprocessor/_regrid/test_extract_levels.py b/tests/unit/preprocessor/_regrid/test_extract_levels.py
index 638d61fbc..f53fbac70 100644
--- a/tests/unit/preprocessor/_regrid/test_extract_levels.py
+++ b/tests/unit/preprocessor/_regrid/test_extract_levels.py
@@ -1,6 +1,4 @@
 """Unit tests for :func:`esmvalcore.preprocessor.regrid.extract_levels`."""
-import os
-import tempfile
 import unittest
 from unittest import mock
 
@@ -35,8 +33,6 @@ class Test(tests.Test):
         self.schemes = [
             'linear', 'nearest', 'linear_extrapolate', 'nearest_extrapolate',
         ]
-        descriptor, self.filename = tempfile.mkstemp('.nc')
-        os.close(descriptor)
 
     def test_invalid_scheme__unknown(self):
         levels = mock.sentinel.levels
@@ -160,7 +156,7 @@ class Test(tests.Test):
         # Check the _create_cube args ...
         self.assertEqual(len(args), 4)
         self.assertEqual(args[0], self.cube)
-        self.assert_array_equal(args[1], new_data)
+        self.assert_array_equal(args[1], np.ma.array(new_data))
         self.assert_array_equal(args[2],
                                 self.cube.coord(axis='z', dim_coords=True))
         self.assert_array_equal(args[3], levels)
@@ -281,15 +277,8 @@ class Test(tests.Test):
         masked = ma.empty(self.shape)
         masked.mask = mask
         cube = _make_cube(masked, dtype=self.dtype)
-        # save cube to test the lazy data interpolation too
-        iris.save(cube, self.filename)
         with mock.patch('stratify.interpolate',
                         return_value=new_data) as mocker:
-            # first test lazy
-            loaded_cube = iris.load_cube(self.filename)
-            result_from_lazy = extract_levels(loaded_cube, levels, scheme)
-            self.assertEqual(result_from_lazy, self.created_cube)
-            # then test realized
             result = extract_levels(cube, levels, scheme)
             self.assertEqual(result, self.created_cube)
             args, kwargs = mocker.call_args
@@ -300,7 +289,7 @@ class Test(tests.Test):
             src_levels_broadcast = np.broadcast_to(pts.reshape(self.z, 1, 1),
                                                    cube.shape)
             self.assert_array_equal(args[1], src_levels_broadcast)
-            self.assert_array_equal(args[2], cube.data)
+            self.assert_array_equal(args[2], np.ma.filled(masked, np.nan))
             # Check the stratify.interpolate kwargs ...
             self.assertEqual(
                 kwargs, dict(axis=0, interpolation=scheme,
diff --git a/tests/unit/preprocessor/_time/test_time.py b/tests/unit/preprocessor/_time/test_time.py
index e492b34b1..ab9e08653 100644
--- a/tests/unit/preprocessor/_time/test_time.py
+++ b/tests/unit/preprocessor/_time/test_time.py
@@ -623,9 +623,10 @@ class TestClimatology(tests.Test):
                            [151, 181]])
         cube = self._create_cube(data, times, bounds)
 
-        result = climate_statistics(cube, operator='mean', period='season')
-        expected = np.array([1., 1., 1.], dtype=np.float32)
-        assert_array_equal(result.data, expected)
+        for period in ('season', 'seasonal'):
+            result = climate_statistics(cube, operator='mean', period=period)
+            expected = np.array([1., 1., 1.], dtype=np.float32)
+            assert_array_equal(result.data, expected)
 
     def test_custom_season_climatology(self):
         """Test for time avg of a realisitc time axis and 365 day calendar."""
@@ -635,12 +636,13 @@ class TestClimatology(tests.Test):
                            [151, 181], [181, 212], [212, 243]])
         cube = self._create_cube(data, times, bounds)
 
-        result = climate_statistics(cube,
-                                    operator='mean',
-                                    period='season',
-                                    seasons=('jfmamj', 'jasond'))
-        expected = np.array([1., 1.], dtype=np.float32)
-        assert_array_equal(result.data, expected)
+        for period in ('season', 'seasonal'):
+            result = climate_statistics(cube,
+                                        operator='mean',
+                                        period=period,
+                                        seasons=('jfmamj', 'jasond'))
+            expected = np.array([1., 1.], dtype=np.float32)
+            assert_array_equal(result.data, expected)
 
     def test_monthly(self):
         """Test for time avg of a realistic time axis and 365 day calendar."""
@@ -650,9 +652,10 @@ class TestClimatology(tests.Test):
                            [151, 181]])
         cube = self._create_cube(data, times, bounds)
 
-        result = climate_statistics(cube, operator='mean', period='mon')
-        expected = np.ones((6, ), dtype=np.float32)
-        assert_array_equal(result.data, expected)
+        for period in ('monthly', 'month', 'mon'):
+            result = climate_statistics(cube, operator='mean', period=period)
+            expected = np.ones((6, ), dtype=np.float32)
+            assert_array_equal(result.data, expected)
 
     def test_day(self):
         """Test for time avg of a realistic time axis and 365 day calendar."""
@@ -662,9 +665,25 @@ class TestClimatology(tests.Test):
                            [367, 368]])
         cube = self._create_cube(data, times, bounds)
 
-        result = climate_statistics(cube, operator='mean', period='day')
-        expected = np.array([1, 1, 1], dtype=np.float32)
-        assert_array_equal(result.data, expected)
+        for period in ('daily', 'day'):
+            result = climate_statistics(cube, operator='mean', period=period)
+            expected = np.array([1, 1, 1], dtype=np.float32)
+            assert_array_equal(result.data, expected)
+
+    def test_hour(self):
+        """Test for time avg of a realistic time axis and 365 day calendar."""
+        data = np.array([2., 2., 10., 4., 4., 6.], dtype=np.float32)
+        times = np.array([0.5, 1.5, 2.5, 24.5, 25.5, 48.5])
+        bounds = np.array([[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6]])
+        cube = self._create_cube(data, times, bounds)
+        cube.coord('time').units = 'hours since 2000-01-01 00:00:00'
+
+        for period in ('hourly', 'hour', 'hr'):
+            result = climate_statistics(cube, operator='mean', period=period)
+            expected = np.array([4., 3., 10.], dtype=np.float32)
+            assert_array_equal(result.data, expected)
+            expected_hours = [0, 1, 2]
+            assert_array_equal(result.coord('hour').points, expected_hours)
 
     def test_period_not_supported(self):
         """Test for time avg of a realistic time axis and 365 day calendar."""
@@ -1752,6 +1771,20 @@ def test_anomalies_custom_season():
     assert_array_equal(result.coord('time').points, cube.coord('time').points)
 
 
+@pytest.mark.parametrize('period', ['hourly', 'hour', 'hr'])
+def test_anomalies_hourly(period):
+    """Test ``anomalies`` with hourly data."""
+    cube = make_map_data(number_years=1)[:48, ...]
+    cube.coord('time').units = 'hours since 2000-01-01 00:00:00'
+    result = anomalies(cube, period)
+    expected = np.concatenate((
+        np.broadcast_to(np.array([[0, -12], [-12, 0]]), (24, 2, 2)),
+        np.broadcast_to(np.array([[0, 12], [12, 0]]), (24, 2, 2)),
+    ))
+    assert_array_equal(result.data, expected)
+    assert result.coord('time') == cube.coord('time')
+
+
 def get_0d_time():
     """Get 0D time coordinate."""
     time = iris.coords.AuxCoord(15.0,