-
Notifications
You must be signed in to change notification settings - Fork 280
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
Support for model functions with keyword-only and positional-only arguments #976
Comments
@schtandard I may not understand your proposed changes.
That's not a change.
That's not a change either.
That's not a change either. So, what change are you proposing? I would be inclined to allow but ignore the bare Just to be clear: a) The first argument of the function does not need to be an independent parameter. That is the default, but it can be specified otherwise. b) Positional arguments will generally become Parameters. c) All Parameters need a default value. Positional arguments have a default value of I don't think we need to change any of these. |
Yes, it is. As I said, explicitly specified independent variables are not currently amended, which is why arguments can be "lost". Here's the example again. def foo(x, a=1, b=1, kind='linear'):
pass
mdl = lmfit.Model(foo, independent_vars=['x'], parameters=['a'])
print(mdl.independent_vars, mdl.param_names)
# ['x'] ['a', 'b']
# 'kind' got lost.
Yes, it is. Currently, the first argument without a default argument is used. If all arguments have default values, an error is raised. In the new case of keyword-only arguments, such an argument may exist but not be the first argument. Here are the example signatures for the two relevant cases again. def foo(x=3, a=1, b=1, kind='linear'): def foo(x=3, *, a, b=1, kind='linear'):
Not supporting positional-only arguments is another option, of course.
But this could be changed as outlined in my post above, no? Adapting
All of this is clear, yes, but I see a danger of confusing terminology: You (and the |
@schtandard I won't get into an argument about how the code works. I am certainly willing to leave the use of a bare "*" unsupported. Adding "support" for it would be to ignore it. It would not affect the default behavior for how lmfit.Model distinguishes independent variables from Parameters.
Lmfit.Model always calls with keyword arguments, assigning a value even when no default is provided. It never uses positional-only arguments. It seems to me that adding support for bare * could cause more confusion, including "well, if you support bare '*', why not bare '/'"? |
Well ok, if always calling using keyword arguments is an unalterable design choice, then supporting positional-only arguments is impossible, of course. Keyword-only arguments are vastly more common in any case (and considered good practice in common cases, as far as I know), so that's also a reasonable choice apart from implementation blockers. I mainly tried to include positional-only arguments because you brought them up in the discussion; I've never used them myself. For me, the more "confusing" thing is that keyword-only arguments are not supported, especially since arbitrary keyword arguments (i.e. Now for me just making the one-line change that ignores the Is it really the desired behavior that with the model function signature def foo(x=3, *, a, b=1, kind='linear'):
And is it really the desired behavior that with the model function signature def foo(x=3, a=1, b=1, kind='linear'): no automatic selection of independent variables can be performed? |
failing with a non-helpful error message seems like the most important problem I've seen here so far. I don't have a strong opinion on what the preferred behavior for guessing independent variables with
should be. I find that syntax sort of "weird beyond belief". In this case, foo() must be called with a keyword parameter "a" but no default value is given in the signature, the traditional signal of a keyword argument. x can be called as a positional argument, but has a default value. So
are all allowed. But of course
is a syntax error. Because that one just makes no sense at all. And clarity is a goal...? Sure. I suppose we should try to support ignoring the bare It is going to be very hard to convince me to support Lmfit programmatically inspects and calls the user's Model function. We can (and we do) make some assumptions and demands on the call signatures that we support. Whether we support bare |
Alright, let me know once you've thought about it. I feel that making the first argument an independent variable regardless of default values is worthwhile even without supporting keyword-only arguments ( |
@schtandard I had a chance to play with That would look change the code at Line 527 in 6f8551a
pos_args = []
sig = inspect.signature(self.func)
for fnam, fpar in sig.parameters.items():
if fpar.kind == fpar.VAR_KEYWORD:
keywords_ = fnam
elif fpar.kind in (fpar.POSITIONAL_ONLY,
fpar.KEYWORD_ONLY,
fpar.POSITIONAL_OR_KEYWORD):
default_vals[fnam] = fpar.default
if (isinstance(fpar.default, (float, int, complex))
and not isinstance(fpar.default, bool)):
kw_args[fnam] = fpar.default
pos_args.append(fnam)
elif fpar.default == fpar.empty:
pos_args.append(fnam)
else:
kw_args[fnam] = fpar.default
indep_vars.append(fnam)
elif fpar.kind == fpar.POSITIONAL_ONLY:
raise ValueError("positional only arguments with '/' is not supported")
elif fpar.kind == fpar.VAR_POSITIONAL:
raise ValueError(f"varargs '*{fnam}' is not supported") with that change made locally, this: import inspect
from lmfit import Model
def f(x=3, *, a, b=1, kind='linear'):
return
for name, arg in inspect.signature(f).parameters.items():
print(f"name={name}, kind={arg.kind}, default={arg.default}")
mod = Model(f)
print('Independent Variables: ', mod.independent_vars)
print('Parameter root names: ', mod._param_root_names)
print('Default values: ', mod.def_vals) gives
I think that would work for your case, and seems perfectly reasonable to me. Selecting To be clear, this would also explicitly forbid bare I still have the feeling this is going to cause some pain somewhere down the road. But I do think that the current status of failing (and with not very clear messages) for functions with that signature is not acceptable. |
Perfect, then we are in agreement.
That looks right, except that you need to remove
I strongly prefer the proposed behavior over choosing
I don't think we need to, but I agree that it's best to do so for now. If somebody comes along with a compelling use-case, it can still be added. (One way would be to automatically create a "pass-through" function with the same signature except that all arguments can be given by keyword that passes the values through to the original function and then use that as the model function.) This is probably something you will want to do separately, if at all, but the whole parsing logic could be simplified quite a bit after this change, I think. The distinction between |
I guess I should open a separate issue about the possibility of "losing" function arguments.. Or maybe I'll just make a PR after the other stuff is dealt with; the solution seems obvious in this case. |
@schtandard Thanks for getting back on this. Yeah, I basically agree with everything you say.
I agree that in the example, making I agree that the code might be made simpler... Model has a lot going on! I agree that it ought to be technically possible to make POSITIONAL_ONLY work - it would be more work to track those, but possible. The general case for POSITIONAL_ONLY (some names used in the signature can change, I guess) seems much weaker to me than KEYWORD_ONLY (don't rely on argument order after this point). The whole design of Model does depend on using the actual names in the signature. Anyway, I think we could not follow the "POSITIONAL_ONLY" intention anyway. With def func(x, /, amp=1, center=0, sigma=1):
...
model = Model(func)
pars = model.make_params(amp=3, center=30, sigma=4) you would still evaluate that with init = model.eval(pars, x=99) which seems kind of odd. I can start a PR, but if you would like to contribute to it, that would be great. |
First Time Issue Code
Not applicable.
Description
As mentioned in this discussion, models currently do not support model functions with keyword-only or positional-only arguments. It would be nice if that support could be added. Here's an example for keyword-only arguments.
A quick fix for supporting these only requires adding
fpar.KEYWORD_ONLY
to the tuple inlmfit-py/lmfit/model.py
Line 532 in 3935485
Supporting positional-only arguments will require changes to
Model.make_funcargs()
(and functions using it), as it currently returns all arguments as keyword-arguments. My approach would be to have it return two values: A tuple of positional arguments and a dictionary of keyword-arguments. (I would probably return arguments that can be given in either form as positional arguments.) This function does not start with a_
but it does seem to be undocumented, so I'm unsure if changing its interface is acceptable? (If not, an alternative would be creating a new method_make_funcargs()
with the new behavior that is used internally, while keepingmake_funcargs()
with unchanged behavior, raising an exception when positional-only arguments are involved.)In any case, there are some further things to contemplate related to the automatic determination of independent variables. These are not exclusively due to the addition of keyword-only arguments, but additional special cases arise. Not addressing this immediately would lead to being locked into the current behavior for the new argument specifications, which is why I bring it up here. Let me know if I should rather open a separate issue. My understanding of the current situation is this:
Here are some possible function signatures and what results when using them as model functions (i.e.
mdl = lmfit.Model(foo)
, as above) afterfpar.KEYWORD_ONLY
has been added to the line mentioned above.Error, because there is no argument without a default value (though the error message
IndexError: pop from empty list
is a bit obscure).a
andkind
are independent variables,x
andb
are parameters.x
andkind
are independent variables,a
andb
are parameters.To me, some of these are a bit counter-intuitive. The second case is especially strange, certainly due to the algorithm not expecting arguments without default values to come after ones with a default, which can only happen with keyword-only arguments.
At the risk of getting side-tracked: When explicitly specifying independent variables and parameters, it seems that unspecified arguments are added to the parameters (unless they have non-numerical default values), but none are added to the independent variables. This way, function arguments can be "lost" and become inaccessible through the model methods.
I would propose updating the rules as follows:
I think these rules should leave the behavior of all currently supported model function unchanged and eliminate some of the strangeness mentioned above. (No arguments can be "lost", they are all either independent variables or parameters. In all three of the example above,
x
andkind
would be independent variables.)I am in principle happy to provide a PR for this (once the details above are decided), but I am very pressed for time and since the necessary changed turned out not to be so tiny, I would only get to it maybe after Christmas.
A Minimal, Complete, and Verifiable example
See above, here's the main one again.
Fit report:
Actually, fit report creation fails with the following traceback.
Error message:
Version information
Link(s)
discussion
The text was updated successfully, but these errors were encountered: