pythonscipyconstraintsscipy-optimize

Why is this constraint function so much slower than a similar one and how do I increase the speed in Scipy Optimize?


I have an optimization that has around 22 constraint functions. I tried to turn that into a single constraint function and the optimizer is taking 10x as long. Any way to reduce the speed?

The below are the original constraint functions. I only included 2 but there are like 22 of them, each with hard coded values for the inequality constraint. The parameters are the values the optimizer is looking for, and the optimizer is looking for like 50 values. For example, for the first constraint, it basically it says, the first 2 parameter values when summed must not exceed 129:

def MaxConstraint000(parameters): 
    _count = np.sum(parameters[0:2])
    return (129 - _count)

def MaxConstraint001(parameters): 
    _count = np.sum(parameters[2:5])
    return (2571 - _count)

_Constraints = ({'type': 'ineq', 'fun': MaxConstraint000}
                , {'type': 'ineq', 'fun': MaxConstraint001})

To simplify my code and instead of pre-determining the location of parameters I tried something like this, where I supply a key value that pulls in the index locations for the parameter values as well as the constant values [129, 2571, etc...] from a data frame. DFData has the same number of rows as the number of parameters. The constraint is identical as the first one, other than I supply a keyValue, which allows me to look up the max value as well as the index locations.

def MaxConstraint(parameters, keyValue):
    _parameters= np.array(parameters)
    _index = np.where(DFData['KeyValues'].values == keyValue)[0] 
    
    _count = np.sum(_parameters[_index])
    _target = DFMaxValues.loc[[keyValue], ['MaxValue']].values[0][0]
    return (_target - _count )

