0
$\begingroup$

I have a Pyomo model where I want to formulate constraints:

[Model.portrait.w, Model.portrait.l] OR [Model.landscape.w, Model.landscape.l]

I can use BigM constraints to achieve this, but I want to try using Pyomo Generalized Disjunctive Programming (GDP) as an alternative.

Code:

import pyomo.environ as pyo
import pyomo.gdp as gdp

def DefineModel(Model):
    Model.Select = pyo.Var(Model.Candidate, domain = pyo.Binary)
    Model.Allocation = pyo.Var(Model.Item, Model.Candidate, within = pyo.Binary, initialize = 0)

    Model.portrait = gdp.Disjunct(Model.Item, Model.Candidate)
    Model.landscape = gdp.Disjunct(Model.Item, Model.Candidate)
    
    def rule_LBWidth1(Model, i):
        return sum(Model.Allocation[i, c] * Model.CandidateWidth[c] for c in Model.Candidate) >= Model.Width[i]
    Model.portrait.w = pyo.Constraint(Model.Item, rule = rule_LBWidth1)

    def rule_LBLength1(Model, i):
        return sum(Model.Allocation[i, c] * Model.CandidateLength[c] for c in Model.Candidate) >= Model.Length[i]
    Model.portrait.l = pyo.Constraint(Model.Item, rule = rule_LBLength1)

    def rule_LBWidth2(Model, i):
        return sum(Model.Allocation[i, c] * Model.CandidateWidth[c] for c in Model.Candidate) >= Model.Length[i]
    Model.landscape.w = pyo.Constraint(Model.Item, rule = rule_LBWidth2)

    def rule_LBLength2(Model, i):
        return sum(Model.Allocation[i, c] * Model.CandidateLength[c] for c in Model.Candidate) >= Model.Width[i]
    Model.landscape.l = pyo.Constraint(Model.Item, rule = rule_LBLength2)

    Model.rotate = gdp.Disjunction(expr = [Model.portrait, Model.landscape])

Error message:

ERROR: Constructing component 'rotate' from data=None failed: ValueError: Unexpected term for Disjunction rotate. Expected a Disjunct object, relational expression, or iterable of relational expressions but got <class 'tuple'> in <class 'pyomo.gdp.disjunct.IndexedDisjunct'>

The code follows the structure of the examples at https://pyomo.readthedocs.io/en/latest/modeling_extensions/gdp/modeling.html#disjunctions, except that it uses indexed functions - which seems to be a problem. I'd appreciate suggestions for how to fix the code.

-- EDIT with solution --

def DefineModel(Model):
    Model.Select = pyo.Var(Model.Candidate, domain = pyo.Binary)
    Model.Allocation = pyo.Var(Model.Item, Model.Candidate, within = pyo.Binary, initialize = 0)

    def portrait_rule(d, i):   # Original width|length order, as specified in the data
        d.w = pyo.Constraint(expr=sum(Model.Allocation[i, c] * Model.CandidateWidth[c] for c in Model.Candidate) >= Model.Width[i])
        d.l = pyo.Constraint(expr=sum(Model.Allocation[i, c] * Model.CandidateLength[c] for c in Model.Candidate) >= Model.Length[i])
    Model.portrait = gdp.Disjunct(Model.Item, rule = portrait_rule)
    
    def landscape_rule(d, i):   # Rotated width|length order
        d.w = pyo.Constraint(expr=sum(Model.Allocation[i, c] * Model.CandidateWidth[c] for c in Model.Candidate) >= Model.Length[i])
        d.l = pyo.Constraint(expr=sum(Model.Allocation[i, c] * Model.CandidateLength[c] for c in Model.Candidate) >= Model.Width[i])
    Model.landscape = gdp.Disjunct(Model.Item, rule = landscape_rule)
    
    def rotate_rule(Model, i):   # Use either portrait or landscape orientation for each item
        return [Model.portrait[i], Model.landscape[i]]
    Model.rotate = gdp.Disjunction(Model.Item, rule=rotate_rule)

# ... other constraints and objective function...
    
    pyo.TransformationFactory('gdp.bigm').apply_to(Model)   # Transform the disjunction rules into a form that the solver can work with
