TP 5/6 : Refactoring d'un composant logiciel (2/3 et 3/3)

Important : Consultez les modalités de rendu.

Deuxième TP

Le type des contraintes

Travail à faire : Actuellement, le type d'une contrainte est codé par un caractère. Définir un enum et l'utiliser.

Gérer les solutions

À ce stade, nous affichons les solutions sur la sortie standard. Nous allons améliorer ce point.

Travail à faire : Créer une classe Solutions qui va offrir des méthodes de gestion des solutions :
  • comptage
  • mémorisation (avec un record pour chaque affectation)
  • affichage (ou pas)
Nous pouvons maintenant considérer que la méthode solve va renvoyer une instance de Solutions.

Gérer les stratégies

Note : Une stratégie peut agir sur quatre points :
  • un travail de simplification du problème à faire avant la recherche,
  • le choix de la prochaine variable à instancier (findVariable),
  • la manière d'explorer le domaine de cette variable (le calcul de step),
  • le travail de vérification à faire après chaque exploration du domaine.
Nous pouvons donc détacher l'implémentation des stratégies du moteur de résolution.
Travail à faire :
  • Créer une interface IStrategy avec les quatre actions évoquées.
  • Créer une classe DefaultStrategy qui implémente les choix par défaut.
  • Utiliser l'interface et la classe afin de simplifier le solveur.
  • Créer une deuxième classe pour la stratégie reduceAndCheckIntervalsStrategy.

La réduction d'intervalles

La réduction d'intervalles est très importante afin de diminuer l'espace de recherche et d'identifier des impasses (intervalles vides). Nous allons isoler cette étape afin de la simplifier et la tester.

Travail à faire :
  • Créer une classe Reducer qui travaille sur un problème à résoudre et propose une méthode de réduction (méthodes à récupérer depuis le solveur).
  • Créer un test unitaire afin de vérifier la réduction d'un ensemble de contraintes (par exemple A+B=1000, A+B=2000).
  • Créer un test unitaire afin de vérifier la réduction de la contrainte multiplicative.
  • La réduction de la contrainte X <> Y n'est pas prévue. Cette réducation n'est possible que si l'un des deux variables est fixée et si sa valeur correspond au min/max de l'autre variable. Voila trois exemples qui peuvent servir à construire un test unitaire.
Réduction de X <> Y
X in [10,20], Y in [20,20]    --->    X in [10,19], Y in [20,20]
X in [10,10], Y in [10,11]    --->    X in [10,10], Y in [11,11]
X in [15,15], Y in [15,15]    --->    X in [],      Y in []
Travail à faire : L'opération de réduction est coûteuse car, à chaque étape, toutes les contraintes sont réétudiées. Améliorons ce calcul avec l'aide d'un (observateur).
  • Associer à chaque variable la liste de ses contraintes.
  • Associer à chaque variable un observateur (interface Consumer<Variable>) qui va être informé de la réduction du domaine (méthode reduce).
  • Maintenir, au niveau du Reducer, un ensemble de contraintes à étudier (en observant les variations de domaine des variables).

Troisième TP

La stratégie alwaysReduce

Nous allons maintenant construire une stratégie qui consiste à réduire le problème à chaque étape.

Travail à faire :
  • Contrairement aux autres stratégies, cette dernière est susceptible de modifier plusieurs domaines à chaque étape. Un travail de sauvegarde/restauration est donc nécessaire. Prévoir une classe Backup qui se charge d'une sauvegarde et de l'opération de restauration associée.
  • Il faut sans doute modifier l'interface IStrategy pour prendre en compte cette spécificité.
  • Proposer une implémentation de cette stratégie et vérifier sur un problème que cette stratégie est bien plus efficace.
Travail à faire : Pour tirer partie de cette stratégie et de la réduction de la contrainte diff, vous pouvez tester la résolution du problème des N-Reines avec le code ci-dessous (92 solutions pour 8 reines).
private Solver makeQueens(int n) {
    var solver = new Solver();
    var queens = new Variable[n];
    for (int i = 0; i < n; i++) {
        queens[i] = solver.newVar("R" + i).domain(1, n);
        for (int j = 0; j < i; j++) {
            solver.addConstraint(queens[i], "<>", queens[j]);
            solver.addConstraint(queens[i], "+", i, "<>", queens[j], "+", j);
            solver.addConstraint(queens[i], "-", i, "<>", queens[j], "-", j);
        }
    }
    solver.reduceAndCheckIntervalsStrategy();
    return solver;
}

