All checks were successful
Build, test and publish / verify (push) Successful in 7m6s
97 lines
4.6 KiB
Java
97 lines
4.6 KiB
Java
package nl.agilecc.agiledancers.solver;
|
|
|
|
import com.google.ortools.Loader;
|
|
import com.google.ortools.sat.CpModel;
|
|
import com.google.ortools.sat.CpSolver;
|
|
import com.google.ortools.sat.CpSolverStatus;
|
|
import com.google.ortools.sat.LinearExpr;
|
|
import jakarta.inject.Singleton;
|
|
import java.util.List;
|
|
import java.util.function.Consumer;
|
|
import nl.agilecc.agiledancers.model.scheduling.SchedulingFailureKind;
|
|
import nl.agilecc.agiledancers.model.scheduling.SchedulingInput;
|
|
import nl.agilecc.agiledancers.model.scheduling.SchedulingPhase;
|
|
import nl.agilecc.agiledancers.model.scheduling.SchedulingProgress;
|
|
import nl.agilecc.agiledancers.model.scheduling.SchedulingResult;
|
|
import nl.agilecc.agiledancers.service.SchedulingFailure;
|
|
import nl.agilecc.agiledancers.service.port.SchedulingEngine;
|
|
|
|
@Singleton
|
|
public class OrToolsSchedulingEngine implements SchedulingEngine {
|
|
private final double timeLimitSeconds;
|
|
|
|
public OrToolsSchedulingEngine() {
|
|
this(1800);
|
|
}
|
|
|
|
/** Allows callers to impose a shorter run budget, for example in automated tests. */
|
|
public OrToolsSchedulingEngine(final double timeLimitSeconds) {
|
|
this.timeLimitSeconds = timeLimitSeconds;
|
|
}
|
|
|
|
@Override
|
|
public SchedulingResult solve(final SchedulingInput input) {
|
|
return solve(input, (final SchedulingProgress progress) -> {});
|
|
}
|
|
|
|
@Override
|
|
public SchedulingResult solve(final SchedulingInput input, final Consumer<SchedulingProgress> observer) {
|
|
final long deadline = System.nanoTime() + (long) (timeLimitSeconds * 1_000_000_000L);
|
|
Loader.loadNativeLibraries();
|
|
final CpModel model = new CpModel();
|
|
final OrderedAssignments vars = OrderedAssignments.create(model, input);
|
|
final LinearExpr attendance = SchedulingObjectives.attendance(model, input, vars);
|
|
final LinearExpr variety = SchedulingObjectives.variety(model, input, vars);
|
|
final CpSolver solver = new CpSolver();
|
|
solver.getParameters().setCatchSigintSignal(false);
|
|
solver.getParameters().setNumSearchWorkers(1).setRandomSeed(0);
|
|
SchedulingResult best = null;
|
|
final List<LinearExpr> objectives = List.of(attendance, LinearExpr.term(vars.spread(), 1), variety);
|
|
final List<SchedulingPhase> phases =
|
|
List.of(SchedulingPhase.ATTENDANCE, SchedulingPhase.SPREAD, SchedulingPhase.VARIETY);
|
|
for (int stage = 0; stage < objectives.size(); stage++) {
|
|
final LinearExpr objective = objectives.get(stage);
|
|
final SchedulingPhase phase = phases.get(stage);
|
|
observer.accept(new SchedulingProgress(phase, best != null));
|
|
final double remaining = (deadline - System.nanoTime()) / 1_000_000_000.0;
|
|
if (remaining <= 0) {
|
|
if (best != null) {
|
|
return new SchedulingResult(false, best.assignments());
|
|
}
|
|
requireSolution(CpSolverStatus.UNKNOWN);
|
|
}
|
|
solver.getParameters().setMaxTimeInSeconds(Math.max(0.001, remaining));
|
|
model.minimize(objective);
|
|
final CpSolverStatus status = solver.solve(model, new SchedulingSolutionObserver(observer, phase));
|
|
if (status == CpSolverStatus.UNKNOWN && best != null) {
|
|
return new SchedulingResult(false, best.assignments());
|
|
}
|
|
requireSolution(status);
|
|
best = new SchedulingResult(status == CpSolverStatus.OPTIMAL, vars.read(input, solver));
|
|
if (status != CpSolverStatus.OPTIMAL) {
|
|
return best;
|
|
}
|
|
model.addEquality(objective, Math.round(solver.objectiveValue()));
|
|
}
|
|
return best;
|
|
}
|
|
|
|
private static void requireSolution(final CpSolverStatus status) {
|
|
if (status != CpSolverStatus.OPTIMAL && status != CpSolverStatus.FEASIBLE) {
|
|
throw new SchedulingFailure(
|
|
switch (status) {
|
|
case INFEASIBLE -> SchedulingFailureKind.INFEASIBLE;
|
|
case UNKNOWN -> SchedulingFailureKind.NO_SOLUTION;
|
|
default -> SchedulingFailureKind.SOLVER_FAILED;
|
|
},
|
|
switch (status) {
|
|
case INFEASIBLE -> "Scheduling is infeasible. Existing assignments were preserved.";
|
|
case UNKNOWN ->
|
|
"The 30-minute limit was reached without a valid result. Existing assignments were"
|
|
+ " preserved.";
|
|
default -> "Solver failed (" + status + "). Existing assignments were preserved.";
|
|
});
|
|
}
|
|
}
|
|
}
|