This is a follow up to a previous post. However, I have revised things and thought the change warranted a separate post for clarity's sake. Apologies if this is poor form, I'll happily amend/delete as required.
In the following minimal example, I use a sparse set (WEEK_PROD_flat
) to formulate my decision variable X
:
Initial Model
weekly_products = {
1: ['Q24'],
2: ['Q24', 'J24'],
3: ['Q24', 'J24','F24'],
4: ['J24', 'F24'],
5: ['F24']
}
from pyomo.environ import *
model = ConcreteModel()
model.WEEKS = Set(initialize = [1,2,3,4,5])
model.PRODS = Set(initialize = ['Q24','J24','F24'])
model.WEEK_PROD = Set(model.WEEKS, initialize = weekly_products)
model.WEEK_PROD_flat = Set(initialize=[(w, p) for w in model.WEEKS for p in model.WEEK_PROD[w]])
model.X = Var(model.WEEK_PROD_flat)
I am attempting to apply constraints to a sub-set of the indices from WEEK_PROD_flat
. To do so, I first formulate the following min and max parameters:
Parameters
week_min = {
(2,'J24'):10,
(3,'J24'):20,
(3,'F24'):10,
(4,'J24'):30,
(4,'F24'):20,
(5,'F24'):30
}
week_max = {
(2,'J24'):30,
(3,'J24'):40,
(3,'F24'):30,
(4,'J24'):50,
(4,'F24'):40,
(5,'F24'):50
}
model.weekMin = Param(model.WEEK_PROD_flat, within = NonNegativeIntegers, initialize = week_min, default = 0)
model.weekMax = Param(model.WEEK_PROD_flat, within = NonNegativeIntegers, initialize = week_max, default = 0)
I am attempting to use these parameters to create a constraint that is cumulative for each (week, product) indices with the following specification:
The output should look like this:
Desired Output
(2, 'J24'): 10 : X[1,Q24] + X[2,Q24] + X[2,J24] : 30
(3, 'J24'): 20 : X[1,Q24] + X[2,Q24] + X[3,Q24] + X[2,J24] + X[3,J24] : 40
(3, 'F24'): 10 : X[1,Q24] + X[2,Q24] + X[3,Q24] + X[3,F24] : 30
(4, 'J24'): 30 : X[1,Q24] + X[2,Q24] + X[3,Q24] + X[2,J24] + X[3,J24] + X[4,J24] :50
(4, 'F24'): 20 : X[1,Q24] + X[2,Q24] + X[3,Q24] + X[3,F24] + X[4,F24] : 40
(5, 'F24'): 30 : X[1,Q24] + X[2,Q24] + X[3,Q24] + X[3,F24] + X[4,F24] + X[5,F24] : 50
In a previous iteration of the problem double indices were not used in the formulation of parameters or the decision variable and sparse sets were not used. I was able to create the desired output by specifying multiple rules and creating constraints for 'J' and 'F' separately.
This would be unwieldly in my actual use case and I would like to be able to create the desired output with a single constraint declaration.
Guidance appreciated!
Current Attempts
So far, the closest I have been able to get to the desired results is below. However, this is far from ideal for a couple of reasons:
1.) A similar rule will obviously have to be called to define constraints for each product element in model.PRODS
that is not 'Q24'. While this is only 2 in this instance, in the actual use case, this would simply not be efficient.
2.) Because the constraint is indexed using the single indices from model.WEEKS
, it was necessary to apply static bounds (0,1000 in the example shown) as the double indexed bounds from weekMin
and weekMax
could not be applied.
Code
def cum_limit_rule(model,w):
subset = {x for x in model.WEEKS if x <= w}
return(0,
sum(model.X[w,p] for w in subset for p in model.WEEK_PROD[w] if p[0] == 'J' or p[0] == 'Q'),
1000)
model.cum_limit = Constraint(model.WEEKS, rule= cum_limit_rule)
Output
cum_limit : Size=5, Index=WEEKS, Active=True
Key : Lower : Body : Upper : Active
1 : 0.0 : X[1,Q24] : 1000.0 : True
2 : 0.0 : X[1,Q24] + X[2,Q24] + X[2,J24] : 1000.0 : True
3 : 0.0 : X[1,Q24] + X[2,Q24] + X[2,J24] + X[3,Q24] + X[3,J24] : 1000.0 : True
4 : 0.0 : X[1,Q24] + X[2,Q24] + X[2,J24] + X[3,Q24] + X[3,J24] + X[4,J24] : 1000.0 : True
5 : 0.0 : X[1,Q24] + X[2,Q24] + X[2,J24] + X[3,Q24] + X[3,J24] + X[4,J24] : 1000.0 : True
Here's a cut at that constraint....
A couple things. You don't mention in your post if you have multiple F
and J
products, but I assume so from context. Otherwise, this is really overly complicated.
Be careful with your defaults for the parameter. You had zeros for max, which would make the model infeasible. If you had a max of 30 in week 3 and a cumulative max of 0 in week 4.... A more sophisticated approach might be to use the prior week's values if there is no allowed changes week-to-week, but that's problem dependent. I just tossed in 1000.
There's probably a couple other ways to do this, but this approach is flexible and accommodates changes to the weekly products.
from pyomo.environ import ConcreteModel, Set, Var, Constraint, Param, NonNegativeIntegers
from itertools import chain
from collections import defaultdict
weekly_products = {
1: ['Q24'],
2: ['Q24', 'J24', 'J17', 'J44'],
3: ['Q24', 'J24', 'F24'],
4: ['Q87', 'J24', 'F24'],
5: ['F24', 'F77']
}
# let's group up the products...
# pyomo prefers lists for initializers to preserve order, so we can sort the set
all_products = sorted(set(chain(*weekly_products.values())))
products_by_type = defaultdict(list)
for p in all_products:
products_by_type[p[0]].append(p)
# print(products_by_type)
min_max_groups = ['J', 'F']
all_groups = ['Q', 'J', 'F']
week_min = {
(2,'J'):10,
(3,'J'):20,
(3,'F'):10,
(4,'J'):30,
(4,'F'):20,
(5,'F'):30
}
week_max = {
(2,'J'):30,
(3,'J'):40,
(3,'F'):30,
(4,'J'):50,
(4,'F'):40,
(5,'F'):50
}
model = ConcreteModel()
### SETS
model.WEEKS = Set(initialize = weekly_products.keys())
model.PRODS = Set(initialize = all_products)
model.GROUP_PROD = Set(all_groups, initialize=products_by_type)
# this is an "indexed set" which is a set of sets, indexed in this case by WEEKS
model.WEEK_PROD = Set(model.WEEKS, initialize=weekly_products)
model.WEEK_PROD_flat = Set(initialize=[(w, p) for w in model.WEEKS for p in model.WEEK_PROD[w]])
### VARS
# initializing X with this reduced flat set will reduce errors and build a more compact
# model instead of |weeks| * |products| if there are only sparse legal combinations to consider:
model.X = Var(model.WEEK_PROD_flat)
### PARAMS
model.weekMin = Param(model.WEEKS * min_max_groups, within = NonNegativeIntegers, initialize = week_min, default = 0)
model.weekMax = Param(model.WEEKS * min_max_groups, within = NonNegativeIntegers, initialize = week_max, default = 1000) # BE CAREFUL WITH DEFAULTS
def weekly_min_max(model, limit_week, group):
q_prod_sum = sum(model.X[week, prod]
for prod in model.GROUP_PROD['Q']
for week in model.WEEKS
if week <= limit_week
if prod in model.WEEK_PROD[week])
prod_sum = sum(model.X[week, prod]
for prod in model.GROUP_PROD[group]
for week in model.WEEKS
if week <= limit_week
if prod in model.WEEK_PROD[week])
return model.weekMin[limit_week, group], q_prod_sum + prod_sum, model.weekMax[limit_week, group]
model.C1 = Constraint(model.WEEKS, min_max_groups, rule=weekly_min_max)
model.pprint()
1 Constraint Declarations
C1 : Size=10, Index=C1_index, Active=True
Key : Lower : Body : Upper : Active
(1, 'F') : 0.0 : X[1,Q24] : 1000.0 : True
(1, 'J') : 0.0 : X[1,Q24] : 1000.0 : True
(2, 'F') : 0.0 : X[1,Q24] + X[2,Q24] : 1000.0 : True
(2, 'J') : 10.0 : X[1,Q24] + X[2,Q24] + X[2,J17] + X[2,J24] + X[2,J44] : 30.0 : True
(3, 'F') : 10.0 : X[1,Q24] + X[2,Q24] + X[3,Q24] + X[3,F24] : 30.0 : True
(3, 'J') : 20.0 : X[1,Q24] + X[2,Q24] + X[3,Q24] + X[2,J17] + X[2,J24] + X[3,J24] + X[2,J44] : 40.0 : True
(4, 'F') : 20.0 : X[1,Q24] + X[2,Q24] + X[3,Q24] + X[4,Q87] + X[3,F24] + X[4,F24] : 40.0 : True
(4, 'J') : 30.0 : X[1,Q24] + X[2,Q24] + X[3,Q24] + X[4,Q87] + X[2,J17] + X[2,J24] + X[3,J24] + X[4,J24] + X[2,J44] : 50.0 : True
(5, 'F') : 30.0 : X[1,Q24] + X[2,Q24] + X[3,Q24] + X[4,Q87] + X[3,F24] + X[4,F24] + X[5,F24] + X[5,F77] : 50.0 : True
(5, 'J') : 0.0 : X[1,Q24] + X[2,Q24] + X[3,Q24] + X[4,Q87] + X[2,J17] + X[2,J24] + X[3,J24] + X[4,J24] + X[2,J44] : 1000.0 : True