Introduction
Pyomo [1] is a popular Python based modeling tool. In [2] a question is posed about a situation where a certain constraint takes more than 8 hours to generate. As we shall see, the reason is that extra indices are used.
The constraint \[y_i = \sum_j x_{i,j} \>\>\>\forall i,j\] is really malformed. The extra \(\forall j\) is problematic. What does this mean? One could say, this is wrong. We can also interpret this differently. Assume the inner \(j\) is scoped (i.e. local). Then we could read this as: repeat the constraint \(y_i = \sum_j x_{i,j}\), \(n\) times. Here \(n=|J|\) is the cardinality of set \(J\).
The GAMS fragment corresponding to this example, shows GAMS will object to this construct:
The Pyomo equivalent can look like:
This fragment is a bit more difficult to read, largely due to syntactic clutter. But in any case: Python and Pyomo accepts this constraint as written. To see what is generated, we can use
This will show something like:
We see for each \(i\) we have three duplicates. The way to fix this is to remove the function argument \(j\) from eqRule:
After this, model.Eq.pprint() produces
This looks much better.
The constraint in the original question was:
Using the knowledge of the previous paragraph we know this should really be:
A simple example
The constraint \[y_i = \sum_j x_{i,j} \>\>\>\forall i,j\] is really malformed. The extra \(\forall j\) is problematic. What does this mean? One could say, this is wrong. We can also interpret this differently. Assume the inner \(j\) is scoped (i.e. local). Then we could read this as: repeat the constraint \(y_i = \sum_j x_{i,j}\), \(n\) times. Here \(n=|J|\) is the cardinality of set \(J\).
The GAMS fragment corresponding to this example, shows GAMS will object to this construct:
11 equation e(i,j); 12 e(i,j).. y(i) =e= sum(j, x(i,j)); **** $125 **** 125 Set is under control already 13 **** 1 ERROR(S) 0 WARNING(S) |
The Pyomo equivalent can look like:
def eqRule(m,i,j): return m.Y[i] == sum(m.X[i,j] for j in m.J); model.Eq = Constraint(model.I,model.J,rule=eqRule) |
This fragment is a bit more difficult to read, largely due to syntactic clutter. But in any case: Python and Pyomo accepts this constraint as written. To see what is generated, we can use
model.Eq.pprint() |
This will show something like:
Eq : Size=6, Index=Eq_index, Active=True Key : Lower : Body : Upper : Active ('i1', 'j1') : 0.0 : Y[i1] - (X[i1,j1] + X[i1,j2] + X[i1,j3]) : 0.0 : True ('i1', 'j2') : 0.0 : Y[i1] - (X[i1,j1] + X[i1,j2] + X[i1,j3]) : 0.0 : True ('i1', 'j3') : 0.0 : Y[i1] - (X[i1,j1] + X[i1,j2] + X[i1,j3]) : 0.0 : True ('i2', 'j1') : 0.0 : Y[i2] - (X[i2,j1] + X[i2,j2] + X[i2,j3]) : 0.0 : True ('i2', 'j2') : 0.0 : Y[i2] - (X[i2,j1] + X[i2,j2] + X[i2,j3]) : 0.0 : True ('i2', 'j3') : 0.0 : Y[i2] - (X[i2,j1] + X[i2,j2] + X[i2,j3]) : 0.0 : True |
We see for each \(i\) we have three duplicates. The way to fix this is to remove the function argument \(j\) from eqRule:
def eqRule(m,i): return m.Y[i] == sum(m.X[i,j] for j in m.J); model.Eq = Constraint(model.I,rule=eqRule) |
After this, model.Eq.pprint() produces
Eq : Size=2, Index=I, Active=True Key : Lower : Body : Upper : Active i1 : 0.0 : Y[i1] - (X[i1,j3] + X[i1,j2] + X[i1,j1]) : 0.0 : True i2 : 0.0 : Y[i2] - (X[i2,j3] + X[i2,j2] + X[i2,j1]) : 0.0 : True |
This looks much better.
The original problem
The constraint in the original question was:
def period_capacity_dept(m, e, j, t, dp): return sum(a[e, j, dp, t]*m.y[e,j,t] for (e,j) in model.EJ)<= K[dp,t] + m.R[t,dp] model.period_capacity_dept = Constraint(E, J, T, DP, rule=period_capacity_dept) |
Using the knowledge of the previous paragraph we know this should really be:
def period_capacity_dept(m, t, dp): return sum(a[e, j, dp, t]*m.y[e,j,t] for (e,j) in model.EJ)<= K[dp,t] + m.R[t,dp] model.period_capacity_dept = Constraint(T, DP, rule=period_capacity_dept) |
Pyomo mixes mathematical notation with programming. I think that is one of the reasons this bug is more difficult to see. In normal programming, adding an argument to a function has an obvious meaning. However in this case, adding e,j means in effect: \(\forall e,j\). If \(e\) and \(j\) belong to large sets, we can easily create a large number of duplicates.
References
- http://www.pyomo.org
- Pyomo: Simple inequality constraint takes unreasonable time to build, https://stackoverflow.com/questions/58034244/pyomo-simple-inequality-constraint-takes-unreasonable-time-to-build