
org.chocosolver.solver.ParallelPortfolio Maven / Gradle / Ivy
/*
* This file is part of choco-solver, http://choco-solver.org/
*
* Copyright (c) 2025, IMT Atlantique. All rights reserved.
*
* Licensed under the BSD 4-clause license.
*
* See LICENSE file in the project root for full license information.
*/
package org.chocosolver.solver;
import org.chocosolver.solver.constraints.Constraint;
import org.chocosolver.solver.constraints.nary.sat.NogoodStealer;
import org.chocosolver.solver.constraints.real.RealConstraint;
import org.chocosolver.solver.exception.InvalidSolutionException;
import org.chocosolver.solver.exception.SolverException;
import org.chocosolver.solver.search.loop.monitors.IMonitorSolution;
import org.chocosolver.solver.search.loop.monitors.NogoodFromRestarts;
import org.chocosolver.solver.search.strategy.BlackBoxConfigurator;
import org.chocosolver.solver.search.strategy.Search;
import org.chocosolver.solver.search.strategy.SearchParams;
import org.chocosolver.solver.search.strategy.selectors.values.IntValueSelector;
import org.chocosolver.solver.search.strategy.strategy.AbstractStrategy;
import org.chocosolver.solver.variables.IntVar;
import org.chocosolver.solver.variables.Variable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Spliterator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
*
* A Portfolio helper.
*
*
* The ParallelPortfolio resolution of a problem is made of four steps:
*
* - adding models to be run in parallel,
* - running resolution in parallel,
* - getting the model which finds a solution (or the best one), if any.
*
* Each of the four steps is needed and the order is imposed too.
* In particular, in step 1. each model should be populated individually with a model of the problem
* (presumably the same model, but not required).
* Populating model is not managed by this class and should be done before applying step 2.,
* with a dedicated method for instance.
*
* Note also that there should not be pending resolution process in any models.
* Otherwise, unexpected behaviors may occur.
*
*
* The resolution process is synchronized. As soon as one model ends (naturally or by hitting a limit)
* the other ones are eagerly stopped.
* Moreover, when dealing with an optimization problem, cut on the objective variable's value is propagated
* to all models on solution.
* It is essential to eagerly declare the objective variable(s) with {@link Model#setObjective(boolean, Variable)}.
*
*
*
* Note that the similarity of the models declared is not required.
* However, when dealing with an optimization problem, keep in mind that the cut on the objective variable's value
* is propagated among all models, so different objectives may lead to wrong results.
*
*
* Since there is no condition on the similarity of the models,
* once the resolution ends, the model which finds the (best) solution is internally stored.
*
*
* Example of use.
*
*
* ParallelPortfolio pares = new ParallelPortfolio();
* int n = 4; // number of models to use
* for (int i = 0; i < n; i++) {
* pares.addModel(modeller());
* }
* pares.solve();
* IOutputFactory.printSolutions(pares.getBestModel());
*
*
*
*
*
* This class uses Java 8 streaming feature, and may be not compliant with older versions.
*
*
*
*
* Project: choco.
*
* @author Charles Prud'homme, Jean-Guillaume Fages
* @since 23/12/2015.
*/
public class ParallelPortfolio {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////// VARIABLES //////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* List of {@link Model}s to be executed in parallel.
*/
private final List models;
/**
* whether or not to use default search configurations for the different threads
**/
private final boolean searchAutoConf;
/**
* This manager is used to synchronize nogood sharing.
*/
private NogoodStealer manager = NogoodStealer.NONE;
/**
* Stores whether or not prepare() method has been called
*/
private boolean isPrepared = false;
/**
* List of {@link Model}s to be executed in parallel.
*/
private final HashMap reliableness;
private final AtomicBoolean solverTerminated = new AtomicBoolean(false);
private final AtomicBoolean solutionFound = new AtomicBoolean(false);
private final AtomicInteger solverRunning = new AtomicInteger(0);
/**
* Point to (one of) the solver(s) which found a solution
*/
private Model finder;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Creates a new ParallelPortfolio
* This class stores the models to be executed in parallel in a {@link ArrayList} initially empty.
*
* @param searchAutoConf changes the search heuristics of the different solvers, except the first one (true by default).
* Must be set to false if search heuristics of the different threads are specified manually, so that they are not erased
*/
public ParallelPortfolio(boolean searchAutoConf) {
this.models = new ArrayList<>();
this.reliableness = new HashMap<>();
this.searchAutoConf = searchAutoConf;
}
/**
* Creates a new ParallelPortfolio
* This class stores the models to be executed in parallel in a {@link ArrayList} initially empty.
* Search heuristics will be changed automatically (except for the first thread that will remain in the same configuration).
*/
public ParallelPortfolio() {
this(true);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////// API //////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Calling this method will ensure that workers equipped with a restart policy not only
* record nogoods from themselves (based on {@link NogoodFromRestarts}) but also based on
* other workers of the portfolio.
*
* @implSpec It is assumed that all models in this portfolio are equivalent (ie, each variable has
* the same ID in each worker).
*/
public void stealNogoodsOnRestarts() {
this.manager = new NogoodStealer();
}
/**
*
* Adds a model to the list of models to run in parallel.
* The model can either be a fresh one, ready for populating, or a populated one.
*
*
* Important:
*
* - the populating process is not managed by this ParallelPortfolio
* and should be done externally, with a dedicated method for example.
*
* -
* when dealing with optimization problems, the objective variables HAVE to be declared eagerly with
* {@link Model#setObjective(boolean, Variable)}.
*
*
*
*
*
* @param model a model to add
*/
public void addModel(Model model) {
addModel(model, true);
}
/**
*
* Adds a model to the list of models to run in parallel.
* The model can either be a fresh one, ready for populating, or a populated one.
*
*
* Important:
*
* - the populating process is not managed by this {@code ParallelPortfolio}
* and should be done externally, with a dedicated method for example.
*
* -
* when dealing with optimization problems, the objective variables HAVE to be declared eagerly with
* {@link Model#setObjective(boolean, Variable)}.
*
*
*
*
*
*
* A reliable model is expected to prove the absence of a solution,
* improving one in the case of optimisation problem.
* A model with non-redundant constraints posted
* to improve resolution at the expense of completeness is considered unreliable.
* An unreliable model cannot share its no-goods and when it stops, cannot stop other models.
*
*
* There should be at least one reliable model in a Portfolio.
* Otherwise, solving may be made incomplete.
*
*
* @param model a model to add
* @param reliable set to {@code true} if the model is reliable.
*/
public void addModel(Model model, boolean reliable) {
this.models.add(model);
this.reliableness.put(model, reliable);
}
/**
* Run the solve() instruction of every model of the portfolio in parallel.
*
*
* Note that a call to {@link #getBestModel()} returns a model which has found the best solution.
*
*
* @return true
if and only if at least one new solution has been found.
* @throws SolverException if no model or only model has been added.
*/
public boolean solve() {
getSolverTerminated().set(false);
getSolutionFound().set(false);
getSolverRunning().set(models.size());
if (!isPrepared) {
prepare();
}
ExecutorService executorService = Executors.newFixedThreadPool(models.size());
try {
// run the solve() method of each model in parallel
executorService.submit(() -> models.parallelStream().forEach(m -> {
if (!getSolverTerminated().get()) {
boolean so = m.getSolver().solve();
// if a solution is found, update the best model
if (!so || finder == m) {
getSolverTerminated().set(so || reliableness.get(m) || getSolverRunning().decrementAndGet() <= 0);
}
}
})).get();
} catch (InterruptedException | ExecutionException | SolverException e) {
getSolverRunning().decrementAndGet();
//If a InvalidSolutionException occurs and at least one model is not reliable
// the exception may come from this model and should be ignored
if (e.getCause() instanceof InvalidSolutionException) {
InvalidSolutionException ex = (InvalidSolutionException) e.getCause();
if (reliableness.get(ex.getModel())) {
throw (SolverException) e.getCause();
}// else ignore the error
} else {
e.printStackTrace();
}
}
executorService.shutdownNow();
getSolverTerminated().set(false);// otherwise, solver.isStopCriterionMet() always returns true
if (getSolutionFound().get() && models.get(0).getResolutionPolicy() != ResolutionPolicy.SATISFACTION) {
int bestAll = getBestModel().getSolver().getBestSolutionValue().intValue();
for (Model m : models) {
int mVal = m.getSolver().getBestSolutionValue().intValue();
// When LCG is on, the best solution might not have been considered yet
// Indeed, the bound is updated after a force restart on failure only
if (m.getResolutionPolicy() == ResolutionPolicy.MAXIMIZE) {
assert mVal <= bestAll || m.getSolver().isLCG(): mVal + " > " + bestAll;
} else assert m.getResolutionPolicy() != ResolutionPolicy.MINIMIZE || mVal >= bestAll || m.getSolver().isLCG() : mVal + " < " + bestAll;
}
}
return getSolutionFound().get();
}
/**
* Returns the first model from the list which, either :
*
* -
* finds a solution when dealing with a satisfaction problem,
*
* -
* or finds (and possibly proves) the best solution when dealing with an optimization problem.
*
*
* or null if no such model exists.
* Note that there can be more than one "finder" in the list, yet, this method returns the index of the first one.
*
* @return the first model which finds a solution (or the best one) or null if no such model exists.
*/
public Model getBestModel() {
return finder;
}
/**
* @return the (mutable!) list of models used in this ParallelPortfolio
*/
public List getModels() {
return models;
}
/**
* Attempts to find all solutions of the declared problem.
*
* - If the method returns an empty list:
*
* - either a stop criterion (e.g., a time limit) stops the search before any solution has been found,
* - or no solution exists for the problem (i.e., over-constrained).
*
* - if the method returns a list with at least one element in it:
*
* - either the resolution stops eagerly du to a stop criterion before finding all solutions,
* - or all solutions have been found.
*
*
*
*
* Note that all variables will be recorded
*
* @return a list that contained the found solutions.
*/
public Stream streamSolutions() {
//noinspection Convert2Diamond
Spliterator it = new Spliterator() {
@Override
public boolean tryAdvance(Consumer super Solution> action) {
if (solve()) {
action.accept(new Solution(getBestModel()).record());
return true;
}
return false;
}
@Override
public Spliterator trySplit() {
return null;
}
@Override
public long estimateSize() {
return Long.MAX_VALUE;
}
@Override
public int characteristics() {
return Spliterator.ORDERED | Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.CONCURRENT;
}
};
return StreamSupport.stream(it, false);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////// INTERNAL METHODS //////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void prepare() {
isPrepared = true;
check();
for (int i = 0; i < models.size(); i++) {
Solver s = models.get(i).getSolver();
s.addStopCriterion(() -> getSolverTerminated().get());
s.plugMonitor((IMonitorSolution) () -> updateFromSolution(s.getModel()));
if (searchAutoConf) {
configureModel(i);
}
}
}
private synchronized void updateFromSolution(Model m) {
if (m.getResolutionPolicy() == ResolutionPolicy.SATISFACTION) {
finder = m;
getSolutionFound().set(true);
} else {
int solverVal = ((IntVar) m.getObjective()).getValue();
int bestVal = m.getSolver().getObjectiveManager().getBestSolutionValue().intValue();
if (m.getResolutionPolicy() == ResolutionPolicy.MAXIMIZE) {
assert solverVal <= bestVal : solverVal + ">" + bestVal;
} else
assert
m.getResolutionPolicy() != ResolutionPolicy.MINIMIZE || solverVal >= bestVal : solverVal + "<" + bestVal;
if (solverVal == bestVal) {
getSolutionFound().set(true);
finder = m;
models.forEach(s1 -> s1.getSolver().onReceivingExternalCut(bestVal));
}
}
}
private void configureModel(int workerID) {
Model worker = getModels().get(workerID);
ResolutionPolicy policy = worker.getResolutionPolicy();
boolean opt = policy != ResolutionPolicy.SATISFACTION;
BlackBoxConfigurator bb = BlackBoxConfigurator.init();
// common settings
bb.setRestartPolicy(SearchParams.Restart.GEOMETRIC, 10, 1.05, 50_000, true);
bb.setNogoodOnRestart(true);
bb.setRestartOnSolution(true);
bb.setExcludeViews(false);
SearchParams.ValSelConf intValConf;
Function intValSel;
SearchParams.VarSelConf intVarConf;
BiFunction> intVarSel;
switch (workerID) {
case 0:
intValConf = new SearchParams.ValSelConf(
SearchParams.ValueSelection.MIN, opt, 16, true);
intValSel = intValConf.make();
intVarConf = new SearchParams.VarSelConf(
SearchParams.VariableSelection.DOMWDEG, 32);
intVarSel = intVarConf.make();
bb.setIntVarStrategy((vars) -> intVarSel.apply(vars, intValSel.apply(worker)));
bb.setMetaStrategy(m -> Search.lastConflict(m, 2));
//TODO DEAL WITH SETVAR --> MINIZINC
if (reliableness.containsKey(worker)) {
manager.add(worker);
}
break;
case 1:
intValConf = new SearchParams.ValSelConf(
SearchParams.ValueSelection.MIN, opt, 16, true);
intValSel = intValConf.make();
intVarConf = new SearchParams.VarSelConf(
SearchParams.VariableSelection.CHS, 32);
intVarSel = intVarConf.make();
bb.setIntVarStrategy((vars) -> intVarSel.apply(vars, intValSel.apply(worker)));
bb.setMetaStrategy(m -> Search.lastConflict(m, 2));
//TODO DEAL WITH SETVAR --> MINIZINC
if (reliableness.containsKey(worker)) {
manager.add(worker);
}
break;
case 2:
intValConf = new SearchParams.ValSelConf(
SearchParams.ValueSelection.MIN, opt, 16, true);
intValSel = intValConf.make();
intVarConf = new SearchParams.VarSelConf(
SearchParams.VariableSelection.DOMWDEG_CACD, 32);
intVarSel = intVarConf.make();
bb.setIntVarStrategy((vars) -> intVarSel.apply(vars, intValSel.apply(worker)));
bb.setMetaStrategy(m -> Search.lastConflict(m, 2));
//TODO DEAL WITH SETVAR --> MINIZINC
if (reliableness.containsKey(worker)) {
manager.add(worker);
}
break;
case 3:
intValConf = new SearchParams.ValSelConf(
SearchParams.ValueSelection.MIN, opt, 16, true);
intValSel = intValConf.make();
intVarConf = new SearchParams.VarSelConf(
SearchParams.VariableSelection.FRBA, 32);
intVarSel = intVarConf.make();
bb.setIntVarStrategy((vars) -> intVarSel.apply(vars, intValSel.apply(worker)));
bb.setMetaStrategy(m -> Search.lastConflict(m, 2));
//TODO DEAL WITH SETVAR --> MINIZINC
if (reliableness.containsKey(worker)) {
manager.add(worker);
}
break;
case 4:
intValConf = new SearchParams.ValSelConf(
SearchParams.ValueSelection.MIN, opt, 16, true);
intValSel = intValConf.make();
intVarConf = new SearchParams.VarSelConf(
SearchParams.VariableSelection.ACTIVITY, 32);
intVarSel = intVarConf.make();
bb.setIntVarStrategy((vars) -> intVarSel.apply(vars, intValSel.apply(worker)));
bb.setMetaStrategy(m -> Search.lastConflict(m, 2));
//TODO DEAL WITH SETVAR --> MINIZINC
break;
case 5:
intValConf = new SearchParams.ValSelConf(
SearchParams.ValueSelection.MIN, opt, 16, true);
intValSel = intValConf.make();
intVarConf = new SearchParams.VarSelConf(
SearchParams.VariableSelection.DOMWDEG_CACD, 32);
intVarSel = intVarConf.make();
bb.setIntVarStrategy((vars) -> intVarSel.apply(vars, intValSel.apply(worker)));
bb.setMetaStrategy(m -> Search.lastConflict(m, 2));
//TODO DEAL WITH SETVAR --> MINIZINC
break;
case 6:
intValConf = new SearchParams.ValSelConf(
SearchParams.ValueSelection.MIN, opt, 16, true);
intValSel = intValConf.make();
intVarConf = new SearchParams.VarSelConf(
SearchParams.VariableSelection.DOMWDEG, 32);
intVarSel = intVarConf.make();
bb.setIntVarStrategy((vars) -> intVarSel.apply(vars, intValSel.apply(worker)));
bb.setMetaStrategy(m -> Search.lastConflict(m, 2));
//TODO DEAL WITH SETVAR --> MINIZINC
if (reliableness.containsKey(worker)) {
manager.add(worker);
}
break;
case 7:
intValConf = new SearchParams.ValSelConf(
SearchParams.ValueSelection.MIN, opt, 16, true);
intValSel = intValConf.make();
intVarConf = new SearchParams.VarSelConf(
SearchParams.VariableSelection.FRBA, 32);
intVarSel = intVarConf.make();
bb.setIntVarStrategy((vars) -> intVarSel.apply(vars, intValSel.apply(worker)));
bb.setMetaStrategy(m -> Search.lastConflict(m, 2));
//TODO DEAL WITH SETVAR --> MINIZINC
break;
default:
intValConf = new SearchParams.ValSelConf(
SearchParams.ValueSelection.MIN, opt, 16, true);
intValSel = intValConf.make();
intVarConf = new SearchParams.VarSelConf(
SearchParams.VariableSelection.CHS, 32);
intVarSel = intVarConf.make();
bb.setIntVarStrategy((vars) -> intVarSel.apply(vars, intValSel.apply(worker)));
bb.setMetaStrategy(m -> Search.lastConflict(m, 1));
//TODO DEAL WITH SETVAR --> MINIZINC
break;
}
bb.make(worker);
}
private void check() {
if (models.size() == 0) {
throw new SolverException("No model found in the ParallelPortfolio.");
}
if (models.get(0).getResolutionPolicy() != ResolutionPolicy.SATISFACTION) {
Variable objective = models.get(0).getObjective();
if (objective == null) {
throw new UnsupportedOperationException("No objective has been defined");
}
if ((objective.getTypeAndKind() & Variable.REAL) != 0) {
for (Constraint c : models.get(0).getCstrs()) {
if (c instanceof RealConstraint) {
throw new UnsupportedOperationException("Ibex is not multithread safe, ParallelPortfolio cannot be used");
}
}
}
}
}
private synchronized AtomicBoolean getSolverTerminated() {
return solverTerminated;
}
private synchronized AtomicBoolean getSolutionFound() {
return solutionFound;
}
private synchronized AtomicInteger getSolverRunning() {
return solverRunning;
}
}