Un problème d'optimisation

Cet exercice est optionnel mais très intéressant !

Nous allons maintenant changer le fonctionnement de notre solveur en ajoutant la minimisation de la valeur d'une variable. Par exemple, pour le problème ci-dessous, la plus petite valeur de COST est 330 pour X=70 et Y=30.

0 <= X, X <= 70,
0 <= Y, Y <= 70,
X + Y = 100,
COST = (3 * X) + (4 * Y)
Travail à faire : Nous allons traiter cette nouvelle fonctionnalité comme une nouvelle stratégie :
  • Préparer une classe OptimizationStrategy qui hérite de AlwaysReduceStrategy.
  • Cette stratégie va conserver la valeur courante minimale de la variable à minimiser (variable qu'elle doit donc connaître).
  • A chaque solution, cette valeur est éventuellement mise à jour.
  • A chaque réduction, cette stratégie va réduire le domaine de la variable pour ne pas autoriser des valeurs plus grandes que celles déjà trouvées. Ainsi, à chaque étape, nous améliorons notre objectif.
  • Amélioration : Comment faire pour ne plus renvoyer les solutions non-optimales ?
Travail à faire : Considérons le problème d'affectation ci-dessous. Nous avons des tâches à faire et des agents pour les faire. Chaque tâche est faite par un seul agent qui ne peut faire qu'au plus une tâche. Cette affectation entraîne un coût qui est détaillé dans le tableau ci-dessous.
           Agent 1   Agent 2   Agent 3   Agent 4
tâche 1 :    8         5         9         9
tâche 2 :    4         2         6         4
tâche 3 :    7         3         7         8
Problème : quelle est l'affection des tâches aux agents qui minimise le coût ? Aidez-vous de la méthode ci-dessous afin de construire un test unitaire qui va trouver la solution de ce problème.
public Solver buildAssignmentProblem(int[][] costs) {
    var solver = new Solver();
    int nbTasks = costs.length;
    int nbAgents = costs[0].length;
    Variable[][] matrix = new Variable[nbTasks][nbAgents];
    
    // create nbTasks * nbAgents variables 0..1
    for (int t = 0; t < nbTasks; t++) {
        for (int a = 0; a < nbAgents; a++) {
            matrix[t][a] = solver.newVar("T" + t + "A" + a).domain(0, 1);
        }
    }
    
    // each task have one agent
    var zero = solver.newConstant(0);
    for (int t = 0; t < nbTasks; t++) {
        var sum = zero;
        for (int a = 0; a < nbAgents; a++) {
            sum = solver.expression2(sum, "+", matrix[t][a]);
        }
        solver.addConstraint(sum, "=", 1);
    }
    
    // each agent have at most one task
    for (int a = 0; a < nbAgents; a++) {
        var sum = zero;
        for (int t = 0; t < nbTasks; t++) {
            sum = solver.expression2(sum, "+", matrix[t][a]);
        }
        solver.addConstraint(sum, "<=", 1);
    }
    
    // compute cost
    var cost = zero;
    for (int t = 0; t < nbTasks; t++) {
        for (int a = 0; a < nbAgents; a++) {
            cost = solver.expression2(cost, "+", matrix[t][a], "*", costs[t][a]);
        }
    }
    var varCost = solver.newVar("cost");
    solver.addConstraint(cost, "=", varCost);
    solver.setVerbose(false);
    // Code a adapter pour minimiser cost
    ....
    ....
    return solver;
}

Modalités de rendu vis etulab

  • Le rendu de votre projet Solver est à faire pour le jeudi 24 octobre 2024 à 23h59. Il faudra que le dépôt git sur etulab soit à jour et que Jean-Luc Massat (identifiant massat) soit membre du projet avec des droits au moins égal à reporter.
  • L'ajout de membre se fait via manage / members dans le menu de votre projet sur etulab, puis en cliquant sur le bouton invite members.
  • Votre projet devra contenir à sa racine un fichier README.md contenant les informations sur ses membres et les emprunts réalisés. Le format du fichier devra être le suivant :
# Solver

## Membres du projet

- NOM Prénom du premier membre
- NOM Prénom du deuxième membre (si applicable)

## Description des emprunts

- Utilisation de ChatGPT pour les classes : `Main.java`, ...
- ...