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

Disable default validation for some parameters #1372

Merged
merged 1 commit into from
Nov 2, 2017
Merged

Conversation

drasmuss
Copy link
Member

Some parameters rely on attributes of their parent object to perform the validation, so the validation cannot be performed until the attribute value is set on that object. Previously, attempting to set the default value for one of these parameters (e.g. net.config[nengo.Connection].transform = 2) would result in an error (because it would try to perform the validation and fail). This PR disables the validation for the defaults on those parameters. Note that the validation will still be performed when the attribute is set on an object instance (e.g., my_conn.transform = 2 or self.transform = Default).

Also adjusted some parameters that were set to non-optional with a value of None (or vice versa), which were revealed by the new test.

How has this been tested?
Added a new test, see test_params.py:test_coerce_defaults

How long should this take to review?

  • Average (neither quick nor lengthy)

Types of changes:

  • Bug fix (non-breaking change which fixes an issue)

Checklist:

  • I have read the CONTRIBUTING.rst document.
  • I have updated the documentation accordingly.
  • I have included a changelog entry.
  • I have added tests to cover my changes.
  • I have run the test suite locally and all tests passed.

Copy link
Contributor

@Seanny123 Seanny123 left a comment

Choose a reason for hiding this comment

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

LGTM. Don't forget to update the changelog before merging.

Copy link
Collaborator

@jgosmann jgosmann left a comment

Choose a reason for hiding this comment

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

One general question: Why do we coerce defaults at all? If my reading of the code (in particular SupportDefaultMixin) is correct, when the actual parameter value is set from the default it is coerced anyways.

Apart from that general question and some more documentation related comments/questions, this PR looks good to me implementation-wise. It would be nice to get rid of that C901 pyflakes complexity warning for the test (it is a bit long and the interesting part is all the way to the end). But currently I cannot think of a better way to organize it either.

nengo/params.py Outdated
coerce_defaults : bool (Default: True)
If True, validate default values for this parameter if they are changed
by the config system. Setting a value on a parameter object instance
will always be validated.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it be more accurate to say 'when' instead of 'if' in the first sentence? I.e.: “If True, validate default values for this parameter when they are changed by the config system.” I would understand that as that this validation happens whenever the value is changed (by the config system), whereas the variant with 'if’ seems to imply that the validation could be separate from changing the value as long as the value is changed at all. But I'm not a native speaker, so maybe the 'if' works equally well.

In the second sentence: What does 'parameter object instance' exactly mean? Object and instance are often the same thing. Could we do with just one of the words? And what exactly is the distinction to the case described in the first sentence? Is it setting a non-default value (I assume?) or is it setting a (potentially default value) on a Parameter instance (as opposed to the Parameter class)?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is trying to distinguish between (1) config[obj].attr = val versus (2) obj.attr = val. The user may not have any config[obj].attr = val lines in their code (i.e., they never change the defaults), in which case coerce would never be called in the (1) format and it wouldn't matter if coerce_defaults was True or False (hence the "if").

The second sentence is trying to describe (2) (obj.attr = val). It might be clearer just to take that part out, since it's describing behaviour that isn't really connected to this parameter (I was just worried that people would think there was no validation being performed if coerce_defaults=False).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks, I understand now better what you intended to say. How about:

If True, validate values for this parameter when they are set in a Config object. Setting a parameter directly on an object will always be validated.

Copy link
Member Author

Choose a reason for hiding this comment

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

Sounds good

n_neurons = IntParam('n_neurons', default=None, low=1)
dimensions = IntParam('dimensions', default=None, low=1)
n_neurons = IntParam('n_neurons', default=1, low=1)
dimensions = IntParam('dimensions', default=1, low=1)
Copy link
Collaborator

Choose a reason for hiding this comment

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

These parameters aren't optional, are they? Does setting the default have any effect for non-optional parameters?

Copy link
Member Author

Choose a reason for hiding this comment

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

You could in theory create an ensemble like nengo.Ensemble(Default, Default), which would use the default values specified here (previously this would have tried to use None, resulting in an error). We could also make these parameters Unconfigurable if we wanted.

Copy link
Collaborator

Choose a reason for hiding this comment

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

nengo.Ensemble(Default, Default) doesn't seem like something I would want to do or encourage. Not sure at the moment what the implications of Unconfigurable are (need to read the related code first).

I think n_neurons should not have a default value because it is not clear what a sensible default would be. 1 is the lower bound, but it is rare to actually want an ensemble with just one neuron. To me it seems better to force the user to be explicit.

I am more ok with the default dimensions=1, but would then go all the way and make the parameter optional to allow nengo.Ensemble(n_neurons).

