Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazy netcdf saves #5191

Merged
merged 92 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
cd7fa42
Basic functional lazy saving.
pp-mo Oct 21, 2022
1f32800
Simplify function signature which upsets Sphinx.
pp-mo Oct 21, 2022
e0f980f
Non-lazy saves return nothing.
pp-mo Oct 21, 2022
67b96cf
Now fixed to enable use with process/distributed scheduling.
pp-mo Oct 23, 2022
8cdbc9b
Remove dask.utils.SerializableLock, which I think was a mistake.
pp-mo Mar 3, 2023
8723f24
Make DefferedSaveWrapper use _thread_safe_nc.
pp-mo Mar 8, 2023
d19a87f
Fixes for non-lazy save.
pp-mo Mar 9, 2023
45e0e60
Avoid saver error when no deferred writes.
pp-mo Mar 10, 2023
6a83200
Reorganise locking code, ready for shareable locks.
pp-mo Mar 10, 2023
78a9346
Remove optional usage of 'filelock' for lazy saves.
pp-mo Mar 10, 2023
31b12f7
Document dask-specific locking; implement differently for threads or …
pp-mo Mar 10, 2023
99b4f41
Minor fix for unit-tests.
pp-mo Mar 10, 2023
5d0a707
Pin libnetcdf to avoid problems -- see #5187.
pp-mo Mar 10, 2023
431036f
Minor test fix.
pp-mo Mar 10, 2023
dc368d9
Move DeferredSaveWrapper into _thread_safe_nc; replicate the NetCDFDa…
pp-mo Mar 13, 2023
6756a46
Update lib/iris/fileformats/netcdf/saver.py
pp-mo Mar 16, 2023
80b4b6c
Update lib/iris/fileformats/netcdf/_dask_locks.py
pp-mo Mar 16, 2023
78a8716
Update lib/iris/fileformats/netcdf/saver.py
pp-mo Mar 16, 2023
47bb08b
Small rename + reformat.
pp-mo Mar 17, 2023
0ece09a
Remove Saver lazy option; all lazy saves are delayed; factor out fill…
pp-mo Mar 18, 2023
940f544
Merge branch 'main' into lazy_save_2
pp-mo Mar 18, 2023
eb97130
Merge remote-tracking branch 'upstream/main' into lazy_save_2
pp-mo Mar 20, 2023
4596081
Repurposed 'test__FillValueMaskCheckAndStoreTarget' to 'test__data_fi…
pp-mo Mar 20, 2023
ad49fbe
Disable (temporary) saver debug printouts.
pp-mo Mar 20, 2023
b29c927
Fix test problems; Saver automatically completes to preserve existing…
pp-mo Mar 20, 2023
8f10281
Fix docstring error.
pp-mo Mar 20, 2023
6a564d9
Fix spurious error in old saver test.
pp-mo Mar 20, 2023
2fb4d6c
Fix Saver docstring.
pp-mo Mar 20, 2023
c84bfdc
More robust exit for NetCDFWriteProxy operation.
pp-mo Mar 20, 2023
5b78085
Fix doctests by making the Saver example functional.
pp-mo Mar 21, 2023
478332e
Improve docstrings; unify terminology; simplify non-lazy save call.
pp-mo Mar 23, 2023
34f154c
Moved netcdf cell-method handling into nc_load_rules.helpers, and var…
pp-mo Mar 27, 2023
d3744ba
Merge branch 'latest' into lazy_save_2
pp-mo Mar 27, 2023
9673ea0
Fix lockfiles and Makefile process.
pp-mo Mar 27, 2023
bcbcbc8
Add unit tests for routine _fillvalue_report().
pp-mo Mar 27, 2023
05c04a1
Remove debug-only code.
pp-mo Mar 27, 2023
679ea47
Added tests for what the save function does with the 'compute' keyword.
pp-mo Mar 28, 2023
70ec9dd
Fix mock-specific problems, small tidy.
pp-mo Mar 28, 2023
28a4674
Restructure hierarchy of tests.unit.fileformats.netcdf
pp-mo Mar 29, 2023
67f4b2b
Tidy test docstrings.
pp-mo Mar 29, 2023
ebec72f
Correct test import.
pp-mo Mar 29, 2023
1f5b904
Avoid incorrect checking of byte data, and a numpy deprecation warning.
pp-mo Mar 29, 2023
5045c9f
Alter parameter names to make test reports clearer.
pp-mo Mar 29, 2023
393407a
Test basic behaviour of _lazy_stream_data; make 'Saver._delayed_write…
pp-mo Mar 29, 2023
518360b
Add integration tests, and distributed dependency.
pp-mo Mar 30, 2023
5c9931f
Docstring fixes.
pp-mo Mar 31, 2023
7daee68
Documentation section and whatsnew entry.
pp-mo Apr 4, 2023
97474f9
Merge branch 'main' into lazy_save_2
pp-mo Apr 4, 2023
64c7251
Various fixes to whatsnew, docstrings and docs.
pp-mo Apr 4, 2023
75043f9
Minor review changes, fix doctest.
pp-mo Apr 11, 2023
445fbe2
Arrange tests + results to organise by package-name alone.
pp-mo Apr 11, 2023
09cb22e
Review changes.
pp-mo Apr 11, 2023
3445f58
Review changes.
pp-mo Apr 12, 2023
cb1e1f7
Enhance tests + debug.
pp-mo Apr 12, 2023
1c81cee
Support scheduler type 'single-threaded'; allow retries on delayed-sa…
pp-mo Apr 13, 2023
370837b
Improve test.
pp-mo Apr 13, 2023
2f5f3c2
Adding a whatsnew entry for 5224 (#5234)
HGWright Apr 4, 2023
a55c6f2
Replacing numpy legacy printing with array2string and remaking result…
HGWright Apr 4, 2023
4914e99
adding a whatsnew entry
HGWright Apr 4, 2023
bd642cd
configure codecov
HGWright Apr 4, 2023
bc5bdd1
remove results creation commit from blame
HGWright Apr 4, 2023
301e59e
fixing whatsnew entry
HGWright Apr 4, 2023
7b3044d
Bump scitools/workflows from 2023.04.1 to 2023.04.2 (#5236)
dependabot[bot] Apr 5, 2023
02f2b66
Use real array for data of of small netCDF variables. (#5229)
pp-mo Apr 6, 2023
a7e0689
Handle derived coordinates correctly in `concatenate` (#5096)
schlunma Apr 12, 2023
c4e8bbb
clarity on whatsnew entry contributors (#5240)
bjlittle Apr 12, 2023
e6661b8
Modernize and simplify iris.analysis._Groupby (#5015)
bouweandela Apr 12, 2023
afbdbbd
Finalises Lazy Data documentation (#5137)
ESadek-MO Apr 12, 2023
b8bb753
Fixes to _discontiguity_in_bounds (attempt 2) (#4975)
stephenworsley Apr 12, 2023
97cc149
update ci locks location (#5228)
bjlittle Apr 13, 2023
f14a321
Updated environment lockfiles (#5211)
scitools-ci[bot] Apr 13, 2023
f7a0b87
Increase retries.
pp-mo Apr 13, 2023
69ddd9d
Change debug to show which elements failed.
pp-mo Apr 13, 2023
8235d60
update cf standard units (#5244)
ESadek-MO Apr 13, 2023
724c6d2
libnetcdf <4.9 pin (#5242)
trexfeathers Apr 13, 2023
4f50dc7
Avoid possible same-file crossover between tests.
pp-mo Apr 13, 2023
0da68cf
Ensure all-different testfiles; load all vars lazy.
pp-mo Apr 13, 2023
e8b7bfd
Revert changes to testing framework.
pp-mo Apr 13, 2023
ad48caf
Remove repeated line from requirements/py*.yml (?merge error), and re…
pp-mo Apr 13, 2023
291b587
Revert some more debug changes.
pp-mo Apr 13, 2023
b2260ef
Merge branch 'latest' into lazy_save_2
pp-mo Apr 13, 2023
33a7d86
Reorganise test for better code clarity.
pp-mo Apr 14, 2023
db6932d
Use public 'Dataset.isopen()' instead of '._isopen'.
pp-mo Apr 14, 2023
631e001
Create output files in unique temporary directories.
pp-mo Apr 14, 2023
2869f97
Tests for fileformats.netcdf._dask_locks.
pp-mo Apr 14, 2023
419727b
Merge branch 'latest' into lazy_save_2
pp-mo Apr 21, 2023
2f4458b
Fix attribution names.
pp-mo Apr 21, 2023
88b7a2a
Merge branch 'latest' into lazy_save_2
pp-mo Apr 21, 2023
98a20e7
Fixed new py311 lockfile.
pp-mo Apr 21, 2023
bbc1167
Fix typos spotted by codespell.
pp-mo Apr 21, 2023
ed38e43
Add distributed test dep for python 3.11
pp-mo Apr 21, 2023
54ec0f8
Fix lockfile for python 3.11
pp-mo Apr 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Use real array for data of of small netCDF variables. (#5229)
* Small netCDF variable data is real.

* Various test fixes.

* More test fixing.

* Fix printout in Mesh documentation.

* Whatsnew + doctests fix.

* Tweak whatsnew.
  • Loading branch information
pp-mo committed Apr 13, 2023
commit 02f2b66926e1c36364641ee5f6a68a17041e080a
16 changes: 8 additions & 8 deletions docs/src/further_topics/ugrid/operations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -430,20 +430,20 @@ creating any associated :class:`~iris.cube.Cube`\s:
node
node_dimension: 'Mesh2d_node'
node coordinates
<AuxCoord: longitude / (degrees) <lazy> shape(5,)>
<AuxCoord: latitude / (unknown) <lazy> shape(5,)>
<AuxCoord: longitude / (degrees) [...] shape(5,)>
<AuxCoord: latitude / (unknown) [...] shape(5,)>
edge
edge_dimension: 'Mesh2d_edge'
edge_node_connectivity: <Connectivity: mesh2d_edge / (unknown) <lazy> shape(6, 2)>
edge_node_connectivity: <Connectivity: mesh2d_edge / (unknown) [...] shape(6, 2)>
edge coordinates
<AuxCoord: longitude / (unknown) <lazy> shape(6,)>
<AuxCoord: latitude / (unknown) <lazy> shape(6,)>
<AuxCoord: longitude / (unknown) [...] shape(6,)>
<AuxCoord: latitude / (unknown) [...] shape(6,)>
face
face_dimension: 'Mesh2d_face'
face_node_connectivity: <Connectivity: mesh2d_face / (unknown) <lazy> shape(2, 4)>
face_node_connectivity: <Connectivity: mesh2d_face / (unknown) [...] shape(2, 4)>
face coordinates
<AuxCoord: longitude / (unknown) <lazy> shape(2,)>
<AuxCoord: latitude / (unknown) <lazy> shape(2,)>
<AuxCoord: longitude / (unknown) [...] shape(2,)>
<AuxCoord: latitude / (unknown) [...] shape(2,)>
long_name: 'my_mesh'
var_name: 'my_mesh'

Expand Down
15 changes: 7 additions & 8 deletions docs/src/userguide/real_and_lazy_data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,17 +189,17 @@ coordinates' lazy points and bounds:

.. doctest::

>>> cube = iris.load_cube(iris.sample_data_path('hybrid_height.nc'), 'air_potential_temperature')
>>> cube = iris.load_cube(iris.sample_data_path('orca2_votemper.nc'),'votemper')

>>> dim_coord = cube.coord('model_level_number')
>>> dim_coord = cube.coord('depth')
>>> print(dim_coord.has_lazy_points())
False
>>> print(dim_coord.has_bounds())
False
True
>>> print(dim_coord.has_lazy_bounds())
False

>>> aux_coord = cube.coord('sigma')
>>> aux_coord = cube.coord('longitude')
>>> print(aux_coord.has_lazy_points())
True
>>> print(aux_coord.has_bounds())
Expand All @@ -214,17 +214,16 @@ coordinates' lazy points and bounds:
>>> print(aux_coord.has_lazy_bounds())
True

>>> derived_coord = cube.coord('altitude')
# Fetch a derived coordinate, from a different file: These can also have lazy data.
>>> cube2 = iris.load_cube(iris.sample_data_path('hybrid_height.nc'), 'air_potential_temperature')
>>> derived_coord = cube2.coord('altitude')
>>> print(derived_coord.has_lazy_points())
True
>>> print(derived_coord.has_bounds())
True
>>> print(derived_coord.has_lazy_bounds())
True

.. note::
Printing a lazy :class:`~iris.coords.AuxCoord` will realise its points and bounds arrays!


Dask Processing Options
-----------------------
Expand Down
5 changes: 4 additions & 1 deletion docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ This document explains the changes made to Iris for this release
🚀 Performance Enhancements
===========================

#. N/A
#. `@pp-mo`_ changed the netCDF loader to fetch data immediately from small netCDF
variables, instead of creating a dask array: This saves both time and memory.
Note that some cubes, coordinates etc loaded from netCDF will now have real data
where previously it was lazy. (:pull:`5229`)


🔥 Deprecations
Expand Down
65 changes: 46 additions & 19 deletions lib/iris/fileformats/netcdf/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,26 +173,53 @@ def _get_actual_dtype(cf_var):
return dummy_data.dtype


# An arbitrary variable array size, below which we will fetch real data from a variable
# rather than making a lazy array for deferred access.
# Set by experiment at roughly the point where it begins to save us memory, but actually
# mostly done for speed improvement. See https://github.com/SciTools/iris/pull/5069
_LAZYVAR_MIN_BYTES = 5000


def _get_cf_var_data(cf_var, filename):
# Get lazy chunked data out of a cf variable.
dtype = _get_actual_dtype(cf_var)

# Create cube with deferred data, but no metadata
fill_value = getattr(
cf_var.cf_data,
"_FillValue",
_thread_safe_nc.default_fillvals[cf_var.dtype.str[1:]],
)
proxy = NetCDFDataProxy(
cf_var.shape, dtype, filename, cf_var.cf_name, fill_value
)
# Get the chunking specified for the variable : this is either a shape, or
# maybe the string "contiguous".
chunks = cf_var.cf_data.chunking()
# In the "contiguous" case, pass chunks=None to 'as_lazy_data'.
if chunks == "contiguous":
chunks = None
return as_lazy_data(proxy, chunks=chunks)
"""
Get an array representing the data of a CF variable.

This is typically a lazy array based around a NetCDFDataProxy, but if the variable
is "sufficiently small", we instead fetch the data as a real (numpy) array.
The latter is especially valuable for scalar coordinates, which are otherwise
unnecessarily slow + wasteful of memory.

"""
total_bytes = cf_var.size * cf_var.dtype.itemsize
if total_bytes < _LAZYVAR_MIN_BYTES:
# Don't make a lazy array, as it will cost more memory AND more time to access.
# Instead fetch the data immediately, as a real array, and return that.
result = cf_var[:]

else:
# Get lazy chunked data out of a cf variable.
dtype = _get_actual_dtype(cf_var)

# Make a data-proxy that mimics array access and can fetch from the file.
fill_value = getattr(
cf_var.cf_data,
"_FillValue",
_thread_safe_nc.default_fillvals[cf_var.dtype.str[1:]],
)
proxy = NetCDFDataProxy(
cf_var.shape, dtype, filename, cf_var.cf_name, fill_value
)
# Get the chunking specified for the variable : this is either a shape, or
# maybe the string "contiguous".
chunks = cf_var.cf_data.chunking()
# In the "contiguous" case, pass chunks=None to 'as_lazy_data'.
if chunks == "contiguous":
chunks = None

# Return a dask array providing deferred access.
result = as_lazy_data(proxy, chunks=chunks)

return result


class _OrderedAddableList(list):
Expand Down
7 changes: 6 additions & 1 deletion lib/iris/tests/integration/netcdf/test_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import os.path
import shutil
import tempfile
from unittest import mock
import warnings

import numpy as np
Expand All @@ -34,7 +35,11 @@ def test_lazy_preserved_save(self):
fpath = tests.get_data_path(
("NetCDF", "label_and_climate", "small_FC_167_mon_19601101.nc")
)
acube = iris.load_cube(fpath, "air_temperature")
# While loading, "turn off" loading small variables as real data.
with mock.patch(
"iris.fileformats.netcdf.loader._LAZYVAR_MIN_BYTES", 0
):
acube = iris.load_cube(fpath, "air_temperature")
self.assertTrue(acube.has_lazy_data())
# Also check a coord with lazy points + bounds.
self.assertTrue(acube.coord("forecast_period").has_lazy_points())
Expand Down
10 changes: 9 additions & 1 deletion lib/iris/tests/integration/test_cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
# importing anything else.
import iris.tests as tests # isort:skip

from unittest import mock

import numpy as np

import iris
Expand All @@ -23,7 +25,13 @@ def test_agg_by_aux_coord(self):
problem_test_file = tests.get_data_path(
("NetCDF", "testing", "small_theta_colpex.nc")
)
cube = iris.load_cube(problem_test_file, "air_potential_temperature")
# While loading, "turn off" loading small variables as real data.
with mock.patch(
"iris.fileformats.netcdf.loader._LAZYVAR_MIN_BYTES", 0
):
cube = iris.load_cube(
problem_test_file, "air_potential_temperature"
)

# Test aggregating by aux coord, notably the `forecast_period` aux
# coord on `cube`, whose `_points` attribute is a lazy array.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
</coord>
</coords>
<cellMethods/>
<data dtype="float64" shape="(2, 2)" state="deferred"/>
<data dtype="float64" shape="(2, 2)" state="loaded"/>
</cube>
</cubes>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
</coord>
</coords>
<cellMethods/>
<data dtype="float64" shape="(2, 2)" state="deferred"/>
<data dtype="float64" shape="(2, 2)" state="loaded"/>
</cube>
</cubes>
2 changes: 1 addition & 1 deletion lib/iris/tests/results/netcdf/save_load_traj.cml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@
<coord name="time"/>
</cellMethod>
</cellMethods>
<data dtype="float32" shape="(10,)" state="deferred"/>
<data dtype="float32" shape="(10,)" state="loaded"/>
</cube>
</cubes>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
</coord>
</coords>
<cellMethods/>
<data dtype="float64" shape="(2, 2)" state="deferred"/>
<data dtype="float64" shape="(2, 2)" state="loaded"/>
</cube>
</cubes>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
</coord>
</coords>
<cellMethods/>
<data dtype="float64" shape="(2, 2)" state="deferred"/>
<data dtype="float64" shape="(2, 2)" state="loaded"/>
</cube>
</cubes>
8 changes: 7 additions & 1 deletion lib/iris/tests/unit/aux_factory/test_AuxCoordFactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# importing anything else.
import iris.tests as tests # isort:skip

from unittest import mock

import numpy as np

import iris
Expand Down Expand Up @@ -143,7 +145,11 @@ def setUp(self):
path = tests.get_data_path(
["NetCDF", "testing", "small_theta_colpex.nc"]
)
self.cube = iris.load_cube(path, "air_potential_temperature")
# While loading, "turn off" loading small variables as real data.
with mock.patch(
"iris.fileformats.netcdf.loader._LAZYVAR_MIN_BYTES", 0
):
self.cube = iris.load_cube(path, "air_potential_temperature")

def _check_lazy(self):
coords = self.cube.aux_coords + self.cube.derived_coords
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def mock_cf_av_var(monkeypatch):
long_name="wibble",
units="m2",
shape=data.shape,
size=np.prod(data.shape),
dtype=data.dtype,
__getitem__=lambda self, key: data[key],
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
build_auxilliary_coordinate`.

"""

# import iris tests first so that some things can be initialised before
# importing anything else
import iris.tests as tests # isort:skip

import contextlib
from unittest import mock

import numpy as np
Expand Down Expand Up @@ -48,6 +48,7 @@ def setUp(self):
long_name="wibble",
units="m",
shape=points.shape,
size=np.prod(points.shape),
dtype=points.dtype,
__getitem__=lambda self, key: points[key],
)
Expand Down Expand Up @@ -111,6 +112,7 @@ def _make_cf_bounds_var(self, dimension_names):
cf_name="wibble_bnds",
cf_data=cf_data,
shape=bounds.shape,
size=np.prod(bounds.shape),
dtype=bounds.dtype,
__getitem__=lambda self, key: bounds[key],
)
Expand Down Expand Up @@ -165,6 +167,7 @@ def setUp(self):
long_name="wibble",
units="m",
shape=points.shape,
size=np.prod(points.shape),
dtype=points.dtype,
__getitem__=lambda self, key: points[key],
)
Expand All @@ -176,21 +179,29 @@ def setUp(self):
cube_parts=dict(coordinates=[]),
)

@contextlib.contextmanager
def deferred_load_patch(self):
def patched__getitem__(proxy_self, keys):
if proxy_self.variable_name == self.cf_coord_var.cf_name:
return self.cf_coord_var[keys]
raise RuntimeError()

self.deferred_load_patch = mock.patch(
# Fix for deferred load, *AND* avoid loading small variable data in real arrays.
with mock.patch(
"iris.fileformats.netcdf.NetCDFDataProxy.__getitem__",
new=patched__getitem__,
)
):
# While loading, "turn off" loading small variables as real data.
with mock.patch(
"iris.fileformats.netcdf.loader._LAZYVAR_MIN_BYTES", 0
):
yield

def test_scale_factor_add_offset_int(self):
self.cf_coord_var.scale_factor = 3
self.cf_coord_var.add_offset = 5

with self.deferred_load_patch:
with self.deferred_load_patch():
build_auxiliary_coordinate(self.engine, self.cf_coord_var)

coord, _ = self.engine.cube_parts["coordinates"][0]
Expand All @@ -199,7 +210,7 @@ def test_scale_factor_add_offset_int(self):
def test_scale_factor_float(self):
self.cf_coord_var.scale_factor = 3.0

with self.deferred_load_patch:
with self.deferred_load_patch():
build_auxiliary_coordinate(self.engine, self.cf_coord_var)

coord, _ = self.engine.cube_parts["coordinates"][0]
Expand All @@ -208,7 +219,7 @@ def test_scale_factor_float(self):
def test_add_offset_float(self):
self.cf_coord_var.add_offset = 5.0

with self.deferred_load_patch:
with self.deferred_load_patch():
build_auxiliary_coordinate(self.engine, self.cf_coord_var)

coord, _ = self.engine.cube_parts["coordinates"][0]
Expand Down Expand Up @@ -239,6 +250,7 @@ def setUp(self):
units="days since 1970-01-01",
calendar=None,
shape=points.shape,
size=np.prod(points.shape),
dtype=points.dtype,
__getitem__=lambda self, key: points[key],
)
Expand All @@ -251,6 +263,7 @@ def setUp(self):
cf_name="wibble_bnds",
cf_data=mock.MagicMock(chunking=mock.Mock(return_value=None)),
shape=bounds.shape,
size=np.prod(bounds.shape),
dtype=bounds.dtype,
__getitem__=lambda self, key: bounds[key],
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def mock_cf_cm_var(monkeypatch):
long_name="wibble",
units="m2",
shape=data.shape,
size=np.prod(data.shape),
dtype=data.dtype,
__getitem__=lambda self, key: data[key],
cf_measure="area",
Expand Down
Loading