Search code examples
pythonscipyscipy-optimizenamed-parametersfsolve

scipy optimize: how to make it work with a function that has only optional arguments?


Let's say I have the following function (very simplified, to explain the gist of the issue):

def func(a=None, b=None, c=None):    
    return 100-(a+b*c)

The purpose of this function is eventually to find the root, i.e. solve for one parameter, when the other two are known, such as the result equals 0.

If I want to use scipy's fsolve function to solve for a, when b and c are known, this is not a problem:

>>> from scipy import optimize
>>> dat = {"b":1, "c":2}
>>> optimize.fsolve(lambda x: func(x, **dat), x0=0)

array([98.])

If I want to solve for any parameter that is not in the first position (c, for instance), it becomes a problem:

>>> dat = {"a":1, "b":2}
>>> optimize.fsolve(lambda x: func(x, **dat), x0=0)

TypeError: func() got multiple values for argument 'a'

I guess a solution would be to set as a fixed "first position" parameter the parameter that has a None value, but I have no idea how to do that, or if it's even possible.

Of course, the "dumb" solution would be to create three different functions to take into account all possible use cases (when a is unknown, when b is unknown, and when c is unknown), but it does not seem a very efficient method to me. In addition, I'll likely encounter a similar problem in the future with functions having much more parameters than that.

Of course, a single one parameter can be set to None but I handle this issue separately, so we can assume that there's always a single one unknown value --the only problem is that this parameter with an unknown value can be any one of the parameters. I want to be able to "tell" scipy to solve for this None value, no matter the position of its parameter.


Solution

  • We'll define a function, called get_partial, which takes a function and a dictionary of the keyword arguments you wish to freeze. It will then return a new function that takes the remaining keyword argument as a positional argument.

    functools.partial doesn't quite do the job here: it does allow you to freeze all keyword arguments except for one, but it doesn't convert the last keyword argument to a positional argument.

    import inspect
    from scipy import optimize
    
    def get_partial(f, fixed_kwargs):
        all_kwargs = inspect.signature(f).parameters.keys()
        bound_kwargs = fixed_kwargs.keys()
        free_kwarg = (all_kwargs - bound_kwargs).pop()
        return lambda x: f(**(fixed_kwargs | {free_kwarg: x}))
    
    def func(a=None, b=None, c=None):
        return 100 - (a + b * c)
    
    dat = {"a": 1, "b": 2}
    
    optimize.fsolve(get_partial(func, dat), x0=0)