Un guide pour OptaPlanner

Un guide pour OptaPlanner

1. Introduction à OptaPlanner

Dans ce didacticiel, nous examinons un solveur de satisfaction de contraintes Java appeléOptaPlanner.

OptaPlanner résout les problèmes de planification en utilisant une suite d'algorithmes avec une configuration minimale.

Bien que la compréhension des algorithmes puisse fournir des détails utiles, le framework nous fournit un travail difficile.

2. Dépendance Maven

Tout d'abord, nous allons ajouter une dépendance Maven pour OptaPlanner:


    org.optaplanner
    optaplanner-core
    7.9.0.Final

Nous localisons la version la plus récente d'OptaPlanner à partir deMaven Central repository.

3. Problem/Solution Class

Pour résoudre un problème, nous avons certainement besoin d'un exemple spécifique.

Le calendrier des cours est un exemple approprié en raison de la difficulté d’équilibrer les ressources telles que les salles, le temps et les enseignants.

3.1. CourseSchedule

CourseSchedule contient une combinaison de nos variables de problème et d'entités de planification, par conséquent c'est la classe de solution. Par conséquent, nous utilisons plusieurs annotations pour le configurer.

Examinons chacun séparément:

@PlanningSolution
public class CourseSchedule {

    private List roomList;
    private List periodList;
    private List lectureList;
    private HardSoftScore score;

La sannotationPlanningSolution indique à OptaPlanner que cette classe contient les données pour englober une solution.

OptaPlanner attend ces composants minimaux: l'entité de planification, les faits du problème et un score.

3.2. Lecture

Lecture, un POJO, ressemble à:

@PlanningEntity
public class Lecture {

    public Integer roomNumber;
    public Integer period;
    public String teacher;

    @PlanningVariable(
      valueRangeProviderRefs = {"availablePeriods"})
    public Integer getPeriod() {
        return period;
    }

    @PlanningVariable(
      valueRangeProviderRefs = {"availableRooms"})
    public Integer getRoomNumber() {
        return roomNumber;
    }
}

Nous utilisons la classeLecture comme entité de planification, nous ajoutons donc une autre annotation sur le getter dansCourseSchedule:

@PlanningEntityCollectionProperty
public List getLectureList() {
    return lectureList;
}

Notre entité de planification contient les contraintes définies.

L'annotationPlanningVariable et les annotationsvalueRangeProviderRef relient les contraintes aux faits du problème.

Ces valeurs de contrainte seront notées ultérieurement pour toutes les entités de planification.

3.3. Faits problématiques

Les variablesroomNumber andperiod agissent comme des contraintes de la même manière.

OptaPlanner note les solutions à la suite d'une logique utilisant ces variables. Nous ajoutons des annotations aux deux méthodesgetter:

@ValueRangeProvider(id = "availableRooms")
@ProblemFactCollectionProperty
public List getRoomList() {
    return roomList;
}

@ValueRangeProvider(id = "availablePeriods")
@ProblemFactCollectionProperty
public List getPeriodList() {
    return periodList;
}

Ces listes sont toutes les valeurs possibles utilisées dans les champsLecture .

OptaPlanner les intègre dans toutes les solutions de l’espace de recherche.

Enfin, il attribue un score à chacune des solutions. Nous avons donc besoin d’un champ pour stocker le score:

@PlanningScore
public HardSoftScore getScore() {
    return score;
}

Sans score, OptaPlanner ne peut pas trouver la solution optimale d'où l'importance soulignée plus tôt.

4. Notation

Contrairement à ce que nous avons vu jusqu'à présent, la classe de scoring nécessite davantage de code personnalisé.

En effet, le calculateur de score est spécifique au problème et au modèle de domaine.

4.1. Java personnalisé

Nous utilisons un simple calcul de score pour résoudre ce problème (bien que cela puisse sembler différent):

public class ScoreCalculator
  implements EasyScoreCalculator {

    @Override
    public Score calculateScore(CourseSchedule courseSchedule) {
        int hardScore = 0;
        int softScore = 0;

        Set occupiedRooms = new HashSet<>();
        for(Lecture lecture : courseSchedule.getLectureList()) {
            String roomInUse = lecture.getPeriod()
              .toString() + ":" + lecture.getRoomNumber().toString();
            if(occupiedRooms.contains(roomInUse)){
                hardScore += -1;
            } else {
                occupiedRooms.add(roomInUse);
            }
        }

        return HardSoftScore.valueOf(hardScore, softScore);
    }
}

Si nous examinons de plus près le code ci-dessus, les parties importantes deviennent plus claires. We calculate a score in the loop because the List<Lecture> contains specific non-unique combinations of rooms and periods.

LeHashSet  est utilisé pour enregistrer une clé unique (chaîne) afin que nous puissions pénaliser les conférences en double dans la même salle et période.

En conséquence, nous recevons des ensembles uniques de pièces et de périodes.

4.2. Drools

Les fichiers Drools nous permettent de modifier rapidement les règles d’application aux fichiers. Bien que la syntaxe puisse parfois prêter à confusion, le fichier Drools peut être un moyen de gérer la logique en dehors des classes compilées.

Notre règle pour empêcher les entrées nulles ressemble à ceci:

global HardSoftScoreHolder scoreHolder;

rule "noNullRoomPeriod"
    when
        Lecture( roomNumber == null );
        Lecture( period == null );
    then
        scoreHolder.addHardConstraintMatch(kcontext, -1);
end

5. Configuration du solveur

Un autre fichier de configuration nécessaire, nous avons besoin d’un fichier XML pour configurer le solveur.

5.1. Fichier de configuration XML


    

    
        