In either case I would suggest bringing those changes up in the dev meeting if we want to go along with them. They seem somewhat fundamental. (Or just leave as it is now with default=None.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Just to be clear, nengo.Ensemble(Default, Default) isn't a new feature introduced by this PR, that's always been possible. So there isn't any fundamentally new feature being introduced here; it was just bugged before, because the default value wasn't a valid value. So we probably shouldn't leave it as default=None.

Setting it to Unconfigurable would prevent nengo.Ensemble(Default, Default), which would be a minor change in behaviour but pretty unlikely to affect anyone. I'd be slightly inclined to leave it as a default of 1, just because it allows more functionality and there doesn't seem to be any downsides (a user can't accidentally end up confused about the default value, since they have to explicitly request it). But I don't think it matters too much either way, since this is a pretty edge use case. We can definitely talk about it more in the dev meeting if you want to add it to the agenda though 👍.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Here is my thought process: First I'm asking myself should these parameters have a default value? This comes basically down to whether there is some specific value with a special property (e.g. a transform of 1 is implementing an identity transform) or we want to say some value is a good value to use (e.g. firing rates between 200 and 400 Hz). Let us assume there is such a value for n_neurons (or dimensions), then there should not be a problem with going all the way and make the constructor argument itself optional.

But I think there is no reasonable default value for n_neurons. Yes, n_neurons=1 is special in so far as it is the lower bound, but advertising this as a good default value seems wrong to me. Yes, it would allow one to do nengo.Ensemble(Default, Default), but I don't see why someone would want to do that instead of nengo.Ensemble(1, 1) (if someone actually wants a single neuron). What I can see a use case for is something like

with nengo.Config(nengo.Ensemble) as cfg:
    cfg[nengo.Ensemble].n_neurons = 50
    cfg[nengo.Ensemble].dimensions = 1
    a = nengo.Ensemble(Default, Default)
    b = nengo.Ensemble(Default, Default)
    # ...

That, however, already works even with a default of n_neurons=None and I'm ok with that because the user still explicitely sets the number of neurons. It is a bit weird that someone has to pass in Default explicitely (I am not aware of any other situations in the API where that would be the case because usually the constructor arguments get a default of Default) and I might argue that the better way to set n_neurons on a bunch of ensemble would be:

n_neurons = 50
dimenisons = 1
a = nengo.Ensemble(n_neurons, dimensions)
b = nengo.Ensemble(n_neurons, dimensions)

If this is in a subnetwork and someone wants to configure the neuron number from outside, it either needs to be passed in or it needs to be set after the creation of the network. That might seem less powerful than the Config approach, but that approach requires the subnetwork to already pass Default as argument to n_neurons which than requires the network to be created in the context of an appropriate config. It seems that explicitely requesting the number of neurons for that network as a constructor argument (to make that dependency clear) is a better idea. Hence, it seems to me that there is not much of a good use case for nengo.Ensemble(Default, Default) and we might as well set n_neurons=Unconfigurable.

Technically, setting n_neurons=Unconfigurable is a breaking change and I don't think there is anything in the current behaviour to be bothered by (it just enforces that the user sets n_neurons explicitely). So I would be fine either keeping current behaviour or making n_neurons Unconfigurable (as it is unlikely to actually affect anyone).

One final point about about setting n_neurons=1 (or to some other default), but not making it optional: To me it seems that this is hard to explain/document which I usually take as an indication that there might be some better way to structure things. Because we were to set a default value, we ought to document it in the docstring, but at the same time function arguments with a default are also optional in Python. So I am not sure how to document this behaviour that there is a default value, but it only comes into play in this special circumstance where the function argument is set explicitely to the Default.

tl;dr:

  • I am not convinced that there is a good use case for nengo.Ensemble(Default, Default).
  • I do not think that there is a good default value for n_neurons.
  • If we set a default anyways, we should also make the parameter optional.
  • I'm fine with either keeping current behaviour or change the default to Unconfigurable (technically breaking, but unlikely to affect anyone).

@@ -322,3 +323,63 @@ class Test(object):
obsolete = params.ObsoleteParam('obsolete', 'not included in params')

assert set(params.iter_params(Test())) == {'p1', 'p2'}


def test_coerce_defaults(): # noqa: C901
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we might potentially be able to come up with a better name for this test. As far as I can tell it is not really testing the correct coercion of defaults, but rather that setting values in a config does not fail if the coercion cannot be done at that point (because missing information about parent objects).

Copy link
Member Author

Choose a reason for hiding this comment

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

No strong opinions, feel free to change it to whatever you'd like.

@drasmuss
Copy link
Member Author

One general question: Why do we coerce defaults at all? If my reading of the code (in particular SupportDefaultMixin) is correct, when the actual parameter value is set from the default it is coerced anyways.

The idea is to give an informative error message earlier on if the user has set the wrong value. For example, if the user does

with nengo.Network():
    net.config[nengo.Ensemble].eval_points = "abc"  # line 1
    ....
    a = nengo.Ensemble(10, 1)  # line 2

If we don't check the defaults, then the user will get an error at line 2 saying that they're trying to create an Ensemble with invalid eval points, which will be somewhat confusing and difficult to track down since they are not setting the eval points on that ensemble. It's nicer if they get an error at line 1 telling them that the new default value they've set is invalid.

@tbekolay
Copy link
Member

For some additional context, Dan and I also discussed the possibility of having a separate method for coercing defaults, which would do some basic validation but nothing that relies on the object instance. It seemed like too much code for too little benefit, so we decided against it.

@drasmuss
Copy link
Member Author

Added a commit to make n_neurons/dimensions unconfigurable.

Copy link
Collaborator

@jgosmann jgosmann left a comment

Choose a reason for hiding this comment

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

Some more small fixes. LGTM now. 🍰

nengo/params.py Outdated
@@ -445,7 +445,7 @@ def coerce_ndarray(self, instance, ndarray): # noqa: C901

@property
def coerce_defaults(self):
return not ("n_neurons" in self.shape or "dimensions" in self.shape)
return not any(is_string(dim) for dim in self.shape)
Copy link
Member Author

Choose a reason for hiding this comment

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

"..." and "*" are valid shape strings that don't need to disable the default coercion

Copy link
Collaborator

Choose a reason for hiding this comment

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

right, fixed it

CHANGES.rst Outdated
@@ -26,7 +26,7 @@ Release History

- Default values can no longer be set for ``Ensemble.n_neurons`` or
``Ensemble.dimensions``
(`#1372 <https://github.com/nengo/nengo/pull/1372>`__)
(`#1372 <https://github.com/nengo/nengo/pull/1372>`_)
Copy link
Member Author

Choose a reason for hiding this comment

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

This needs to be an anonymous hyperlink, since it is identical to the one below.

Copy link
Collaborator

Choose a reason for hiding this comment

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

my bad, didn't know about anonymous hyperlinks, also fixed.

Copy link
Contributor

@Seanny123 Seanny123 left a comment

Choose a reason for hiding this comment

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

LGTM. flake8 checks are failing, but only due to problems not associated to this PR.

Copy link
Member

@tbekolay tbekolay left a comment

Choose a reason for hiding this comment

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

Made a commit with some minor things, mostly fixing the complexity warning with a dict. Will merge after lunch if no objections!

with nengo.Network() as net:
# NB: Probe must go before Connection
Copy link
Collaborator

Choose a reason for hiding this comment

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

What does NB stand for?
Why must the Probe go before connection?
What do you think about using pytest.mark.parametrize to eliminate the outer for loop?

Copy link
Collaborator

Choose a reason for hiding this comment

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

btw: I like the dictionary approach. :)

Copy link
Member

@tbekolay tbekolay Nov 2, 2017

Choose a reason for hiding this comment

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

NB basically mean important note, thought it was relatively common but I can change it if it's not.

Probe.solver uses connection's default by default. I had changed the for loop to use net.objects (which has all the types) instead of the list and got failures because the probe solver was using weights; this was because the connection default had changed, and the probe solver uses the connection's default by default. I added the note so that other people wouldn't try to do the same and get failures due to dict ordering.

IMO parametrize would inflate the number of tests by a fair bit, and doesn't feel like it'd be any more readable, so I would opt for keeping it as is.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Probe.solver uses connection's default by default. I had changed the for loop to use net.objects (which has all the types) instead of the list and got failures because the probe solver was using weights; this was because the connection default had changed, and the probe solver uses the connection's default by default. I added the note so that other people wouldn't try to do the same and get failures due to dict ordering.

Shouldn't that be solved by moving the with nengo.Network() as net: inside the for loop? That seems better to me because it isolates the indivdual tests and the order of iteration does not matter. (If I'm not overlooking something.)

IMO parametrize would inflate the number of tests by a fair bit, and doesn't feel like it'd be any more readable, so I would opt for keeping it as is.

The output for a test failure might be more readable, but I would have to actually try that out and not sure if I'm feeling that strongly about it ...

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 that be solved by moving the with nengo.Network() as net: inside the for loop?

I don't think that would matter, as you're always within that network's context, so if the connection default is set before the probe default, it will use it.

You could get around it by simply not being in the network's context though; it's not actually necessary for anything the test is doing. Just tested it and it works, so I'll push that change.

@tbekolay
Copy link
Member

tbekolay commented Nov 2, 2017

The appveyor failure seems to be outside our control at the moment, and the TravisCI build will be fixed once rebased. OK if I merge @jgosmann ?

@jgosmann
Copy link
Collaborator

jgosmann commented Nov 2, 2017

👍 Go ahead.

Some parameters rely on attributes of their parent object
to perform the validation, so the validation cannot be performed
until the attribute value is set on that object.

Additionally, Ensemble.n_neurons and Ensemble.dimensions
no longer have a default value set, meaning that they cannot
be configured to have a default.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants