Search code examples
pythonor-toolsinteger-programming

Minimize AbsEquality rather than enforce in OrTools


I'm trying to solve the following using OR tools:

Given the following bags containing different colors of balls:

bag red blue green black
A 10 5 85 0
B 25 50 25 0
C 0 100 0 0
D 90 5 5 0
E 2 0 98 0
F 0 0 0 100

How many of each type of bag would I need to have an equal number of each color of ball?

For cases like this where there is an exact answer, the following code:

bags= [
[10,5,85,0],
[25,50,25,0],
[0,100,0,0],
[90,5,5,0],
[2,0,98,0],
[0,0,0,100]
]


bags_n = len(bags)
color_n = len(bags[0])

print(f'Bags: {bags_n}')
print(f'Colors: {color_n}')

color_count= [0] * color_n

for c in range(color_n):
  for b in bags:
    color_count[c]+= b[c]


print(color_count)
print(f'Inital total: {sum(color_count)}')
print(f'Inital equal share: {sum(color_count)//color_n}')

model = cp_model.CpModel()

weights = []

for r in range(bags_n):
  weights.append(model.NewIntVar(1,1000,f'Weight of Bag: {r}'))

total = model.NewIntVar(0, 100000, 'total')

model.Add(
    sum(flatten(
          [[bags[r][c] * weights[r] for r in range(bags_n)] for c in range(color_n)]
        )) == total
)
          
equal = model.NewIntVar(0, 10000, 'equal share')
model.AddDivisionEquality(equal, total, color_n)

for c in range(color_n):
  diff_c = model.NewIntVar(0, 1000, 'diff_'+str(c))
  model.Add(diff_c == sum([bags[r][c] * weights[r] for r in range(bags_n)]) - equal)
  model.AddAbsEquality(0, diff_c)

solver = cp_model.CpSolver()
status = solver.Solve(model)

if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
    print(f'Maximum of objective function: {solver.ObjectiveValue()}\n')
    for v in weights:
      print(f'{solver.Value(v)}')
    print(f'total = {solver.Value(total)}')
    print(f'equal share = {solver.Value(equal)}')
else:
    print(status)

gives back valid weights:

82 2 70 78 5 79

If I change the setup to something like

bags= [
[50,40,10],
[30,20,50],
[30,30,40],
[30,25,45],
]

The model becomes infeasible, I assume due to the fact that there are no weights that satisfy the AbsEquality for every color.

How can I change this to get me the solution closest to an even distribution even if a perfect solution is infeasable?


Solution

  • Christopher Hamkins' suggestion worked great:

    bags= [
    [50,40,10],
    [30,20,50],
    [30,30,40],
    [30,25,45],
    ]
    
    
    bags_n = len(bags)
    color_n = len(bags[0])
    
    print(f'Bags: {bags_n}')
    print(f'Colors: {color_n}')
    
    color_count= [0] * color_n
    
    for c in range(color_n):
      for b in bags:
        color_count[c]+= b[c]
    
    
    
        
    print(color_count)
    print(["{0:.0%}".format(c/sum(color_count)) for c in color_count])
    
    
    model = cp_model.CpModel()
    
    weights = []
    
    for r in range(bags_n):
      weights.append(model.NewIntVar(1,500,f'Weight of Bag: {r}'))
    
    max = model.NewIntVar(0,100000000,f'Max')
    model.AddMaxEquality(max,
                         [sum([bags[r][c] * weights[r] for r in range(bags_n)]) for c in range(color_n)]
                         )
    
    min = model.NewIntVar(0,100000000,f'Min')
    model.AddMinEquality(min,
                         [sum([bags[r][c] * weights[r] for r in range(bags_n)]) for c in range(color_n)]
                         )
    diff = model.NewIntVar(0,100000000,f'Diff')
    model.Add(max - min == diff)
    
    model.Minimize(diff)
    
    
    solver = cp_model.CpSolver()
    status = solver.Solve(model)
    
    if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
        print(f'max = {solver.Value(max)}')
        print(f'min = {solver.Value(min)}')
        print(f'diff = {solver.Value(diff)}')
    
        bag_weights = [0] * bags_n
    
        for i,v in enumerate(weights):
          bag_weights[i] = solver.Value(v)
          print(f'{solver.Value(v)}')
        
        color_count = [0] * color_n
    
        for c in range(color_n):
          for i,b in enumerate(bags):
            color_count[c]+= (b[c] * bag_weights[i])
    
    
        
        print(color_count)
        print(["{0:.0%}".format(c/sum(color_count)) for c in color_count])
    
    else:
        print(status)