1

I have an application to develop at school that consists in generating work groups for a subject. The restrictions I have to fulfill are the following:

  • Students having similar GPAs should be on the same group.
  • Students that previously have worked together should be on different groups.
  • Group Size should have a minimum and a maximum size possible.

My predicate currently looks like this:

groups(Students, GPAs, PreviouslyWorkedTogether, [MinSize,MaxSize],Groups).

Students is a list of student IDs (ex:. [1,2,3,4]).

GPAs is a list of GPAs (ex:.[4.0,3.5,2.0,3.7]).

The above lists relate to each other such that Student with ID = 1 has a GPA of 4.0, ID = 2 has GPA = 3.5, and so on.

PreviouslyWorkedTogether is a list of Pairs where each element has the IDs of two students that worked together before. (ex [ [1,3] , [2,4] ] - Student 1 worked with Student 3 and Student 2 worked with Student 4).

Groups is the desired result. It has the same size of the Students list. For each Student it should fill the variable with the group ID that the student belongs to. (ex:. [1,2,1,2] -> This means that students 1 and 3 are in group 1,students 2 and 4 are in group 2).

I already implemented successfully (I think) the Group Size boundaries. However I am having trouble with the GPA and WorkedTogether part.

As they seem very similar to each other i will only approach one of them.

The next code snippet is my current solve for the GPA problem.

getGPAs(_, [], []).
getGPAs(GPAs, [H|T], [GroupGPAsH | GroupGPAsT]):-
    element(H, GPAs, GPA),   
    GroupGPAsH #= GPA,
    getGPAs(GPAs, T, GroupGPAsT).

constrain_GPA(_, _, MaxGroupID, GroupID, []):- GroupID #> MaxGroupID, !.
constrain_GPA(GPAs, Groups, MaxGroupID, GroupID, [DiffsH | DiffsT]):-
    findall(Index, element(Index,Groups,GroupID),GroupElems),
    getGPAs(GPAs, GroupElems, GroupGPAs),
    minimum(MinGPA, GroupGPAs),
    maximum(MaxGPA, GroupGPAs),
    NextID is GroupID + 1,
    DiffsH #= MaxGPA - MinGPA,
    constrain_GPA(GPAs, Groups, MaxGroupID, NextID, DiffsT).

Firstly I find all the indexes of the elements that belong to GroupID and store it in a list (GroupElems).

After that, I use those indexes to get the GPAs of each member of the group.

Then I just have to get the maximum and minimum values of the GPAs of the group and calculate the difference, storing it in a list containing the differences of GPAs of each group.

After that, all I do is minimize the sum of the array values so we get the result that contains the best result of groups with similar GPAs.

