Skip to content

Conversation

lbdreyer
Copy link
Member

@lbdreyer lbdreyer commented Sep 9, 2019

CF conventions support a set of standard_name modifiers. See 3.3. Standard Name for more details, specifically:

A standard name is associated with a variable via the attribute standard_name which takes a string value comprised of a standard name optionally followed by one or more blanks and a standard name modifier (a string value from Appendix C, Standard Name Modifiers).

This PR modifies the CFVariableMixin standard_name setter (and the pyke rules) to support standard name modifiers.

I found there wasn't much testing of cube/coord names so I have split this PR into two commits:

  1. The first commit has no functional changes. It just adds testing of the pyke rules to show that they don't break later when I add the functional changes
  2. The second commit adds the changes to include the standard_name modifiers

Note that standard name modifiers have associated units (See CF Appendix C) however I have not included that in this PR as I am only modifying the standard_name setter which at the moment doesn't have any requirement on the unit

@lbdreyer lbdreyer requested a review from bjlittle September 9, 2019 12:23
@lbdreyer lbdreyer added this to the v2.3.0 milestone Sep 9, 2019
@lbdreyer
Copy link
Member Author

lbdreyer commented Sep 9, 2019

Ping @bjlittle !

Copy link
Member

@bjlittle bjlittle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lbdreyer Nice one, over to you... 😄

# Supported standard name modifiers. Ref: [CF] Appendix C.
STD_NAME_MODIFIERS = ['detection_minimum', 'number_of_observations',
'standard_error', 'status_flag']

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lbdreyer It's kinda weird to define constants here that aren't used in this module. Rather they're used in _cube_coord_common.py, and specifically in only one place...

I see the intent, but would you consider binding their use closer to their definition? i.e., actually define STD_NAME_MODIFIERS as a variable within the iris._cube_coord_common.get_valid_standard_name function.

self._standard_name = name
else:
raise ValueError('%r is not a valid standard_name' % name)
self._standard_name = get_valid_standard_name(name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could change get_valid_standard_name so that if None is passed in, then None is passed out. That way any code that calls get_valid_standard_name doesn't need to explicitly handle the None case (as above).

So here we would simply have:

@standard_name.setter
def standard_name(self, name):
    self._standard_name = get_valid_standard_name(name)

standard_name_modifier not in
iris.fileformats.cf.STD_NAME_MODIFIERS):
raise ValueError(error_msg)
return name
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider pulling the return name out from within the embedded if else and make it the last statement of the function i.e.,

def get_valid_standard_name(name):
    ...
    if ...
        ...
    else:
        ...
    return name

Copy link
Member Author

@lbdreyer lbdreyer Sep 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree, it would be nicer. I just couldn't quite think through how to do that, but I will give it another look!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lbdreyer I think that all you need to do is simply move the return name outside the if ... else ..., as your not changing the contents of name, the output of the function is just the same as the input.

If the input std_name (from name) is valid, then it will pass through the if ..., and then if the standard_name_modifier is valid it will simply drop through and return name. On the otherhand, if the modifier is not valid an exception is raised and the return name is never be reached.

If the input name is invalid, then it will pass through to the else ... where an exception is raised and the return name will never be reached.

Should just work, right? Check my logic is right, though...

# Standard names are optionally followed by a standard name
# modifier, separated by one or more blank spaces
name_parts = name.split(' ')
std_name, std_name_extension = name_parts[0], name_parts[1:]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a (compiled) regular expression would allow you to easily pluck out the standard name and modifier, and avoid dealing with a std_name_extension that is a list.

with self.temp_filename(suffix='.nc') as fout:
iris.save(cube, fout)
detection_limit_cube = iris.load_cube(fout)
self.assertEqual(detection_limit_cube.standard_name, standard_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍

class Test_standard_name__setter(tests.IrisTest):
def test_valid_standard_name(self):
cf_var = CFVariableMixin()
cf_var.standard_name = 'air_temperature'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you then self.assertEqual(cf_var.standard_name, 'air_temperature') to ensure that we get back what we set?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my concern with that was that we are then testing the standard_name getter and setter at the same time, which doesn't feel very unit-testy but I guess we should test not just that setting the standard doesn't raise errors, but also that it is done correctly and it's hard to do that in isolation from testing the getter.


def test_none_standard_name(self):
cf_var = CFVariableMixin()
cf_var.standard_name = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, include a self.assertIsNone(cf_var.standard_name)?

engine = mock.Mock(
cube=Cube([23]),
cf_var=cf_var)
return engine
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondered why you pulled this out of TestInvalidGlobalAttributes, but you're also using it in the new test class TestCubeName... nice 👍

@bjlittle
Copy link
Member

@lbdreyer What's the strategy for checking the units of a standard_name with a standard name modifier?

Separate PR?

inp = ('latitude_coord', 'lat_long_name', 'lat_var_name', 'latitude')
exp = ('latitude', 'lat_long_name', 'lat_var_name',
{'invalid_standard_name': 'latitude_coord'})
self.check_names(inp, exp)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lbdreyer Whoa... nice test coverage 🎉

@lbdreyer
Copy link
Member Author

@lbdreyer What's the strategy for checking the units of a standard_name with a standard name modifier?

Separate PR?

I have raised this in a separate issue: #3408

@lbdreyer
Copy link
Member Author

@bjlittle I believe I have addressed your review comments in the third commit "review actions"

Note: I had to rebase the first two commits due a conflict that was caused when #3399 was merged, but those commits should be the same as they were before.

@bjlittle
Copy link
Member

@lbdreyer Awesome, thanks! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants