Search code examples
pythonpyomo

Generate constraints in pyomo with a loop and changing inputs


As an input for my optimization model I have a node structure that links variables with each other as in (simplified version):

from __future__ import annotations
import typing as T

import pyomo.environ as po


class DummyNode:
    def __init__(self, name: str):
        self.CHILDREN: T.List[DummyNode] = []
        self.MOTHER: DummyNode = None
        self.NAME = name

    def insert_child_below(self, child: DummyNode):
        child.MOTHER = self
        self.CHILDREN.append(child)


A = DummyNode("A")
B = DummyNode("B")
C = DummyNode("C")

A.insert_child_below(B)
A.insert_child_below(C)

I need to iteratively generate constraints that are able to capture the relationships between the variables. I have tried the following (dummy model for the purpose of this question, constraints dont really make sense):

model = po.AbstractModel("dummy")

for element in [A, B, C]:
    # generate variable for each node
    setattr(model, element.NAME, po.Var())

    # set constraint for each node, linking it to its mother
    def constraint(model):
        return (
            getattr(model, element.NAME)
            + (getattr(model, element.MOTHER.NAME) if element.MOTHER is not None else 0)
            == 3
        )

    setattr(model, f"{element.NAME}_cons", po.Constraint(rule=constraint))


out = model.construct()

Nevertheless, I guess because of the way pyomo constructs models, I end up generating the same constraint three times:

3 Constraint Declarations
    A_cons : Size=1, Index=None, Active=True
        Key  : Lower : Body  : Upper : Active
        None :   3.0 : C + A :   3.0 :   True
    B_cons : Size=1, Index=None, Active=True
        Key  : Lower : Body  : Upper : Active
        None :   3.0 : C + A :   3.0 :   True
    C_cons : Size=1, Index=None, Active=True
        Key  : Lower : Body  : Upper : Active
        None :   3.0 : C + A :   3.0 :   True

I've tried saving the constraint functions to an external dict, but that hasn't worked either. Any ideas?


Solution

  • If you change your construct to a ConcreteModel it works as you intended. If you "bring your own data", I think the ConcreteModel is by far the best way to go anyhow.

    That said, A couple of ideas...

    • I think using the getattr and setattr are confusing and I'm not sure if there are any hidden pitfalls with directly setting attributes within the model. I don't see it documented. Buyer beware!

    • It seems odd to use the nodes names as the variables. It seems much more intuitive to have a set of nodes and then whatever variables make sense around that like flow, or such. If you make the nodes a set, things seem to fall into place... And you can still use node-based methods as needed.

    • Caution on below: I've overwritten the __repr__ method for node to clean up the output, but I've broken the __repr__ contract because you cannot distinguish duplicates with this. You may need to alter that or prevent dupe names.

    Example:

    import pyomo.environ as po
    
    class DummyNode:
        def __init__(self, name: str):
            self.CHILDREN: T.List[DummyNode] = []
            self.MOTHER: DummyNode = None
            self.NAME = name
    
        def insert_child_below(self, child: 'DummyNode'):
            child.MOTHER = self
            self.CHILDREN.append(child)
    
        def __repr__(self):
            return self.NAME
    
    
    A = DummyNode("A")
    B = DummyNode("B")
    C = DummyNode("C")
    
    A.insert_child_below(B)
    A.insert_child_below(C)
    
    model = po.ConcreteModel("dummy")
    
    # SETS
    model.N = po.Set(initialize=[A, B, C])
    
    # VARS
    model.flow = po.Var(model.N, domain=po.NonNegativeReals)
    
    # CONSTRAINTS
    # Example:  flow limit to parent...
    @model.Constraint(model.N)
    def flow_limit(model, n: DummyNode):
        if n.MOTHER is None:
            return model.flow[n] <= 3
        else:
            return model.flow[n] <= model.flow[n.MOTHER]
    
    model.pprint()
    

    Yields:

    1 Set Declarations
        N : Size=1, Index=None, Ordered=Insertion
            Key  : Dimen : Domain : Size : Members
            None :     1 :    Any :    3 : {A, B, C}
    
    1 Var Declarations
        flow : Size=3, Index=N
            Key : Lower : Value : Upper : Fixed : Stale : Domain
              A :     0 :  None :  None : False :  True : NonNegativeReals
              B :     0 :  None :  None : False :  True : NonNegativeReals
              C :     0 :  None :  None : False :  True : NonNegativeReals
    
    1 Constraint Declarations
        flow_limit : Size=3, Index=N, Active=True
            Key : Lower : Body              : Upper : Active
              A :  -Inf :           flow[A] :   3.0 :   True
              B :  -Inf : flow[B] - flow[A] :   0.0 :   True
              C :  -Inf : flow[C] - flow[A] :   0.0 :   True