            org.example.optaplanner.ScoreCalculator
        
    

    
        10
    

En raison de nos annotations dans la classeCourseSchedule, nous utilisons l'élémentscanAnnotatedClasses ici pour analyser les fichiers sur le chemin de classe.

Le contenu de l'élémentscoreDirectorFactory définit notre classeScoreCalculator pour contenir notre logique de notation.

Lorsque nous voulons utiliser un fichier Drools, nous remplaçons le contenu de l'élément par:

courseScheduleScoreRules.drl

Notre réglage final est l'élément de terminaison. Plutôt que de rechercher sans cesse une solution optimisée qui pourrait ne jamais exister, ce paramètre arrêtera la recherche après un certain délai.

Dix secondes suffisent amplement à la plupart des problèmes.

6. Essai

Nous avons configuré nos solutions, solutions et problèmes. Testons-le!

6.1. Mise en place de notre test

Premièrement, nous faisons une configuration:

SolverFactory solverFactory = SolverFactory
  .createFromXmlResource("courseScheduleSolverConfiguration.xml");
solver = solverFactory.buildSolver();

unsolvedCourseSchedule = new CourseSchedule();

Deuxièmement, nous remplissons les données dans la collection d'entités de planification et les objets du fait de problèmeList.

6.2. Test d'exécution et de vérification

Enfin, nous le testons en appelantsolve.

CourseSchedule solvedCourseSchedule = solver.solve(unsolvedCourseSchedule);

assertNotNull(solvedCourseSchedule.getScore());
assertEquals(-4, solvedCourseSchedule.getScore().getHardScore());

Nous vérifions que lesolvedCourseSchedule a un score qui nous indique que nous avons la solution «optimale».

En prime, nous créons une méthode d'impression qui affichera notre solution optimisée:

public void printCourseSchedule() {
    lectureList.stream()
      .map(c -> "Lecture in Room "
        + c.getRoomNumber().toString()
        + " during Period " + c.getPeriod().toString())
      .forEach(k -> logger.info(k));
}

Cette méthode affiche:

Lecture in Room 1 during Period 1
Lecture in Room 2 during Period 1
Lecture in Room 1 during Period 2
Lecture in Room 2 during Period 2
Lecture in Room 1 during Period 3
Lecture in Room 2 during Period 3
Lecture in Room 1 during Period 1
Lecture in Room 1 during Period 1
Lecture in Room 1 during Period 1
Lecture in Room 1 during Period 1

Notez comment les trois dernières entrées se répètent. Cela se produit parce qu'il n'y a pas de solution optimale à notre problème. Nous avons choisi trois périodes, deux salles de classe et dix conférences.

Il n'y a que six conférences possibles en raison de ces ressources fixes. À tout le moins, cette réponse montre à l'utilisateur qu'il n'y a pas assez de salles ou de périodes pour contenir toutes les conférences.

7. Fonctionnalités supplémentaires

Notre exemple pour OptaPlanner que nous avons créé était simple. Cependant, le framework a ajouté des fonctionnalités pour des cas d'utilisation plus variés. Nous voudrons peut-être mettre en œuvre ou modifier notre algorithme d'optimisation, puis spécifier le framework pour l'utiliser.

En raison des récentes améliorations apportées aux capacités de multi-threading de Java, OptaPlanner donne également aux développeurs la possibilité d'utiliser plusieurs implémentations de multi-threading telles que fork and join, résolution incrémentielle et multitenancy.

Reportez-vous auxdocumentation pour plus d'informations.

8. Conclusion

La structure OptaPlanner fournit aux développeurs un outil puissant pour résoudre les problèmes de satisfaction des contraintes tels que la planification et l’allocation de ressources.

OptaPlanner offre une utilisation minimale des ressources JVM et une intégration à Java EE. L'auteur continue de prendre en charge le cadre, et Red Hat l'a ajouté dans le cadre de sa suite de règles de gestion.

Comme toujours, le code peut être trouvéover on Github.