Przeglądaj źródła

Implement Explanability of hard score

tripeur 4 lat temu
rodzic
commit
0ae0b365fe

+ 50 - 6
src/main/java/fr/jaquin/bdlg/planner/SolverController.java

@@ -2,10 +2,15 @@ package fr.jaquin.bdlg.planner;
 
 import java.util.UUID;
 import java.util.concurrent.ExecutionException;
-
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import fr.jaquin.bdlg.planner.domain.Assignement;
+import fr.jaquin.bdlg.planner.domain.MealAssignement;
+import fr.jaquin.bdlg.planner.domain.MealSlot;
 import fr.jaquin.bdlg.planner.domain.Planning;
 import fr.jaquin.bdlg.planner.domain.PlanningInput;
 import fr.jaquin.bdlg.planner.domain.PlanningSolution;
+import org.optaplanner.core.api.score.ScoreExplanation;
 import org.optaplanner.core.api.score.ScoreManager;
 import org.optaplanner.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore;
 import org.optaplanner.core.api.solver.SolverJob;
@@ -25,10 +30,10 @@ public class SolverController {
   @Autowired
   private SolverManager<Planning, UUID> solverManager;
   @Autowired
-    private ScoreManager<Planning, HardMediumSoftScore> scoreManager;
+  private ScoreManager<Planning, HardMediumSoftScore> scoreManager;
+  private Collector<CharSequence, ?, String> listCollector = Collectors.joining(",", "[", "]");
 
-  
-	@CrossOrigin(origins = "http://localhost:8081")
+  @CrossOrigin(origins = "http://localhost:8081")
   @PostMapping("/solve")
   public PlanningSolution solve(@RequestBody PlanningInput inputs) {
     Planning problem = inputs.generatePlanningProblem();
@@ -44,8 +49,47 @@ public class SolverController {
     } catch (InterruptedException | ExecutionException e) {
       throw new IllegalStateException("Solving failed.", e);
     }
-    System.out.println(scoreManager.explainScore(solution));
-    return PlanningSolution.from(solution);
+
+    ScoreExplanation<Planning, HardMediumSoftScore> explanation =
+        scoreManager.explainScore(solution);
+    PlanningSolution output = PlanningSolution.from(solution);
+    output.setExplanation(stringifyExplanation(explanation));
+    System.out.println(explanation.getSummary());
+    return output;
+  }
+
+  private String stringifyExplanation(ScoreExplanation<Planning, HardMediumSoftScore> explanation) {
+    return explanation.getConstraintMatchTotalMap().values().stream()
+        // Return only Hard Constraint
+        .filter(predicate -> predicate.getScore().getHardScore() < 0)
+        // Create a dictionnary with the name of the constraint as key
+        .map(constraint -> "\"" + constraint.getConstraintName() + "\":"
+        // Populate the value with an array of justification objects
+            + constraint.getConstraintMatchSet().stream()
+                // Filter pair that have a negative impact on the score
+                .filter(matchElt -> matchElt.getScore().getHardScore() < 0)
+                .map(match -> match.getJustificationList().stream()
+                    .map(elt -> stringifyConstraint(elt)).collect(listCollector))
+                .collect(listCollector))
+        .collect(Collectors.joining(",", "{", "}"));
+  }
+
+  private String stringifyConstraint(Object val) {
+    if (val instanceof Assignement) {
+      Assignement casted = (Assignement) val;
+      return "{\"type\":\"Assignement\",\"slotId\":\"" + casted.getSlot().getId()
+          + "\",\"volonteerId\":" + casted.getVolonteer().getId().toString() + "}";
+    }
+    if (val instanceof MealAssignement) {
+      MealAssignement casted = (MealAssignement) val;
+      return "{\"type\":\"MealAssignement\",\"slotId\":\"" + casted.getMealSlot().getId()
+          + "\",\"volonteerId\":" + casted.getVolonteer().getId().toString() + "}";
+    }
+    if (val instanceof MealSlot) {
+      MealSlot casted = (MealSlot) val;
+      return "{\"type\":\"MealSlot\",\"slotId\":\"" + casted.getId() + "\"}";
+    }
+    return "\"" + val.getClass().getName() + "\"";
   }
 
 }

