Search code examples
pythondictionaryscipyleast-squares

Passing a dictonary to `scipy.optimize.least_squares`


I have a couple of functions that are defined in an external library. I cannot change the arguments or contents of these functions. Take as an example the following functions (although the originals are much more complicated):

def func1(info: dict) -> float:
    return 1 - (1.5 * info["b"] - info["a"])

def func2(info: dict) -> float:
    return 1 - (np.exp(info["c"]) - info["a"])

I have an initial guess and I'm trying to apply scipy.optimize.least_squares to find the optimal values to minimize func1 or func2 (not both at the same time), i.e. the goal would be something like this

import scipy

def func1(info: dict) -> float:
    return 1 - (1.5 * info["b"] - info["a"])

def func2(info: dict) -> float:
    return 1 - (np.exp(info["c"]) - info["a"])

initial_dict = {"a" : 5, "b" : 7}
result = scipy.optimize.least_squares(func1, initial_dict)
initial_dict["c"] = 3
result2 = scipy.optimize.least_squares(func1, initial_dict)

The issue is that least_squares only accepts floats, and not dicts. I think one could cast the values of the dict to a list and write a "wrapper" function that turns the list back into a dict, i.e.

def func1_wrapped(lst: list[float]) -> float:
    a, b, c = lst
    tmp_dict = {"a" : a, "b": b, "c": c}
    return func1(tmp_dict)

result1 = scipy.optimize.least_squares(func1_wrapped,[5, 7, 3])

Is something like this reasonable? Is there a better, more efficient way of doing this?


Solution

  • One possible way to wrap is

    import scipy.optimize
    import numpy as np
    
    def dict_least_squares(fn, dict0, *args, **kwargs):
        keys = list(dict0.keys());
        result = scipy.optimize.least_squares(
            lambda x: fn({k:v for k,v in zip(keys, x)}), # wrap the argument in a dict
            [dict0[k] for k in keys], # unwrap the initial dictionary
            *args, # pass position arguments
            **kwargs # pass named arguments
        )
        # wrap the solution in a dictionary
        try:
            result.x = {k:v for k,v in zip(keys, result.x)}
        except:
            pass;
        return result;
    
    

    This maintains the interface of original least squares function, by forwarding arbitrary position arguments *args, or named arguments **kwargs.

    Usage examples

    def func1(info: dict) -> float:
        return 1 - (1.5 * info["b"] - info["a"])
    initial_dict = {"a" : 5, "b" : 7}
    dict_least_squares(func1, initial_dict)
    

    Gives

     active_mask: array([0., 0.])
            cost: 1.2378255801353088e-15
             fun: array([4.97559158e-08])
            grad: array([ 4.97559158e-08, -7.46338734e-08])
             jac: array([[ 1.        , -1.49999999]])
         message: '`xtol` termination condition is satisfied.'
            nfev: 37
            njev: 16
      optimality: 7.463387344664022e-08
          status: 3
         success: True
               x: {'a': 6.384615399632931, 'b': 4.92307689991801}
    

    Then

    def func2(info: dict) -> float:
        return 1 - (np.exp(info["c"]) - info["a"])
    initial_dict["c"] = 3
    dict_least_squares(func2, initial_dict)
    

    gives

     active_mask: array([0., 0., 0.])
            cost: 4.3463374554994224e-17
             fun: array([-9.32345157e-09])
            grad: array([-9.32345157e-09,  0.00000000e+00,  2.12348517e-11])
             jac: array([[ 1.        ,  0.        , -0.00227757]])
         message: '`gtol` termination condition is satisfied.'
            nfev: 41
            njev: 20
      optimality: 9.323451566345398e-09
          status: 1
         success: True
               x: {'a': -0.9977224357504928, 'b': 7.0, 'c': -6.0846446250890684}