pythonpyomo

Workaround for Pyomo constraint that evaluates the max / min of an optimization variable


I am currently working on implementing a MINLP optimization problem in Python. To solve the problem, I am using the Pyomo environment and the Couenne solver (Version 0.2.2).

All constraints have been formulized without any issues so far, except for the following one. Specifically, the challenge is to formulate a constraint for an optimization variable, let's call it

model.x = pyo.Var(range(0, 25), bounds=(Min_value, Max_value), domain=pyo.NonNegativeReals)

such that:

The maximum and minimum values of model.x should have a minimum separation of S.

In other words, max(model.x) - min(model.x) >= S.

Unfortunately, you cannot use the built-in Python max() and min() functions directly.

def Min_Max(model):
    return max(model.x)- min(model.x) >= S

model.Const_MinMax =  pyo.Constraint(rule=Min_Max)

The following error occurs:

ValueError: Invalid constraint expression.
The constraint expression resolved to a trivial Boolean (False) instead of a Pyomo object.
Please modify your rule to return Constraint.Infeasible instead of False.

In Python, for some reason, it only takes the maximum and minimum indices of model.x (which are integer values) and subtracts them. It is evident that a Boolean expression is returned, which is a Incorrect Pyomo data type.

Possible data types that could be used i think are: <class 'pyomo.core.expr.numeric_expr.LinearExpression'> or <class 'pyomo.core.expr.relational_expr.EqualityExpression'> <class 'pyomo.core.expr.relational_expr.InequalityExpression'>

Using max(pyo.value(model.x[i]) for i in range(0, 25)) is also not feasible, as it only checks the initialized value of model.x at the beginning, and this value is not retrieved during the optimization process.

In addition, I tried to find the minimum and maximum using a loop and the use of if statements. This also did not work, as if conditions are not allowed (again due to the Boolean context). Error message:

PyomoException: Cannot convert non-constant Pyomo expression (x[1]  <  x[0]) to bool.
This error is usually caused by using a Var, unit, or mutable Param in a
Boolean context such as an "if" statement, or when checking container
membership or equality. For example,
m.x = Var()
if m.x >= 1:
    pass
and
m.y = Var()
if m.y in [m.x, m.y]:
    pass
would both cause this exception.

However, there is the pyo.Expr_if condition provided by Pyomo. Therefore, I tried to structure the loop as follows.

 def Min_Max (model):
       Min=model.x[0]
       Max=model.x[0]
       for i in range(0,25):  
          Max=pyo.Expr_if(model.x[i]>Max,model.x[i],Max)
          Min=pyo.Expr_if(model.x[i]<Min,model.x[i],Min)       
       return Max-Min>= S 

model.Const_MinMax =  pyo.Constraint(rule=Min_Max)

Unfortunately, this also did not lead to success. While the solver no longer gives an error message, it does not start solving either. The likely cause for this is the highly nested if conditions created by the loop.

My question now is whether anyone has a workaround on how to implement this condition? Is it perhaps recommended to use an environment other than Pyomo if I want to implement this condition?

If there are still questions regarding the condition or if further explanations are needed, let me know.


Solution

  • Well, you've tried all of the "illegal" ways:

    So, the challenge is how to make it linear. The problem would be much easier if you wanted to enforce the maximal separation between x values. If that were the case, you will need:

    Your problem is a bit more complicated because you want to enforce a minimal separation. An example below. I think you are probably stuck doing the pair-wise comparisons as I show in the constraint. Then you have a secondary constraint to basically enforce that the separation constraint is valid at least one time.

    In order to make this mechanism work, you also need a Big-M value to limit make the separation constraint nonsensically low in the case where it is not the selected value. You should choose M appropriately. Also, you should be using pyomo Sets wherever possible, not range(), it is just a ton easier to write and troubleshoot or modify.

    The example just tries to minimize the sum of x, but enforces a minimal delta. You can see x[4] "takes one for the team."

    CODE:

    import pyomo.environ as pyo
    
    delta = 5  # the required sep between at least 2 elements
    M = 100  # the maximum possible separation
    m = pyo.ConcreteModel()
    
    ### SETS
    m.S = pyo.Set(initialize=range(5))
    
    ### VARS
    m.x = pyo.Var(m.S, domain=pyo.NonNegativeReals)
    m.selected = pyo.Var(m.S, m.S, domain=pyo.Binary,
                         doc='Selected indices for the pair with required delta')
    
    ### OBJ:  minimize sum of x
    m.obj = pyo.Objective(expr=sum(m.x[s] for s in m.S))
    
    
    ### CONSTRAINTS
    @m.Constraint(m.S, m.S)
    def delta_met(m, s1, s2):
        return m.x[s1] - m.x[s2] >= delta * m.selected[s1, s2] - M * (1 - m.selected[s1, s2])
    
    
    m.requirement_met = pyo.Constraint(expr=sum(m.selected[s1, s2] for s1 in m.S for s2 in m.S) >= 1)
    
    m.pprint()
    
    ### SOLVE
    solver = pyo.SolverFactory('cbc')
    res = solver.solve(m)
    print(res)
    m.x.display()
    # m.selected.display()
    

    Output (Truncated):

    x : Size=5, Index=S
        Key : Lower : Value : Upper : Fixed : Stale : Domain
          0 :     0 :   0.0 :  None : False : False : NonNegativeReals
          1 :     0 :   0.0 :  None : False : False : NonNegativeReals
          2 :     0 :   0.0 :  None : False : False : NonNegativeReals
          3 :     0 :   0.0 :  None : False : False : NonNegativeReals
          4 :     0 :   5.0 :  None : False : False : NonNegativeReals