+ 1 - 0
src/main/java/fr/jaquin/bdlg/planner/domain/AssignementPair.java

@@ -3,6 +3,7 @@ package fr.jaquin.bdlg.planner.domain;
 public class AssignementPair {
   public final String slotId;
   public final Long volonteerId;
+  public boolean isFixed;
 
   public AssignementPair(String slotId, Long volonteerId) {
     this.slotId = slotId;

+ 1 - 1
src/main/java/fr/jaquin/bdlg/planner/domain/PlanningInput.java

@@ -162,7 +162,7 @@ public class PlanningInput {
           throw new NullPointerException(
               "Impossible to find the corresponding assignement object." + pair.slotId);
         } else {
-          assignements.add(new Assignement(currentMaxId, timeslot, volonteer, false));
+          assignements.add(new Assignement(currentMaxId, timeslot, volonteer, pair.isFixed));
           currentMaxId++;
         }
       }

+ 12 - 0
src/main/java/fr/jaquin/bdlg/planner/domain/PlanningSolution.java

@@ -6,6 +6,7 @@ import org.optaplanner.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore
 public class PlanningSolution {
   private ArrayList<AssignementPair> assignements;
   private String message = "";
+  private String explanation = "";
   private HardMediumSoftScore score;
 
   public PlanningSolution() {
@@ -44,8 +45,19 @@ public class PlanningSolution {
     return this.message;
   }
 
+  public void setMessage(String msg) {
+    this.message = msg;
+  }
+
   public ArrayList<AssignementPair> getAssignements() {
     return this.assignements;
   }
 
+  public void setExplanation(String explanation) {
+    this.explanation = explanation;
+  }
+
+  public String getExplanation() {
+    return this.explanation;
+  }
 }

+ 4 - 0
src/main/java/fr/jaquin/bdlg/planner/domain/Volonteer.java

@@ -46,4 +46,8 @@ public class Volonteer {
     return Objects.hash(id);
   }
 
+  @Override
+  public String toString() {
+    return "Volunteer[id=" + this.id + "]";
+  }
 }

+ 2 - 3
src/main/java/fr/jaquin/bdlg/planner/solver/PlanningConstraintProvider.java

@@ -35,13 +35,13 @@ public class PlanningConstraintProvider implements ConstraintProvider {
   private Constraint completeAllTimeslot(ConstraintFactory constraintFactory) {
     return constraintFactory.from(Assignement.class)
         .filter((assignment) -> assignment.getVolonteer() == null)
-        .penalize("Timeslot not initialized ", HardMediumSoftScore.ONE_HARD);
+        .penalize("Timeslot not initialized", HardMediumSoftScore.ONE_HARD);
   }
 
   private Constraint feedAllVolunteer(ConstraintFactory constraintFactory) {
     return constraintFactory.from(MealAssignement.class)
         .filter((assignment) -> assignment.getMealSlot() == null)
-        .penalize("Meal not initialized ", HardMediumSoftScore.ONE_HARD);
+        .penalize("Meal not initialized", HardMediumSoftScore.ONE_HARD);
   }
 
   private Constraint volunteerConflict(ConstraintFactory constraintFactory) {
@@ -82,7 +82,6 @@ public class PlanningConstraintProvider implements ConstraintProvider {
 
   private Constraint volunteerMealConflict(ConstraintFactory constraintFactory) {
     // a volonteer cannot go to 2 timeslot at the same time.
-
     // Select a assignement ...
     return getAssignedSlotStream(constraintFactory)
         // ... and pair it with another assignement ...

+ 1 - 1
src/main/resources/application.properties

@@ -6,7 +6,7 @@
 ########################
 
 # The solver runs for 30 seconds. To run for 5 minutes use "5m" and for 2 hours use "2h".
-optaplanner.solver.termination.spent-limit = 15s
+optaplanner.solver.termination.spent-limit = 2s
 
 # To change how many solvers to run in parallel
 # optaplanner.solver-manager.parallel-solver-count=4