Skip to content

Commit 363b9f8

Browse files
pp-moabooton
authored andcommitted
PI-3473: Netcdf loading ancillary variables (SciTools#3556)
* _regrid_area_weighted_array: Tweak variable order to near other use in code (SciTools#3571) * Fix problems with export and echo command. (SciTools#3577) * Pushdocs fix2 (SciTools#3580) * Revert to single-line command for doctr invocation. * Added script comment, partly to force Github respin. * Added whatsnew for Black. (SciTools#3581) * Fixes required due to the release of iris-grib v0.15.0 (SciTools#3582) * Fix python-eccodes pin in travis (SciTools#3593) * Netcdf load of ancillary vars: first working.
1 parent 738093f commit 363b9f8

File tree

3 files changed

+207
-21
lines changed

3 files changed

+207
-21
lines changed

lib/iris/fileformats/_pyke_rules/fc_rules_cf.krb

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,22 @@ fc_build_cell_measure
498498
python engine.rule_triggered.add(rule.name)
499499

500500

501+
#
502+
# Context:
503+
# This rule will trigger for each ancillary_variable case specific fact.
504+
#
505+
# Purpose:
506+
# Add the ancillary variable to the cube.
507+
#
508+
fc_build_ancil_var
509+
foreach
510+
facts_cf.ancillary_variable($var)
511+
assert
512+
python ancil_var = engine.cf_var.cf_group.ancillary_variables[$var]
513+
python build_ancil_var(engine, ancil_var)
514+
python engine.rule_triggered.add(rule.name)
515+
516+
501517
#
502518
# Context:
503519
# This rule will trigger iff a CF latitude coordinate exists and
@@ -1941,36 +1957,37 @@ fc_extras
19411957
# Add it to the cube
19421958
cube.add_aux_coord(coord, data_dims)
19431959

1944-
# Update the coordinate to CF-netCDF variable mapping.
1960+
# Make a list with names, stored on the engine, so we can find them all later.
19451961
engine.provides['coordinates'].append((coord, cf_coord_var.cf_name))
19461962

19471963

19481964
################################################################################
1949-
def build_cell_measures(engine, cf_cm_attr, coord_name=None):
1965+
def build_cell_measures(engine, cf_cm_var):
19501966
"""Create a CellMeasure instance and add it to the cube."""
19511967
cf_var = engine.cf_var
19521968
cube = engine.cube
19531969
attributes = {}
19541970

19551971
# Get units
1956-
attr_units = get_attr_units(cf_cm_attr, attributes)
1972+
attr_units = get_attr_units(cf_cm_var, attributes)
19571973

1958-
data = _get_cf_var_data(cf_cm_attr, engine.filename)
1974+
# Get (lazy) content array
1975+
data = _get_cf_var_data(cf_cm_var, engine.filename)
19591976

19601977
# Determine the name of the dimension/s shared between the CF-netCDF data variable
19611978
# and the coordinate being built.
1962-
common_dims = [dim for dim in cf_cm_attr.dimensions
1979+
common_dims = [dim for dim in cf_cm_var.dimensions
19631980
if dim in cf_var.dimensions]
19641981
data_dims = None
19651982
if common_dims:
19661983
# Calculate the offset of each common dimension.
19671984
data_dims = [cf_var.dimensions.index(dim) for dim in common_dims]
19681985

19691986
# Determine the standard_name, long_name and var_name
1970-
standard_name, long_name, var_name = get_names(cf_cm_attr, coord_name, attributes)
1987+
standard_name, long_name, var_name = get_names(cf_cm_var, None, attributes)
19711988

19721989
# Obtain the cf_measure.
1973-
measure = cf_cm_attr.cf_measure
1990+
measure = cf_cm_var.cf_measure
19741991

19751992
# Create the CellMeasure
19761993
cell_measure = iris.coords.CellMeasure(data,
@@ -1984,6 +2001,51 @@ fc_extras
19842001
# Add it to the cube
19852002
cube.add_cell_measure(cell_measure, data_dims)
19862003

2004+
# Make a list with names, stored on the engine, so we can find them all later.
2005+
engine.provides['cell_measures'].append((cell_measure, cf_cm_var.cf_name))
2006+
2007+
2008+
2009+
################################################################################
2010+
def build_ancil_var(engine, cf_av_var):
2011+
"""Create an AncillaryVariable instance and add it to the cube."""
2012+
cf_var = engine.cf_var
2013+
cube = engine.cube
2014+
attributes = {}
2015+
2016+
# Get units
2017+
attr_units = get_attr_units(cf_av_var, attributes)
2018+
2019+
# Get (lazy) content array
2020+
data = _get_cf_var_data(cf_av_var, engine.filename)
2021+
2022+
# Determine the name of the dimension/s shared between the CF-netCDF data variable
2023+
# and the AV being built.
2024+
common_dims = [dim for dim in cf_av_var.dimensions
2025+
if dim in cf_var.dimensions]
2026+
data_dims = None
2027+
if common_dims:
2028+
# Calculate the offset of each common dimension.
2029+
data_dims = [cf_var.dimensions.index(dim) for dim in common_dims]
2030+
2031+
# Determine the standard_name, long_name and var_name
2032+
standard_name, long_name, var_name = get_names(cf_av_var, None, attributes)
2033+
2034+
# Create the AncillaryVariable
2035+
av = iris.coords.AncillaryVariable(
2036+
data,
2037+
standard_name=standard_name,
2038+
long_name=long_name,
2039+
var_name=var_name,
2040+
units=attr_units,
2041+
attributes=attributes)
2042+
2043+
# Add it to the cube
2044+
cube.add_ancillary_variable(av, data_dims)
2045+
2046+
# Make a list with names, stored on the engine, so we can find them all later.
2047+
engine.provides['ancillary_variables'].append((av, cf_av_var.cf_name))
2048+
19872049

19882050

19892051
################################################################################

lib/iris/fileformats/netcdf.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,10 @@ def __setstate__(self, state):
459459

460460
def _assert_case_specific_facts(engine, cf, cf_group):
461461
# Initialise pyke engine "provides" hooks.
462+
# These are used to patch non-processed element attributes after rules activation.
462463
engine.provides["coordinates"] = []
464+
engine.provides["cell_measures"] = []
465+
engine.provides["ancillary_variables"] = []
463466

464467
# Assert facts for CF coordinates.
465468
for cf_name in cf_group.coordinates.keys():
@@ -479,6 +482,12 @@ def _assert_case_specific_facts(engine, cf, cf_group):
479482
_PYKE_FACT_BASE, "cell_measure", (cf_name,)
480483
)
481484

485+
# Assert facts for CF ancillary variables.
486+
for cf_name in cf_group.ancillary_variables.keys():
487+
engine.add_case_specific_fact(
488+
_PYKE_FACT_BASE, "ancillary_variable", (cf_name,)
489+
)
490+
482491
# Assert facts for CF grid_mappings.
483492
for cf_name in cf_group.grid_mappings.keys():
484493
engine.add_case_specific_fact(
@@ -597,31 +606,38 @@ def _load_cube(engine, cf, cf_var, filename):
597606
# Run pyke inference engine with forward chaining rules.
598607
engine.activate(_PYKE_RULE_BASE)
599608

600-
# Populate coordinate attributes with the untouched attributes from the
601-
# associated CF-netCDF variable.
602-
coordinates = engine.provides.get("coordinates", [])
603-
609+
# Having run the rules, now populate the attributes of all the cf elements with the
610+
# "unused" attributes from the associated CF-netCDF variable.
611+
# That is, all those that aren't CF reserved terms.
604612
def attribute_predicate(item):
605613
return item[0] not in _CF_ATTRS
606614

607-
for coord, cf_var_name in coordinates:
608-
tmpvar = filter(
609-
attribute_predicate, cf.cf_group[cf_var_name].cf_attrs_unused()
610-
)
615+
def add_unused_attributes(iris_object, cf_var):
616+
tmpvar = filter(attribute_predicate, cf_var.cf_attrs_unused())
611617
for attr_name, attr_value in tmpvar:
612-
_set_attributes(coord.attributes, attr_name, attr_value)
618+
_set_attributes(iris_object.attributes, attr_name, attr_value)
619+
620+
def fix_attributes_all_elements(role_name):
621+
elements_and_names = engine.provides.get(role_name, [])
622+
623+
for iris_object, cf_var_name in elements_and_names:
624+
add_unused_attributes(iris_object, cf.cf_group[cf_var_name])
625+
626+
# Populate the attributes of all coordinates, cell-measures and ancillary-vars.
627+
fix_attributes_all_elements("coordinates")
628+
fix_attributes_all_elements("ancillary_variables")
629+
fix_attributes_all_elements("cell_measures")
613630

614-
tmpvar = filter(attribute_predicate, cf_var.cf_attrs_unused())
615-
# Attach untouched attributes of the associated CF-netCDF data variable to
616-
# the cube.
617-
for attr_name, attr_value in tmpvar:
618-
_set_attributes(cube.attributes, attr_name, attr_value)
631+
# Also populate attributes of the top-level cube itself.
632+
add_unused_attributes(cube, cf_var)
619633

634+
# Work out reference names for all the coords.
620635
names = {
621636
coord.var_name: coord.standard_name or coord.var_name or "unknown"
622637
for coord in cube.coords()
623638
}
624639

640+
# Add all the cube cell methods.
625641
cube.cell_methods = [
626642
iris.coords.CellMethod(
627643
method=method.method,

lib/iris/tests/test_netcdf.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import os.path
1717
import shutil
1818
import stat
19+
from subprocess import check_call
1920
import tempfile
2021
from unittest import mock
2122

@@ -27,15 +28,24 @@
2728
import iris.analysis.trajectory
2829
import iris.fileformats._pyke_rules.compiled_krb.fc_rules_cf_fc as pyke_rules
2930
import iris.fileformats.netcdf
31+
from iris.fileformats.netcdf import load_cubes as nc_load_cubes
3032
import iris.std_names
3133
import iris.util
34+
from iris.coords import AncillaryVariable, CellMeasure
3235
import iris.coord_systems as icoord_systems
3336
import iris.tests.stock as stock
3437
from iris._lazy_data import is_lazy_data
3538

3639

3740
@tests.skip_data
3841
class TestNetCDFLoad(tests.IrisTest):
42+
def setUp(self):
43+
self.tmpdir = None
44+
45+
def tearDown(self):
46+
if self.tmpdir is not None:
47+
shutil.rmtree(self.tmpdir)
48+
3949
def test_monotonic(self):
4050
cubes = iris.load(
4151
tests.get_data_path(
@@ -240,6 +250,104 @@ def test_cell_methods(self):
240250

241251
self.assertCML(cubes, ("netcdf", "netcdf_cell_methods.cml"))
242252

253+
def test_ancillary_variables(self):
254+
# Note: using a CDL string as a test data reference, rather than a binary file.
255+
ref_cdl = """
256+
netcdf cm_attr {
257+
dimensions:
258+
axv = 3 ;
259+
variables:
260+
int64 qqv(axv) ;
261+
qqv:long_name = "qq" ;
262+
qqv:units = "1" ;
263+
qqv:ancillary_variables = "my_av" ;
264+
int64 axv(axv) ;
265+
axv:units = "1" ;
266+
axv:long_name = "x" ;
267+
double my_av(axv) ;
268+
my_av:units = "1" ;
269+
my_av:long_name = "refs" ;
270+
my_av:custom = "extra-attribute";
271+
data:
272+
axv = 1, 2, 3;
273+
my_av = 11., 12., 13.;
274+
}
275+
"""
276+
self.tmpdir = tempfile.mkdtemp()
277+
cdl_path = os.path.join(self.tmpdir, "tst.cdl")
278+
nc_path = os.path.join(self.tmpdir, "tst.nc")
279+
# Write CDL string into a temporary CDL file.
280+
with open(cdl_path, "w") as f_out:
281+
f_out.write(ref_cdl)
282+
# Use ncgen to convert this into an actual (temporary) netCDF file.
283+
command = "ncgen -o {} {}".format(nc_path, cdl_path)
284+
check_call(command, shell=True)
285+
# Load with iris.fileformats.netcdf.load_cubes, and check expected content.
286+
cubes = list(nc_load_cubes(nc_path))
287+
self.assertEqual(len(cubes), 1)
288+
avs = cubes[0].ancillary_variables()
289+
self.assertEqual(len(avs), 1)
290+
expected = AncillaryVariable(
291+
np.ma.array([11.0, 12.0, 13.0]),
292+
long_name="refs",
293+
var_name="my_av",
294+
units="1",
295+
attributes={"custom": "extra-attribute"},
296+
)
297+
self.assertEqual(avs[0], expected)
298+
299+
def test_cell_measures(self):
300+
# Note: using a CDL string as a test data reference, rather than a binary file.
301+
ref_cdl = """
302+
netcdf cm_attr {
303+
dimensions:
304+
axv = 3 ;
305+
ayv = 2 ;
306+
variables:
307+
int64 qqv(ayv, axv) ;
308+
qqv:long_name = "qq" ;
309+
qqv:units = "1" ;
310+
qqv:cell_measures = "area: my_areas" ;
311+
int64 ayv(ayv) ;
312+
ayv:units = "1" ;
313+
ayv:long_name = "y" ;
314+
int64 axv(axv) ;
315+
axv:units = "1" ;
316+
axv:long_name = "x" ;
317+
double my_areas(ayv, axv) ;
318+
my_areas:units = "m2" ;
319+
my_areas:long_name = "standardised cell areas" ;
320+
my_areas:custom = "extra-attribute";
321+
data:
322+
axv = 11, 12, 13;
323+
ayv = 21, 22;
324+
my_areas = 110., 120., 130., 221., 231., 241.;
325+
}
326+
"""
327+
self.tmpdir = tempfile.mkdtemp()
328+
cdl_path = os.path.join(self.tmpdir, "tst.cdl")
329+
nc_path = os.path.join(self.tmpdir, "tst.nc")
330+
# Write CDL string into a temporary CDL file.
331+
with open(cdl_path, "w") as f_out:
332+
f_out.write(ref_cdl)
333+
# Use ncgen to convert this into an actual (temporary) netCDF file.
334+
command = "ncgen -o {} {}".format(nc_path, cdl_path)
335+
check_call(command, shell=True)
336+
# Load with iris.fileformats.netcdf.load_cubes, and check expected content.
337+
cubes = list(nc_load_cubes(nc_path))
338+
self.assertEqual(len(cubes), 1)
339+
cms = cubes[0].cell_measures()
340+
self.assertEqual(len(cms), 1)
341+
expected = CellMeasure(
342+
np.ma.array([[110.0, 120.0, 130.0], [221.0, 231.0, 241.0]]),
343+
measure="area",
344+
var_name="my_areas",
345+
long_name="standardised cell areas",
346+
units="m2",
347+
attributes={"custom": "extra-attribute"},
348+
)
349+
self.assertEqual(cms[0], expected)
350+
243351
def test_deferred_loading(self):
244352
# Test exercising CF-netCDF deferred loading and deferred slicing.
245353
# shape (31, 161, 320)

0 commit comments

Comments
 (0)