-
-
Notifications
You must be signed in to change notification settings - Fork 74
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
Expressing dependencies between parameters #160
Comments
JAB:
|
@jbednar Could you clarify what you mean by this in practice? Do we really want to declare what parameters a method depends on all the time? What happens if you forget to declare it? It seems this is bound to increase the complexity of param as well as complicating the declaration of a Parameterized class. Having not thought about this too deeply my initial suggestion is simply to introduce set_hooks, which will let a user set up whatever dependencies they want between parameters without having to introduce some complex API for declaring dependencies in some special way. I'm happy to be persuaded of some other solution but based on your description I currently cannot imagine what that would look like. |
I think the suggestion is for the option to say that a particular method depends on (some) parameters, not for it to be required to do so.
I agree param should be kept simple, but I also think it's important for param not to suggest a way of doing things that leads to complicated/varied solutions by users - if possible! Sorry for no details yet. I have been thinking about it in the background but can't say what exactly I propose yet. |
There would be absolutely no requirement that one would have to declare that a method depends on a Parameter, no more than there is a requirement that you declare docs, bounds, etc. If you do declare those things, then other code can make use of this additional information, but if you don't, it can't, and so be it. We can probably make the default behavior simply to re-run the relevant method whenever any parameter changes, leaving such declarations only for cases when someone wants to express finer control over which bit to re-run. I'm happy for If we can achieve this separation, we'll have a hope of maintaining things in the long run. If not, we'll take the hit, but I don't want to take it just because we haven't tried to do it properly first. |
Just to clarify I didn't think there would be any requirement to always have to express dependencies I just have trouble envisioning in what cases you would declare such dependencies, in what cases you wouldn't, how you would express them or indeed what it actually means to declare dependencies on specific methods. I'm just trying to get an idea of what your actual proposal is. Using a class Example(param.Parameterized):
A = param.Number(default=10)
B = param.Number(default=5)
@link('A')
def link_a_to_b(self, **parameters):
self.B = parameters['A']/2. The equivalent using class Example(param.Parameterized):
A = param.Number(default=10)
B = param.Number(default=5)
def __init__(self, **params):
super().__init__(**params)
self.add_hooks({'A': lambda A: setattr(self, 'B', A/2.)}) Is this anywhere close to what you were imagining? |
Also not thinking too deeply about it, here is my suggestion: class Example(param.Parameterized):
A = param.Number(default=10)
B = param.Number(default=5)
@param.dependencies([A],[B])
def link_a_to_b(self, **parameters):
self.B = parameters['A']/2. The only difference is 1) no need to declare parameters as strings as the actual objects should be in the class namespace (and we always write our methods after our parameter declarations) and 2) it is nice to declare both the inputs (parameter reads) and outputs (parameter writes) together. |
Of course this could also be supported simultaneously as the decorator that declares reads and writes together: class Example(param.Parameterized):
A = param.Number(default=10)
B = param.Number(default=5)
@param.reads([A])
@param.writes([B])
def link_a_to_b(self, **parameters):
self.B = parameters['A']/2. Probably better if you do only reads or only writes - otherwise, I don't like the wasted vertical whitespace. |
We've implemented what we think we need. See #262 for any leftover issues. |
None of the trivial examples seem to capture the most obvious use of dependent parameters. The most trivial example I can think of to demonstrate the point is to have a dropdown list of countries, with another list that needs to contain the states/territories based on one or more selected countries. This SHOULD be trivial (and is for most implementations), but it seems impossible in hv. |
@jl-massey The widgets in holoviews long precede any ability to express dependent parameters. We are currently building a new library called panel which, among many other things, will make it easy to set up dependencies between widgets with or without the use of |
(Attempt to capture notes/plan from PR, JB, JLS, CB - feel free to edit and/or comment.)
Users of param GUI widget toolkits often want to be able to control the range or type of some widgets based on the values of others.
For example, consider specification of a date range: one might want to allow a user either to select start date and end date, or to select a start date and duration. Or the value in one numeric widget might affect the range of another slider. Or a widget should be greyed out or not depending on the value of another widget.
Beyond GUI toolkits, being able to express dependencies between parameters seems like something that might be useful to any user of param. Many of us have occasionally felt the need for linking parameters while using param in the real world, but we've never actually needed it enough to justify the added complexity, and have solved our problems a different way.
We should therefore consider at what level we should support expressing dependencies between parameters.
Solve at param level?
It's likely that many of the dependencies desired by GUI widget users could be expressed at the domain level (i.e. using param), so it could be valuable for param to support expressing those dependencies in a general way, such as by having parameters register that they subscribe to other parameters' values (being careful to avoid a vicious circle) and allowing code to be registered to respond to such changes. Solving this at the param level would handle a large class of problems across all the widget toolkits, so could be very beneficial. However, it has the danger of making Param impossible to understand (which data flows where, when?) and maintain, if not done well.
Solve at param GUI toolkit level?
Since it's not yet clear if the above can be done well, or would inherently have the problems described above when sufficiently advanced to cover the typical real-world cases where these issues arise, an alternative solution would be to extend the toolkit-specific classes like paramnb.Widgets() to allow an update method to be registered that gets called whenever a parameter is changed. Such a method would then check for changes in parameters and apply changes to others that depend on it. This seems straightforward to implement, and would allow us to discover exactly what it is we might want to support in param.
However, this solution has several disadvantages over the param-level solution:
Doesn't help non-gui users and would have to be implemented for each widget toolkit.
The GUI code would have to know about the underlying objects. Although a convention could be adopted whereby if specific parameter names (e.g. start_date, end_date, duration) are detected, the appropriate widgets and logic are automatically used (ensuring that all three of those values are set consistently regardless of how the data was input by the user).
Proposal
We propose to solve at the GUI toolkit level, gather real-world use cases together as a set of demos, and then consider again about implementing at the param level. We take the risk that we end up just using the GUI level solutions forever, and code based on them spreads everywhere.
Additional note: megawidgets
While implementing the above proposal, note that some things - such as being able to select date ranges flexibly - cut across nearly every application domain, and so it would make sense to encapsulate at the widget level. E.g. date range selection logic could be encapsulated into a reusable megawidget (for each GUI toolkit) that at bottom supplies a concrete date range for Param to consume, via a flexible user interface that could select between all typical options like explicit start and end dates, start date plus duration, end date minus duration, duration up to today, duration up to the most recent start of week/end of week/start of month/end of month, etc.
Advantages: fully client side (and thus responsive e.g. considering case of JS widgets communicating with python over the internet) and would prevent downstream code from having to worry about any of these issues. For anything covered by such a megawidget, the underlying Param-based code only needs to express that it needs a date range, and the toolkits will handle the rest. I.e. using megawidgets could cut down on the complexity of parameter dependency/linking support required.
Disadvantages: Would need to be implemented for each widget toolkit, more or less from scratch. Jim already proposed a simpler version of this for ipywidgets (jupyter-widgets/ipywidgets#1118), but it was rejected as best being implemented as a separate library, so it might be a struggle to get each such library to accept responsibility for this functionality.
The text was updated successfully, but these errors were encountered: