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?
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.
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()
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