0

I try to solve a "simple" logical problem with OR-Tools or clingo(ASP). It goes like this: I have a set of Persons like Person(Tony, Bob, Ann, Carl, Amber, Peter) I also have groups like Group1(Bob, Ann, Carl, Amber, Peter), Group2(Bob, Amber), and Group3(Amber). Now I want to select two persons from Group1, one person from Group2 and one person from Group3 - so in total four persons with all constraints fulfilled. Possible solutions would be Carl, Ann, Bob, Amber or Peter, Ann, Bob, Amber or Carl, Peter, Bob, Amber. How can I achieve that?

Edit: I tried this in ASP clingo:

person(tony; bob; ann; carl; amber; peter).
group1(bob; ann; carl; amber; peter).
group2(bob; amber).
group3(amber).

% rules
{select(X): person(X), group1(X)} = 2.
{select(X): person(X), group2(X)} = 1.
{select(X): person(X), group3(X)} = 1.

#show select/1.

But I only get:

clingo version 5.5.0
Reading from stdin
Solving...
Answer: 1
select(amber) select(carl)
Answer: 2
select(amber) select(peter)
Answer: 3
select(amber) select(ann)
SATISFIABLE

Models       : 3
Calls        : 1
Time         : 0.004s (Solving: 0.00s 1st Model: 0.00s Unsat: 0.00s)
CPU Time     : 0.000s

Thanks and regards

jmlots
  • 25
  • 6
  • What did you try? – sascha Aug 10 '21 at 16:13
  • I only have some working code in ASP, added it to the original post. Thanks! – jmlots Aug 10 '21 at 16:27
  • 1
    The solution shown looks good in regards to: `Now I want to select two persons from Group1, one person from Group2 and one person from Group3 - so in total four persons with all constraints fulfilled.` The solutions you expect is actually something different as selecting *Amber* not only aggregates a 1 in group3, but also a 1 in group2 (meaning: you cannot ever select bob here). | So try formalizing your expected solution first (as i think your description fits the ASP model more compared to the solutione expected) – sascha Aug 10 '21 at 16:36
  • Let me clarify, so I want to select four persons total. Two from G1, one from G2 and one from G3. If I go through this I can only select Amber for G3 (since she is the only one in it). For G2 I can just select Bob now (since Amber is already selected and he is the only one left). For G1 I need to select two persons and three persons are left: ann, carl and amber. I only need two of them so the possible selects are: 1. ann, carl 2. ann, peter 3. carl, peter. Thats how I came to the 3 expected solutions. My main problem is to formulate this correctly in OR-Tools or ASP or Prolog. Any idea? – jmlots Aug 10 '21 at 16:47
  • 1
    I got what you are looking for (by reverse-engineering the example), but you did not express it formally. Without a formal model, it's really hard to map those problems to general-purpose solvers though!!! – sascha Aug 10 '21 at 17:22
  • I tried to formulate the exact same model in ASP like you did in python. – jmlots Aug 10 '21 at 17:57

2 Answers2

2

Quick hack:

Code

import numpy as np
from ortools.sat.python import cp_model

# DATA
# ----
names = ['Tony', 'Bob', 'Ann', 'Carl', 'Amber', 'Peter']
matrix = np.array([
  [0, 1, 1, 1, 1, 1],     # group 0 "members": NOT Tony, Bob, Ann, ...
  [0, 1, 0, 0, 1, 0],     # group 1 "members": Bob, Amber      
  [0, 0, 0, 0, 1, 0]      # group 2 "members": Amber
])
cardinalities = [2, 1, 1] # group 0, group 1, group 2

# MODEL
# -----
model = cp_model.CpModel()
# 2d boolean assignment-matrix: person X group picked?
var_assign = np.empty(matrix.shape, dtype=object)
for a in range(matrix.shape[0]):
  for b in range(matrix.shape[1]):
    var_assign[a, b] = model.NewBoolVar('')

# forbid to pick a person from a group without being a "member"
forbidden_picks = np.where(matrix == 0)
for ind, group in enumerate(forbidden_picks[0]):
  model.Add(var_assign[group, forbidden_picks[1][ind]] == 0)

# a person is picked from at most one group
for person in range(matrix.shape[1]):
  model.Add(var_assign[:, person].sum() <= 1)

# a group is picked exactly n times 
for group, group_card in enumerate(cardinalities):
  model.Add(var_assign[group, :].sum() == group_card)

# SOLVE
# -----
class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback):
    def __init__(self, variables):
        cp_model.CpSolverSolutionCallback.__init__(self)
        self.__variables = variables

    def on_solution_callback(self):
        selected = []
        for g in range(len(cardinalities)):
          for p_ind, p in enumerate(names):
            if self.Value(self.__variables[g][p_ind]) == 1:
              selected.append(p)
        print('Solution: ', selected)

solver = cp_model.CpSolver()
solution_printer = VarArraySolutionPrinter(var_assign.tolist())
status = solver.SearchForAllSolutions(model, solution_printer)

Out

Solution:  ['Ann', 'Carl', 'Bob', 'Amber']
Solution:  ['Ann', 'Peter', 'Bob', 'Amber']
Solution:  ['Carl', 'Peter', 'Bob', 'Amber']

Approach

Just think about some assignment-matrix:

Tony  Bob  Ann  Carl  Amber  Peter
 -     1    1    1      1      1   = group 0 = 2
 -     1    -    -      1      -   = group 1 = 1
 -     -    -    -      1      -   = group 2 = 1 

 <=1  <=1  <=1  <=1    <=1    <=1
sascha
  • 32,238
  • 6
  • 68
  • 110
1

You formulated your described problem perfectly in ASP and it shows the correct results. What you forgot to state is that you do not want to select the same person from two different groups.

I modified your ASP program to the following:

person(tony; bob; ann; carl; amber; peter).
group1(bob; ann; carl; amber; peter).
group2(bob; amber).
group3(amber).

% select the amount for each group individually
{select(1,X): group1(X)} = 2.
{select(2,X): group2(X)} = 1.
{select(3,X): group3(X)} = 1.

% you are not allowed to select a person in two groups
:- select(G,X), select(G',X), G < G'.

#show .
#show select(X) : select(G,X).

This gives your expected answers.

Max Ostrowski
  • 575
  • 1
  • 3
  • 15