Search code examples
pythonmathematical-optimizationcvxpy

How to constrict your problem to allow a custom-defined "cool-off period"?


I am making use of CVXPY and CVXOPT to solve a problem that allows us to control the running of motors in the cheapest possible ways.

We have existing constraints which consist of, cost, volume, and level which need to be maintained (all of which work, and is only to give a further definition of the problem at hand). We use this to minimize the cost and provide an optimized approach.

Recently, we wish to add a new constraint that will allow for a cool-off period. Which we can set. So if the selection value is 1, the next two periods should be a 0 if this makes sense. Essentially, if the pump runs for a period it should be off for the next two to cool down.

Please see below for a current function that is being used. Currently, no cool-off constraint has been implemented, and wish to get a push in the right direction of how best to implement such a solution.

def optimiser(self, cost_, volume_,v_min,flow_,min_level,max_level,initial_level,period_lengths,out_flow_, errors) :
        """
        This function optimizes the regime possible combinations using convex optimization. We assign and define the problem and uses `GLPK_MI` to solve our problem.

        Parameters
        ----------
        cost_
            Numpy Array
        volume_
            Numpy Array
        v_min
            Float -> minimum volume
        flow_
            Numpy Array
        min_level
            Float -> minimum level
        max_level
            Float -> maximum level
        initial_level
            Float -> initial level
        period_lengths
            Integer -> number of period lengths
        out_flow_
            Numpy Array
        errors
            Exceptions -> error handling

        Returns
        ----------
        selection
            Solved problem with the best possible combination for pumping regime.
        """
        FACTOR =  0.001*1800*self.RESERVOIR_VOLUME
        input_flow_matrix=np.zeros((max(period_lengths),len(period_lengths)))
        for i,l in enumerate(period_lengths):
            input_flow_matrix[:l,i]=1
        selection = cp.Variable(shape=cost_.shape,boolean=True)
        assignment_constraint = cp.sum(selection,axis=1) == 1
        input_flow_= cp.sum(cp.multiply(flow_,selection),axis=1)
        input_flow_vector=cp.vec(cp.multiply(input_flow_matrix,np.ones((max(period_lengths), 1)) @ cp.reshape(input_flow_,(1,len(period_lengths)))))
        res_flow= (input_flow_vector-cp.vec(out_flow_))
        res_level=cp.cumsum(res_flow) * FACTOR + initial_level
        volume_= cp.sum(cp.multiply(volume_,selection))
        volume_constraint = volume_ >= v_min
        min_level_constraint = res_level >= min_level
        max_level_constraint = res_level <= max_level

        if errors is LevelTooLowError:
            constraints = [assignment_constraint, max_level_constraint, volume_constraint]
        elif errors is LevelTooHighError:
            constraints = [assignment_constraint, min_level_constraint, volume_constraint]
        elif errors is MaxVolumeExceededError:
            constraints = [assignment_constraint, min_level_constraint, volume_constraint]
        else:
            constraints = [assignment_constraint, max_level_constraint, min_level_constraint, volume_constraint]

        cost_ = cp.sum(cp.multiply(cost_,selection))
        assign_prob = cp.Problem(cp.Minimize(cost_),constraints)
        assign_prob.solve(solver=cp.GLPK_MI, verbose=False)   
        return selection

Sample of data which we work with

Here is our cost_ array

[[0.   0.8 ]
 [0.   0.8 ]
 [0.   0.8 ]
 [0.   0.8 ]
 [0.   0.8 ]
 [0.   0.8 ]
 [0.   0.8 ]
 [0.   0.8 ]
 [0.   0.9 ]
 [0.   0.9 ]
 [0.   0.9 ]
 [0.   0.9 ]
 [0.   0.9 ]
 [0.   0.9 ]
 [0.   0.9 ]
 [0.   0.9 ]
 [0.   1.35]
 [0.   1.35]
 [0.   1.35]
 [0.   1.8 ]
 [0.   1.2 ]]

And here is our volume_ array

[[    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 34200.]
 [    0. 51300.]
 [    0. 51300.]
 [    0. 51300.]
 [    0. 68400.]
 [    0. 51300.]]

I hope my problem has been well defined for anyone to understand what I wish to do at the moment. If anything else is needed, I can happily provide this for anyone.

Thanks in advance.


Solution

  • Something like this is sometimes used in power generation. A generator may have to cool down before allowing it to be turned on again.

    Let's assume we have a binary variable x[t] indicating if the generator is "on" at period t i.e.

     x[t] = 1 if machine is "on"
            0               "off"
    

    Then we want to require:

    if x[t-1]=1 and x[t]=0 => x[t+1] = 0
    

    This reads as: if turned off at t, then also stay off at t+1. This can be written as a linear constraint:

    x[t+1] <= x[t] - x[t-1] + 1    for all periods t
    

    If we read "if the pump runs for a period it should be off for the next two to cool down" as indicating that in addition, the maximum up-time is 1, then we can simply write:

    x[t-1] + x[t] + x[t+1] <= 1  for all t
    

    This will allow patterns like [0,0,0],[1,0,0],[0,1,0],[0,0,1] but forbids patterns like [1,1,1],[0,1,1],[1,1,0],[1,0,1].