constrain_GPA(GPAs, Groups, MaxGroupID, 1, Diffs),
sum(Diffs, #=, SumDiffs),
labeling([minimize(SumDiffs)], Groups),

However it continues to give me an error on the labeling predicate. I believe the problem is the findall predicate. I don't know if i can use it or not.

The logic seems right however the coding is the problem.

Here's the complete code (Test query at the end)

:- use_module(library(clpfd)).
:- use_module(library(lists)).

groups(Students, GPAs, PreviousUCsInfo, [MinSize, MaxSize], Groups):-
    %create an array representing the groups of each student
    length(Students, NumStudents),
    length(Groups, NumStudents),
    MaxNumGroups is NumStudents div MinSize,
    MinNumGroupsMod is NumStudents mod MaxSize,
    if_then_else(
                    (MinNumGroupsMod = 0),
                        (MinNumGroups is NumStudents div MaxSize),
                        (MinNumGroups is (NumStudents div MaxSize) + 1)
                ),
    domain([MaxGroupID], MinNumGroups, MaxNumGroups),
    domain(Groups, 1, MaxNumGroups),

    %constrain group size
    nvalue(MaxGroupID, Groups),
    constrain_count(Groups, [MinSize, MaxSize], MaxGroupID, 1),

    %contrain GPA
    constrain_GPA(GPAs, Groups, MaxGroupID, 1, Diffs),
    sum(Diffs, #=, SumDiffs),

    append(Groups, [MaxGroupID], LabelVars),
    labeling([minimize(SumDiffs)], LabelVars).

constrain_count(_, _, MaxGroupID, GroupID):- GroupID #> MaxGroupID.
constrain_count(Groups, [MinSize, MaxSize], MaxGroupID, GroupID):-
    count(GroupID, Groups, #=, Times),
    Times #>= MinSize #/\ Times #=< MaxSize,
    NextID is GroupID + 1,
    constrain_count(Groups, [MinSize, MaxSize], MaxGroupID, NextID).

getGPAs(_, [], []).
getGPAs(GPAs, [H|T], [GroupGPAsH | GroupGPAsT]):-
    element(H, GPAs, GPA),   
    GroupGPAsH #= GPA,
    getGPAs(GPAs, T, GroupGPAsT).

constrain_GPA(_, _, MaxGroupID, GroupID, []):- GroupID #> MaxGroupID, !.
constrain_GPA(GPAs, Groups, MaxGroupID, GroupID, [DiffsH | DiffsT]):-
    findall(Index, element(Index,Groups,GroupID),GroupElems),
    getGPAs(GPAs, GroupElems, GroupGPAs),
    minimum(MinGPA, GroupGPAs),
    maximum(MaxGPA, GroupGPAs),
    NextID is GroupID + 1,
    DiffsH #= MaxGPA - MinGPA,
    constrain_GPA(GPAs, Groups, MaxGroupID, NextID, DiffsT).

if_then_else(C, I, _):- C, !, I.
if_then_else(_, _, E):- E.

%Query to test: groups([1,2,3],[4,2,3],_,[1,2],Var).
% The results expected are:
%for groups of 1 student every combination of groups.
%For groups of 2 students the following:
%[1,2,1]
%[1,2,2]
%[2,1,1]
%[2,1,2]
false
  • 10,264
  • 13
  • 101
  • 209

1 Answers1

1

You are getting an instantiation error, because your variable SumDiffs does not depend functionally on LabelVars. Some comments:

The main issue is the objective function. The spec is a bit ambiguous:

Students having similar GPAs should be on the same group.

Apparently, you have interpreted this as:

Find a solution that minimizes sum(g in 1..n)(max(i in group g)(GPA[i]) - min(i in group g)(GPA[i]))

I guess that this is the source of your trouble in constrain_GPA/5. I guess that you are trying to compute a quantity DiffsH per group and to minimize their sum. Computing that quantity is a bit awkward. You can't use findall/3 that way, because element/3 is a constraint, and can never return more values on backtracking. I would read the spec slightly differently:

Find a solution that minimizes sum(i,j in the same group)(|GPA[i] - GPA[j]|)

That is easier to implement: compute (n-1)*(n-2) quantities, one for each ordered pair (i,j), and take their sum.

Or you can read the spec in yet another way:

Find a solution such that if GPA[i] = GPA[j], then i and j should be in the same group.

turning the whole thing into a satisfiability problem. I would ask my professor for a clarification of the spec.

Small points:

  • Instead of nvalue(MaxGroupID,Groups), maximum(MaxGroupID,Groups) is likely to me more efficient.

  • Instead of if_then_else(A,B,C), use the built-in (A -> B ; C).

  • constrain_count/4 can be replaced by global_cardinality/2, but bear in mind that you must allow for empty groups.

I hope this helps you to complete your assignment.

Mats Carlsson
  • 1,426
  • 7
  • 3
  • The project is about otimization. The restriction: "Students having similar GPAs should be on the same group." is about getting the best solution possible (close GPAs for group members). That is why i use minimize. – Francisco Manuel Canelas Filip Dec 19 '18 at 18:00
  • i.e., "Find a solution that minimizes sum(i,j in the same group)(|GPA[i] - GPA[j]|)." – Mats Carlsson Dec 19 '18 at 19:34
  • Thanks for the feedback. I believe that if i fix the findall problem this code can work.Since findall can't be used here my problem now is the following: Given a list, how can i get the positions in which a value occurs. (.ie: Given the list [1,2,3,1,2,1], and the value 1 it should return [1,4,6] as they are the positions of the value 1). I believe i have to use element, however if i have multiple values it doesn't give me the answer in the format i want which is a list of indexs – Francisco Manuel Canelas Filip Dec 19 '18 at 21:50
  • The problem is, you need a _constraint_, say, occurrences(Values,N,Positions), that constrains Positions to be the list/set of positions of Values in which N occurs. You can't have constraints with lists of nonfixed length. If SICStus included variables with domains not over integers, but over sets of integers, then you could use that. But SICStus does not support that. For this reason, I recommend the second reading of the spec. – Mats Carlsson Dec 20 '18 at 08:34