Please look at the below code listing. I have added comments so that the code is self-explanatory. Essentially, the approach is for each bin, count the number of small, medium and large sized balls - so for e.g. consider bin a, if sum_dv_["a", "s"] >= 1(number of small balls in bin a), then the indicator variable bin_dv_["a", "s"] == 1. Then since we can have only one sized ball in a bin so : bin_dv_["a", "s"] + bin_dv_["a", "m"] + bin_dv_["a", "l"] <= 1. This is done for all bins.
from ortools.linear_solver import pywraplp
import itertools
s = pywraplp.Solver("", pywraplp.Solver.SCIP_MIXED_INTEGER_PROGRAMMING)
balls_size = ["s", "m", "s", "m", "l", "s", "m"]
balls_id_size = {i: j for i, j in zip(range(len(balls_size)), balls_size)}
bins = ["a", "b", "c"]
dv = {(i, j): s.BoolVar("") for i, j in itertools.product(balls_id_size, bins)}
# each ball should be assigned to 1 bin
for i in balls_id_size:
s.Add(sum(dv[i, bn] for bn in bins) == 1)
# placeholder decision variable to store number of small/medium/large sized
# balls in each bin
sum_size_bin = {
(i, j): s.IntVar(0, 10, "") for i, j in itertools.product(bins, ["s", "m", "l"])
}
# boolean flag. will be turned = 1 if corresponding sum >= 1
# i.e. if number of small balls in bin 'b' is 2 (say), then the corresponding boolean flag = 1
# sum_size_bin[b, "s"] >= 1 ==> bool_size_bin[b, "s"] = 1
# above constraint will be stated later
bool_size_bin = {
(i, j): s.BoolVar("") for i, j in itertools.product(bins, ["s", "m", "l"])
}
s_balls = [i for i, j in balls_id_size.items() if j == "s"]
m_balls = [i for i, j in balls_id_size.items() if j == "m"]
l_balls = [i for i, j in balls_id_size.items() if j == "l"]
for b in bins:
# number of small sized balls in bin 'b'
s.Add(
sum_size_bin[b, "s"]
== sum([j for i, j in dv.items() if (i[0] in s_balls) and (i[1] == b)])
)
# 100 is big_M
# if number of small sized balls in bin 'b' is >= 1 then the corresponding boolean flag == 1
s.Add(sum_size_bin[b, "s"] - 100 * bool_size_bin[b, "s"] <= 0)
# number of medium sized balls in bin 'b'
s.Add(
sum_size_bin[b, "m"]
== sum([j for i, j in dv.items() if (i[0] in m_balls) and (i[1] == b)])
)
# 100 is big_M
# if number of medium sized balls in bin 'b' is >= 1 then the corresponding boolean flag == 1
s.Add(sum_size_bin[b, "m"] - 100 * bool_size_bin[b, "m"] <= 0)
# number of large sized balls in bin 'b'
s.Add(
sum_size_bin[b, "l"]
== sum([j for i, j in dv.items() if (i[0] in l_balls) and (i[1] == b)])
)
# 100 is big_M
# if number of large sized balls in bin 'b' is >= 1 then the corresponding boolean flag == 1
s.Add(sum_size_bin[b, "l"] - 100 * bool_size_bin[b, "l"] <= 0)
# in each bin we can have balls of only 1 size
s.Add(bool_size_bin[b, "s"] + bool_size_bin[b, "m"] + bool_size_bin[b, "l"] <= 1)
s.Solve()
# investigate the solution
for b in bins:
item_binMapping = [i for i in dv if ((i[1] == b) and (dv[i].SolutionValue() > 0.1))]
item_size = [
j for i, j in balls_id_size.items() if i in [j[0] for j in item_binMapping]
]
print(list(zip(item_binMapping, item_size)))
Above results in:
# (item_id, bin, size)
[((1, 'a'), 'm'), ((3, 'a'), 'm'), ((6, 'a'), 'm')]
[((0, 'b'), 's'), ((2, 'b'), 's'), ((5, 'b'), 's')]
[((4, 'c'), 'l')]
all bins are homogenous in terms of the balls that they have.
We can add symmetry breaking constraints - because now, bin a or bin b both can have either all medium or all small. This can vary from solver to solver or may be within different runs also. We can force, that bin a should always have small balls, so that all runs yields the same output.