$\endgroup$
3
  • 1
    $\begingroup$ Why not try linearizing the constraint by yourself, and then adding these new constraints into the model? By using GDP in both BigM and specifically convex hull transformation, it adds a bunch of the binary variables, that in many cases, leads to the slow convergence. Do you try that? $\endgroup$
    – A.Omidi
    Nov 18, 2023 at 8:06
  • 1
    $\begingroup$ I have done that, with a BigM constraint version constructed manually. On the other hand, sorting the data in a specific way mimics the same effect much more efficiently. The manual BigM and disjunction versions both have many more variables and take far longer to solve to optimality. But the point is to try the different techniques - especially the GDP library, which I hadn't used before. This is all part of a series of blog articles solvermax.com/blog/optimal-but-not-practical-first-attempt The next article is the manual BigM, the article after that will be about GDP. $\endgroup$
    – Solver Max
    Nov 18, 2023 at 17:47
  • $\begingroup$ Thanks for sharing the link. $\endgroup$
    – A.Omidi
    Nov 18, 2023 at 19:45

1 Answer 1

1
$\begingroup$

The error is because you are passing IndexedDisjuncts to the Model.rotate disjunction. The "disjunction expression" should be a list of (scalar) Disjuncts, relational expressions, or iterables of relational expressions. You can fix by declaring an indexed disjunction:

def rotate_rule(m, i):
    return [m.portrait[i], m.landscape[i]]
Model.rotate = Disjunction(Model.Item, rule=rotate_rule)

or

@Model.Disjunction(Model.Item)
def rotate(m, i):
    return [m.portrait[i], m.landscape[i]]

-- EDIT --

You also have a structural issue: you are assigning constraints to the IndexedDisjunct containers portrait and landscape. Those constraints are not attached to any block (Model, Block, or Disjunct) and will not be picked up by the transformations or the problem writers. You can see this by "pretty printing" the model with

Model.pprint()

and you should see that "l" and "w" are not printed as part of the model.

I am not 100% certain what you are trying to accomplish, but looking back, I believe you want a disjunction for each Item? If that is the case, then you might want something closer to:

Model.Allocation = pyo.Var(Model.Item, Model.Candidate, within = pyo.Binary, initialize = 0)

def portrait_rule(d, i):
    m = d.model()
    d.w = pyo.Constraint(
        expr=sum(m.Allocation[i, c] * m.CandidateWidth[c] for c in m.Candidate) >= m.Width[i]
    )
    d.l = pyo.Constraint(
        expr=sum(m.Allocation[i, c] * m.CandidateLength[c] for c in m.Candidate) >= m.Length[i]
    )

Model.portrait = gdp.Disjunct(Model.Items, rule=portrait_rule)

def landscape_rule(d, i):
    m = d.model()
    d.w = pyo.Constraint(
        expr=sum(m.Allocation[i, c] * m.CandidateWidth[c] for c in m.Candidate) >= m.Length[i]
    )
    d.l = pyo.Constraint(
        expr=sum(m.Allocation[i, c] * m.CandidateLength[c] for c in m.Candidate) >= m.Width[i]
    )

Model.landscape = gdp.Disjunct(Model.Items, rule=landscape_rule)

def rotate_rule(m, i):
    return [m.portrait[i], m.landscape[i]]
Model.rotate = gdp.Disjunction(expr=rotate_rule)
$\endgroup$
3
  • $\begingroup$ If I change the definitions to Model.portrait = gdp.Disjunct(Model.Item) and Model.landscape = gdp.Disjunct(Model.Item) and use either of your declarations, then the code runs, but all four constraints are ignored, so the solution is invalid. $\endgroup$
    – Solver Max
    Nov 17, 2023 at 2:26
  • $\begingroup$ Code and example data set are at github.com/SolverMax/Random/tree/main/GDP The optimal objective function value should be 28,914 $\endgroup$
    – Solver Max
    Nov 17, 2023 at 2:49
  • 1
    $\begingroup$ It works! With a couple of minor tweaks, it now behaves correctly. For anyone with similar issues, I've updated the code at github.com/SolverMax/Random/tree/main/GDP. Thanks for your help. $\endgroup$
    – Solver Max
    Nov 17, 2023 at 17:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.