_Constraints = ({'type': 'ineq', 'fun': MaxConstraint, 'args': ('keyValue1', )}
                , {'type': 'ineq', 'fun': MaxConstraint, 'args': ('keyValue2', )}

This results in 10 times longer execution. How do I get it down to approximately the same speed as the first one? I would prefer the second implementation as therefore instead of going into each constraint and changing the MaxValue, I can just update a dictionary or CSV file instead. Additionally, if the rows of the data get mixed, I don't have hard coded index values.

Thanks!


Full set of constraints:

def MaxConstraint000(parameters):
    _count = np.sum(parameters[0:2])
    return (129 - _count)

def MaxConstraint001(parameters): 
    _count = np.sum(parameters[2:5])
    return (2571 - _count)

def MaxConstraint002(parameters): 
    _count = np.sum(parameters[5:8])
    return (3857 - _count)

def MaxConstraint003(parameters): 
    _count = np.sum(parameters[8:10])
    return (823 - _count)

def MaxConstraint004(parameters): 
    _count = np.sum(parameters[10:13])
    return (823 - _count)

def MaxConstraint005(parameters): 
    _count = np.sum(parameters[13:16])
    return (3857 - _count)

def MaxConstraint006(parameters): 
    _count = np.sum(parameters[16:21])
    return (4714 - _count)

def MaxConstraint007(parameters): 
    _count = np.sum(parameters[21:25])
    return (3429 - _count)

def MaxConstraint008(parameters): 
    _count = np.sum(parameters[25:28])
    return (3429 - _count)

def MaxConstraint009(parameters): 
    _count = np.sum(parameters[28:30])
    return (3429 - _count)

def MaxConstraint010(parameters): 
    _count = np.sum(parameters[30:33])
    return (2914 - _count)

def MaxConstraint011(parameters):
    _count = np.sum(parameters[33:38])
    return (6000 - _count)

def MaxConstraint012(parameters): 
    _count = np.sum(parameters[38:43])
    return (6000 - _count)

def MaxConstraint013(parameters): 
    _count = np.sum(parameters[43:45])
    return (429 - _count)

def MaxConstraint014(parameters): 
    _count = np.sum(parameters[45:47])
    return (1457 - _count)

def MaxConstraint015(parameters): 
    _count = np.sum(parameters[47:51])
    return (4286 - _count)

def MaxConstraint016(parameters): 
    _count = np.sum(parameters[51:53])
    return (2143 - _count)

def MaxConstraint017(parameters): 
    _count = np.sum(parameters[53:57])
    return (4286 - _count)

def MaxConstraint018(parameters): 
    _count = np.sum(parameters[57:64])
    return (2143 - _count)

def MaxConstraint019(parameters): 
    _count = np.sum(parameters[64:67])
    return (2571 - _count)

def MaxConstraint020(parameters): 
    _count = np.sum(parameters[67:72])
    return (1714 - _count)

def MaxConstraint021(parameters): 
    _count = np.sum(parameters[72:75])
    return (4286 - _count)
    
_Bounds = ((0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000)
           , (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000)
           , (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000)
           , (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000)
           , (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000)
           , (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000)
           , (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000)
           , (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000)
           , (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000), (0, 10000)
           , (0, 10000), (0, 10000), (0, 10000))

_Constraints = ({'type': 'ineq', 'fun': MaxConstraint000}
                , {'type': 'ineq', 'fun': MaxConstraint001}
                , {'type': 'ineq', 'fun': MaxConstraint002}
                , {'type': 'ineq', 'fun': MaxConstraint003}
                , {'type': 'ineq', 'fun': MaxConstraint004}
                , {'type': 'ineq', 'fun': MaxConstraint005}
                , {'type': 'ineq', 'fun': MaxConstraint006}
                , {'type': 'ineq', 'fun': MaxConstraint007}
                , {'type': 'ineq', 'fun': MaxConstraint008}
                , {'type': 'ineq', 'fun': MaxConstraint009}
                , {'type': 'ineq', 'fun': MaxConstraint010}
                , {'type': 'ineq', 'fun': MaxConstraint011}
                , {'type': 'ineq', 'fun': MaxConstraint012}
                , {'type': 'ineq', 'fun': MaxConstraint013}
                , {'type': 'ineq', 'fun': MaxConstraint014}
                , {'type': 'ineq', 'fun': MaxConstraint015}
                , {'type': 'ineq', 'fun': MaxConstraint016}
                , {'type': 'ineq', 'fun': MaxConstraint017}
                , {'type': 'ineq', 'fun': MaxConstraint018}
                , {'type': 'ineq', 'fun': MaxConstraint019}
                , {'type': 'ineq', 'fun': MaxConstraint020}
                , {'type': 'ineq', 'fun': MaxConstraint021}
               )

############# Solve Optim Problem ############### 
_OptimResultsConstraint = scipy_opt.minimize(ObjectiveFunction
                                             , x0 = [10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,
                                                   10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,
                                                   10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,
                                                   10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,
                                                   10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,
                                                   10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,
                                                   10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,
                                                   10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000,
                                                   10000, 10000, 10000] # starting guesses
                                             , method = 'trust-constr' # trust-constr, SLSQP
                                             , options = {'maxiter': 1000000000}
                                             , bounds = _Bounds
                                             , constraints = _Constraints) 

Solution

  • Your multiple constraints are really just one constraint. The most compact way to represent it is a sparse CSR array:

    import numpy as np
    from scipy.optimize import LinearConstraint
    import scipy.sparse
    
    A = scipy.sparse.csr_array(
        (
            np.ones(75),    # data
            np.arange(75),  # indices
            (
                0, 2, 5, 8, 10, 13, 16, 21, 25, 28,
                30, 33, 38, 43, 45, 47, 51, 53, 57,
                64, 67, 72, 75,
            ),
        ),
    )
    
    constraint = LinearConstraint(
        A=A,
        ub=(
            129, 2571, 3857, 823, 823, 3857, 4714, 3429, 3429, 3429,
            2914, 6000, 6000, 429, 1457, 4286, 2143, 4286, 2143, 2571,
            1714, 4286,
        ),
    )