mods = m.getModifiers();
+ if (mods.contains(Modifier.DEFAULT) && !mods.contains(Modifier.STATIC)
+ && !excluded.contains(m.getSimpleName().toString())) {
+ methods.add(m);
+ }
+ }
+ methods.sort(Comparator.comparing(this::signature));
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("package ").append(pkg).append(";\n\n")
+ .append("/**\n")
+ .append(" * Recording subclass of {@link ").append(annotated.getSimpleName()).append("}: overrides the ")
+ .append(methods.size()).append(" factory methods of\n")
+ .append(" * {@link ").append(factories.getQualifiedName()).append("} to journalize them.\n")
+ .append(" * \n")
+ .append(" * GENERATED at compile time by {@code ").append(getClass().getName()).append("}.\n")
+ .append(" */\n")
+ .append("public class ").append(name).append(" extends ").append(annotated.getSimpleName()).append(" {\n\n")
+ .append(" /**\n * Names of the factory methods which are not journalized (no effect on the model).\n */\n")
+ .append(" public static final java.util.Set EXCLUDED = java.util.Set.of(")
+ .append(excluded.stream().sorted().map(e -> "\"" + e + "\"").collect(Collectors.joining(", ")))
+ .append(");\n\n")
+ .append(" private static final FactoryMethod[] M = new FactoryMethod[").append(methods.size()).append("];\n\n")
+ .append(" static {\n");
+ for (int i = 0; i < methods.size(); i++) {
+ ExecutableElement m = methods.get(i);
+ sb.append(" M[").append(i).append("] = FactoryMethod.of(\"").append(m.getSimpleName()).append('"');
+ for (VariableElement p : m.getParameters()) {
+ sb.append(", ").append(erasure(p.asType())).append(".class");
+ }
+ sb.append(");\n");
+ }
+ sb.append(" }\n");
+
+ for (ExecutableElement c : ElementFilter.constructorsIn(annotated.getEnclosedElements())) {
+ if (c.getModifiers().contains(Modifier.PRIVATE)) {
+ continue;
+ }
+ sb.append("\n public ").append(name).append('(').append(parameters(c)).append(") {\n")
+ .append(" super(").append(names(c)).append(");\n")
+ .append(" }\n");
+ }
+
+ for (int i = 0; i < methods.size(); i++) {
+ ExecutableElement m = methods.get(i);
+ boolean isVoid = m.getReturnType().getKind() == TypeKind.VOID;
+ String superCall = "super." + m.getSimpleName() + "(" + names(m) + ")";
+ String args = "new Object[]{" + names(m) + "}";
+ sb.append("\n @Override\n")
+ .append(" public ").append(typeParameters(m)).append(m.getReturnType()).append(' ')
+ .append(m.getSimpleName())
+ .append('(').append(parameters(m)).append(") {\n")
+ .append(" if (!topLevel()) {\n");
+ if (isVoid) {
+ sb.append(" ").append(superCall).append(";\n")
+ .append(" return;\n")
+ .append(" }\n")
+ .append(" getRecorder().run(M[").append(i).append("], () -> ").append(superCall)
+ .append(", ").append(args).append(");\n");
+ } else {
+ sb.append(" return ").append(superCall).append(";\n")
+ .append(" }\n")
+ .append(" return getRecorder().call(M[").append(i).append("], () -> ").append(superCall)
+ .append(", ").append(args).append(");\n");
+ }
+ sb.append(" }\n");
+ }
+ sb.append("}\n");
+
+ try (Writer w = processingEnv.getFiler().createSourceFile(pkg + "." + name, annotated).openWriter()) {
+ w.write(sb.toString());
+ }
+ }
+
+ private String erasure(TypeMirror t) {
+ return processingEnv.getTypeUtils().erasure(t).toString();
+ }
+
+ private String signature(ExecutableElement m) {
+ return m.getSimpleName() + m.getParameters().stream().map(p -> erasure(p.asType()))
+ .collect(Collectors.joining(",", "(", ")"));
+ }
+
+ /**
+ * @return the declaration of the parameters of m , named p0, p1, ...
+ */
+ private static String parameters(ExecutableElement m) {
+ List extends VariableElement> params = m.getParameters();
+ return IntStream.range(0, params.size()).mapToObj(k -> {
+ String t = params.get(k).asType().toString();
+ if (m.isVarArgs() && k == params.size() - 1) {
+ t = t.substring(0, t.length() - 2) + "...";
+ }
+ return t + " p" + k;
+ }).collect(Collectors.joining(", "));
+ }
+
+ /**
+ * @return the declaration of the type parameters of m , e.g. {@code }, or an empty
+ * string
+ */
+ private static String typeParameters(ExecutableElement m) {
+ if (m.getTypeParameters().isEmpty()) {
+ return "";
+ }
+ return m.getTypeParameters().stream().map(tp -> {
+ List bounds = tp.getBounds().stream().map(TypeMirror::toString)
+ .filter(b -> !b.equals("java.lang.Object")).toList();
+ return bounds.isEmpty() ? tp.toString() : tp + " extends " + String.join(" & ", bounds);
+ }).collect(Collectors.joining(", ", "<", "> "));
+ }
+
+ private static String names(ExecutableElement m) {
+ return IntStream.range(0, m.getParameters().size()).mapToObj(k -> "p" + k).collect(Collectors.joining(", "));
+ }
+}
diff --git a/codegen/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/codegen/src/main/resources/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 0000000000..c00b8c9957
--- /dev/null
+++ b/codegen/src/main/resources/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+org.chocosolver.codegen.RecordingModelProcessor
diff --git a/etc/spec-bench.sh b/etc/spec-bench.sh
new file mode 100755
index 0000000000..03753f73b8
--- /dev/null
+++ b/etc/spec-bench.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+#
+# Equivalence bench of ModelSpec on a set of instances, each one in its own JVM.
+#
+# Usage: spec-bench.sh ...
+# Instances: *.fzn, *.xml, *.xml.lzma found under the instance directories.
+# Output: /results.tsv (status, instance, seconds, detail), one log per instance in /logs.
+# Extra JVM options (e.g., -Dspec.nodes=2000) can be passed with the SPEC_OPTS environment variable, and another main
+# class with SPEC_MAIN (default: org.chocosolver.parser.spec.SpecEquivalenceMain).
+#
+set -u
+CP=$1; OUT=$2; WORKERS=$3; LIMIT=$4; HEAP=$5; shift 5
+mkdir -p "$OUT/logs"
+export CP OUT LIMIT HEAP
+find "$@" -type f \( -name '*.fzn' -o -name '*.xml' -o -name '*.xml.lzma' \) | sort > "$OUT/instances.txt"
+echo "$(wc -l < "$OUT/instances.txt") instances, $WORKERS workers, $LIMIT s, $HEAP"
+tr '\n' '\0' < "$OUT/instances.txt" | xargs -0 -P "$WORKERS" -I{} sh -c '
+ f="$1"; n=$(basename "$f")
+ java -Xmx"$HEAP" ${SPEC_OPTS:-} -cp "$CP" ${SPEC_MAIN:-org.chocosolver.parser.spec.SpecEquivalenceMain} "$f" "$LIMIT" \
+ > "$OUT/logs/$n.out" 2> "$OUT/logs/$n.err"
+ code=$?
+ if ! grep -q "^RESULT" "$OUT/logs/$n.out"; then
+ reason=$(grep -m1 -o "OutOfMemoryError" "$OUT/logs/$n.err" || echo "exit $code")
+ printf "RESULT\tCRASH\t%s\t-\t%s\n" "$n" "$reason" > "$OUT/logs/$n.out"
+ fi
+ grep "^RESULT" "$OUT/logs/$n.out" | cut -f2- >> "$OUT/results.tsv"
+' _ {}
+echo "done"; cut -f1 "$OUT/results.tsv" | sort | uniq -c
diff --git a/parsers/src/main/java/org/chocosolver/parser/RegParser.java b/parsers/src/main/java/org/chocosolver/parser/RegParser.java
index 5506e46aa1..127b847f20 100644
--- a/parsers/src/main/java/org/chocosolver/parser/RegParser.java
+++ b/parsers/src/main/java/org/chocosolver/parser/RegParser.java
@@ -29,6 +29,7 @@
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Comparator;
+import java.util.List;
/**
* A regular parser with default and common services
@@ -221,6 +222,18 @@ public void createSolver() {
}
}
+ /**
+ * Create a model to be filled by the parser.
+ * Override to use a specific kind of model, e.g., a model which can be duplicated ({@link Model#record}).
+ *
+ * @param name name of the model
+ * @param settings settings of the model
+ * @return a new model
+ */
+ protected Model newModel(String name, SettingsBuilder settings) {
+ return new Model(name, settings);
+ }
+
public void freesearch(Solver solver) {
BlackBoxConfigurator bb;
if (solver.getObjectiveManager().isOptimization()) {
@@ -257,6 +270,13 @@ public final boolean setUp(String... args) throws SetUpException {
return true;
}
+ /**
+ * @return the models built by this parser, one per core
+ */
+ public final List getModels() {
+ return portfolio.getModels();
+ }
+
/**
* Create a complementary search on non-decision variables
*
diff --git a/parsers/src/main/java/org/chocosolver/parser/dimacs/DIMACS.java b/parsers/src/main/java/org/chocosolver/parser/dimacs/DIMACS.java
index 3488526be3..06e5532c08 100644
--- a/parsers/src/main/java/org/chocosolver/parser/dimacs/DIMACS.java
+++ b/parsers/src/main/java/org/chocosolver/parser/dimacs/DIMACS.java
@@ -62,7 +62,7 @@ public void createSolver() {
String iname = Paths.get(instance).getFileName().toString();
parsers = new DIMACSParser[nb_cores];
for (int i = 0; i < nb_cores; i++) {
- Model threadModel = new Model(iname + "_" + (i + 1), this.setEnableSAT(!cp));
+ Model threadModel = newModel(iname + "_" + (i + 1), this.setEnableSAT(!cp));
threadModel.getSolver().logWithANSI(ansi);
portfolio.addModel(threadModel);
parsers[i] = new DIMACSParser();
diff --git a/parsers/src/main/java/org/chocosolver/parser/flatzinc/Flatzinc.java b/parsers/src/main/java/org/chocosolver/parser/flatzinc/Flatzinc.java
index 72936c0cfd..54d1ff07fe 100644
--- a/parsers/src/main/java/org/chocosolver/parser/flatzinc/Flatzinc.java
+++ b/parsers/src/main/java/org/chocosolver/parser/flatzinc/Flatzinc.java
@@ -122,7 +122,7 @@ public void createSolver() {
datas = new Datas[nb_cores];
String iname = instance == null ? "" : Paths.get(instance).getFileName().toString();
for (int i = 0; i < nb_cores; i++) {
- Model threadModel = new Model(iname + "_" + (i + 1), this);
+ Model threadModel = newModel(iname + "_" + (i + 1), this);
threadModel.getSolver().logWithANSI(ansi);
portfolio.addModel(threadModel);
datas[i] = new Datas(threadModel, level, oss);
diff --git a/parsers/src/main/java/org/chocosolver/parser/flatzinc/ast/FConstraint.java b/parsers/src/main/java/org/chocosolver/parser/flatzinc/ast/FConstraint.java
index a8dc87245c..f4f7e9df04 100755
--- a/parsers/src/main/java/org/chocosolver/parser/flatzinc/ast/FConstraint.java
+++ b/parsers/src/main/java/org/chocosolver/parser/flatzinc/ast/FConstraint.java
@@ -436,7 +436,7 @@ public void build(Model model, Datas datas, String id, List exps, Li
bbs[i] = (BoolVar) bs[i];
}
bbs[bs.length] = r;
- new Constraint("BoolSumLeq0Reif", new PropBoolSumEq0Reif(bbs)).post();
+ model.custom("BoolSumLeq0Reif", bbs, PropBoolSumEq0Reif::new).post();
return;
}
if (c.isInstantiated()) {
@@ -491,7 +491,7 @@ public void build(Model model, Datas datas, String id, List exps, Li
bbs[i] = (BoolVar) bs[i];
}
bbs[bs.length] = r;
- new Constraint("BoolSumEq0Reif", new PropBoolSumEq0Reif(bbs)).post();
+ model.custom("BoolSumEq0Reif", bbs, PropBoolSumEq0Reif::new).post();
return;
}
}
diff --git a/parsers/src/main/java/org/chocosolver/parser/mps/MPS.java b/parsers/src/main/java/org/chocosolver/parser/mps/MPS.java
index 6dfc0bce81..fd7cda93c8 100644
--- a/parsers/src/main/java/org/chocosolver/parser/mps/MPS.java
+++ b/parsers/src/main/java/org/chocosolver/parser/mps/MPS.java
@@ -93,7 +93,7 @@ public void createSolver() {
String iname = Paths.get(instance).getFileName().toString();
parsers = new MPSParser[nb_cores];
for (int i = 0; i < nb_cores; i++) {
- Model threadModel = new Model(iname + "_" + (i + 1), this);
+ Model threadModel = newModel(iname + "_" + (i + 1), this);
threadModel.getSolver().logWithANSI(ansi);
threadModel.setPrecision(precision);
portfolio.addModel(threadModel);
diff --git a/parsers/src/main/java/org/chocosolver/parser/xcsp/XCSP.java b/parsers/src/main/java/org/chocosolver/parser/xcsp/XCSP.java
index b5796951dc..5473457927 100644
--- a/parsers/src/main/java/org/chocosolver/parser/xcsp/XCSP.java
+++ b/parsers/src/main/java/org/chocosolver/parser/xcsp/XCSP.java
@@ -77,7 +77,7 @@ public void createSolver() {
String iname = Paths.get(instance).getFileName().toString();
parsers = new XCSPParser[nb_cores];
for (int i = 0; i < nb_cores; i++) {
- Model threadModel = new Model(iname + "_" + (i + 1), this);
+ Model threadModel = newModel(iname + "_" + (i + 1), this);
threadModel.getSolver().logWithANSI(ansi);
portfolio.addModel(threadModel);
parsers[i] = new XCSPParser();
diff --git a/parsers/src/test/java/org/chocosolver/parser/spec/SpecEquivalenceMain.java b/parsers/src/test/java/org/chocosolver/parser/spec/SpecEquivalenceMain.java
new file mode 100644
index 0000000000..b927a655c7
--- /dev/null
+++ b/parsers/src/test/java/org/chocosolver/parser/spec/SpecEquivalenceMain.java
@@ -0,0 +1,79 @@
+/*
+ * This file is part of choco-parsers, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.parser.spec;
+
+import org.testng.SkipException;
+
+import java.io.PrintStream;
+import java.nio.file.Paths;
+
+/**
+ * Runs the equivalence check of {@link SpecEquivalenceTest} on a single instance, to be launched in its own JVM
+ * (see {@code etc/spec-bench.sh}): a large instance can neither exhaust the memory of the others nor survive its
+ * time limit.
+ *
+ * Prints one line: {@code RESULTstatusinstancesecondsdetail}, with status among OK, FAIL,
+ * SKIP (not journalizable), ERROR, OOM and TIMEOUT.
+ *
+ * Usage: {@code SpecEquivalenceMain [time limit in seconds]}; the system properties of
+ * {@link SpecEquivalenceTest} apply.
+ *
+ * @author Charles Prud'homme
+ */
+public class SpecEquivalenceMain {
+
+ public static void main(String[] args) {
+ String file = args[0];
+ long limit = args.length > 1 ? Long.parseLong(args[1]) : 600;
+ String name = Paths.get(file).getFileName().toString();
+ System.setProperty("spec.progress", "false");
+ PrintStream out = System.out;
+ // the parsers and the solver may print: keep stdout for the result only
+ System.setOut(new PrintStream(java.io.OutputStream.nullOutputStream()));
+ long start = System.currentTimeMillis();
+ Thread watchdog = new Thread(() -> {
+ try {
+ Thread.sleep(limit * 1000);
+ result(out, "TIMEOUT", name, start, "exceeds " + limit + " s");
+ Runtime.getRuntime().halt(3);
+ } catch (InterruptedException ignored) {
+ // done in time
+ }
+ });
+ watchdog.setDaemon(true);
+ watchdog.start();
+ SpecEquivalenceTest test = new SpecEquivalenceTest();
+ int status = 0;
+ try {
+ test.testEquivalence(name, file);
+ result(out, "OK", name, start, test.usageSummary());
+ } catch (SkipException e) {
+ result(out, "SKIP", name, start, e.getMessage());
+ } catch (AssertionError e) {
+ result(out, "FAIL", name, start, e.getMessage());
+ status = 1;
+ } catch (OutOfMemoryError e) {
+ result(out, "OOM", name, start, "");
+ status = 2;
+ } catch (Throwable e) {
+ result(out, "ERROR", name, start, e.toString());
+ status = 1;
+ }
+ watchdog.interrupt();
+ out.flush();
+ Runtime.getRuntime().halt(status);
+ }
+
+ private static void result(PrintStream out, String status, String name, long start, String detail) {
+ String d = detail == null ? "" : detail.replace('\t', ' ').replace('\n', ' ');
+ if (d.length() > 500) {
+ d = d.substring(0, 500) + "...";
+ }
+ out.printf("RESULT\t%s\t%s\t%.1f\t%s%n", status, name, (System.currentTimeMillis() - start) / 1000.0, d);
+ out.flush();
+ }
+}
diff --git a/parsers/src/test/java/org/chocosolver/parser/spec/SpecEquivalenceTest.java b/parsers/src/test/java/org/chocosolver/parser/spec/SpecEquivalenceTest.java
new file mode 100644
index 0000000000..fa35def394
--- /dev/null
+++ b/parsers/src/test/java/org/chocosolver/parser/spec/SpecEquivalenceTest.java
@@ -0,0 +1,331 @@
+/*
+ * This file is part of choco-parsers, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.parser.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.Solver;
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.search.strategy.Search;
+import org.chocosolver.solver.spec.ModelSpec;
+import org.chocosolver.solver.spec.Recorder;
+import org.chocosolver.solver.spec.SharedObjects;
+import org.chocosolver.solver.spec.SpecException;
+import org.chocosolver.solver.spec.Step;
+import org.chocosolver.solver.variables.IntVar;
+import org.chocosolver.solver.variables.Variable;
+import org.testng.Assert;
+import org.testng.SkipException;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Equivalence bench of {@link ModelSpec}: each instance is parsed into a plain model and into a model recorded
+ * with {@link Model#record}, whose spec is instantiated twice. All the models must have the same structure and the
+ * same search (solutions, nodes, fails), and the instances must not share mutable objects with each other or with
+ * the recorded model.
+ *
+ * Instances which cannot be journalized are skipped and reported in {@code target/spec-coverage.md}.
+ *
+ * System properties:
+ *
+ * {@code spec.dir}: a directory of instances (.fzn, .xml, .xml.lzma) to use instead of the instances of the
+ * test resources listed in {@code spec-instances.txt} (one per family of problems);
+ * {@code spec.nodes}: node limit of each resolution (default: 100);
+ * {@code spec.sharing}: whether shared objects are looked for (default: true);
+ * {@code spec.sharing.maxsize}: maximum number of variables and constraints of a model in which shared
+ * objects are looked for (default: 20000);
+ * {@code spec.filter}: regular expression selecting the instances by file name (default: all).
+ *
+ *
+ * @author Charles Prud'homme
+ */
+public class SpecEquivalenceTest {
+
+ private static final long NODES = Long.getLong("spec.nodes", 100);
+
+ /**
+ * The instances of the test resources to use, one per family of problems: similar instances add time, not
+ * coverage.
+ */
+ private static final String INSTANCES = "spec-instances.txt";
+
+ /**
+ * Whether shared objects are looked for (memory consuming on large instances).
+ */
+ private static final boolean SHARING = Boolean.parseBoolean(System.getProperty("spec.sharing", "true"));
+
+ /**
+ * Shared objects are not looked for in models with more variables and constraints (memory consuming).
+ */
+ private static final int SHARING_MAX_SIZE = Integer.getInteger("spec.sharing.maxsize", 20_000);
+
+ /**
+ * Regular expression selecting the instances by name.
+ */
+ private static final String FILTER = System.getProperty("spec.filter", ".*");
+
+ /**
+ * Two models parsed independently own their tuples and diagrams: not worth traversing to find the (global)
+ * objects they already share.
+ */
+ private static final Set> BASELINE_ALLOWED = Set.of(PrintStream.class,
+ org.chocosolver.solver.constraints.extension.Tuples.class,
+ org.chocosolver.util.objects.graphs.MultivaluedDecisionDiagram.class);
+
+ /**
+ * Types which may be shared by models: immutable, or shared by design.
+ */
+ private static final Set> ALLOWED = Set.of(PrintStream.class);
+
+ // coverage, aggregated over instances
+ private final Map unsupported = new TreeMap<>();
+ private final List skipped = new ArrayList<>();
+ private final Map usage = new TreeMap<>();
+ private final Map baseline = new TreeMap<>();
+ private final List sharingSkipped = new ArrayList<>();
+ private int passed;
+
+ @DataProvider
+ public Object[][] instances() throws IOException {
+ String dir = System.getProperty("spec.dir");
+ List res = new ArrayList<>();
+ if (dir != null) {
+ Path root = Paths.get(dir);
+ try (Stream files = Files.walk(root)) {
+ files.map(Path::toString)
+ .filter(f -> f.endsWith(".fzn") || f.endsWith(".xml") || f.endsWith(".xml.lzma"))
+ .filter(f -> Paths.get(f).getFileName().toString().matches(FILTER))
+ .sorted()
+ .forEach(f -> res.add(new Object[]{root.relativize(Paths.get(f)).toString(), f}));
+ }
+ } else {
+ Path list = Paths.get(Objects.requireNonNull(getClass().getResource("/" + INSTANCES)).getPath());
+ Path root = list.getParent();
+ for (String line : Files.readAllLines(list)) {
+ String name = line.strip();
+ if (!name.isEmpty() && !name.startsWith("#")
+ && Paths.get(name).getFileName().toString().matches(FILTER)) {
+ res.add(new Object[]{name, root.resolve(name).toString()});
+ }
+ }
+ }
+ return res.toArray(new Object[0][]);
+ }
+
+ @Test(groups = "spec", dataProvider = "instances", timeOut = 120_000)
+ public void testEquivalence(String name, String file) throws Exception {
+ progress(name);
+ // the models are kept alive only when needed, to save memory on large instances
+ Model direct = build(file, false);
+ String expected = structure(direct);
+ Set baseline = Set.of();
+ boolean sharing = SHARING && direct.getNbVars() + direct.getNbCstrs() <= SHARING_MAX_SIZE;
+ if (!sharing) {
+ reportSharingSkipped(name);
+ } else {
+ // objects already shared by two models parsed independently (global singletons): excused by identity
+ baseline = SharedObjects.common(direct, build(file, false), BASELINE_ALLOWED, List.of());
+ reportBaseline(baseline);
+ }
+ String stats = solve(direct);
+ direct = null;
+
+ Model recorded = build(file, true);
+ ModelSpec spec;
+ try {
+ spec = recorded.getRecorder().snapshot();
+ } catch (SpecException e) {
+ reportUnsupported(name, recorded.getRecorder());
+ throw new SkipException("cannot be journalized: " + recorded.getRecorder().unsupported().get(0));
+ }
+ assertSame(structure(recorded), expected, "recording changes the model");
+ List excused = new ArrayList<>(SharedObjects.constantsOf(spec));
+ excused.addAll(baseline);
+ List shared = new ArrayList<>();
+ Model i1 = spec.instantiate().model();
+ assertSame(structure(i1), expected, "the instantiated model differs");
+ if (sharing) {
+ shared.addAll(SharedObjects.between(recorded, i1, ALLOWED, excused));
+ }
+ recorded = null;
+ Model i2 = spec.instantiate().model();
+ if (sharing) {
+ shared.addAll(SharedObjects.between(i1, i2, ALLOWED, excused));
+ Assert.assertTrue(shared.isEmpty(), "shared mutable objects: " + shared);
+ }
+
+ // same search, the instances being solved concurrently
+ ExecutorService pool = Executors.newFixedThreadPool(2);
+ try {
+ Future f1 = pool.submit(() -> solve(i1));
+ Future f2 = pool.submit(() -> solve(i2));
+ Assert.assertEquals(f1.get(), stats, "the instantiated model searches differently");
+ Assert.assertEquals(f2.get(), stats, "the instantiated model searches differently");
+ } finally {
+ pool.shutdownNow();
+ }
+ reportUsage(spec);
+ }
+
+ /**
+ * Log the instance being tested, to identify it if the JVM crashes.
+ */
+ private static void progress(String name) throws IOException {
+ if (!Boolean.parseBoolean(System.getProperty("spec.progress", "true"))) {
+ return;
+ }
+ Path log = Paths.get(System.getProperty("user.dir"), "target", "spec-progress.log");
+ Files.createDirectories(log.getParent());
+ Files.writeString(log, name + "\n", java.nio.file.StandardOpenOption.CREATE, java.nio.file.StandardOpenOption.APPEND);
+ }
+
+ /**
+ * Compare two structures, reporting the first different line.
+ */
+ private static void assertSame(String actual, String expected, String message) {
+ if (!actual.equals(expected)) {
+ String[] a = actual.split("\n");
+ String[] e = expected.split("\n");
+ int i = 0;
+ while (i < a.length && i < e.length && a[i].equals(e[i])) {
+ i++;
+ }
+ Assert.fail(message + ", line " + i + ": expected <" + (i < e.length ? e[i] : "EOF")
+ + "> but was <" + (i < a.length ? a[i] : "EOF") + ">");
+ }
+ }
+
+ /**
+ * Parse file into a plain model, or into a recorded model.
+ */
+ private static Model build(String file, boolean record) {
+ try {
+ return SpecParsers.parse(file, 1, record).getModel();
+ } catch (IllegalStateException e) {
+ throw new SkipException(e.getMessage());
+ }
+ }
+
+ /**
+ * @return a description of the structure of the model: variables and constraints, in order
+ */
+ private static String structure(Model m) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(m.getNbVars()).append(" vars, ").append(m.getNbCstrs()).append(" cstrs, objective: ")
+ .append(m.getObjective() == null ? "none" : m.getObjective().getName()).append('\n');
+ for (Variable v : m.getVars()) {
+ sb.append(v.getClass().getSimpleName()).append(' ').append(v.getName());
+ if (v instanceof IntVar) {
+ sb.append(' ').append(((IntVar) v).getLB()).append("..").append(((IntVar) v).getUB())
+ .append(" #").append(((IntVar) v).getDomainSize());
+ }
+ sb.append('\n');
+ }
+ m.getGroups().forEach(g -> sb.append("group ").append(g.getName()).append(Arrays.stream(g.getVariables())
+ .map(Variable::getName).collect(Collectors.joining(",", "[", "]"))).append('\n'));
+ for (Constraint c : m.getCstrs()) {
+ sb.append(c.getName()).append(Arrays.stream(c.getPropagators())
+ .map(p -> p.getClass().getSimpleName() + "/" + p.getNbVars())
+ .collect(Collectors.joining(",", "[", "]"))).append('\n');
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Solve with the default (deterministic) search, limited in nodes.
+ */
+ private static String solve(Model m) {
+ Solver s = m.getSolver();
+ Search.defaultSearch(m);
+ s.limitNode(NODES);
+ Number best = null;
+ while (s.solve()) {
+ if (m.getObjective() != null) {
+ best = s.getBestSolutionValue();
+ }
+ }
+ return s.getSolutionCount() + " sols, " + s.getNodeCount() + " nodes, " + s.getFailCount() + " fails"
+ + (best == null ? "" : ", best=" + best);
+ }
+
+ private synchronized void reportUnsupported(String name, Recorder recorder) {
+ skipped.add(name + " (" + recorder.unsupported().size() + ")");
+ Set reasons = new HashSet<>();
+ for (Recorder.Unsupported u : recorder.unsupported()) {
+ reasons.add(u.operation() + ": " + u.reason().replaceAll("variable \\S+", "variable _"));
+ }
+ reasons.forEach(r -> unsupported.merge(r, 1, Integer::sum));
+ }
+
+ private synchronized void reportSharingSkipped(String name) {
+ sharingSkipped.add(name);
+ }
+
+ private synchronized void reportBaseline(Set objects) {
+ objects.stream().map(o -> o.getClass().getName()).distinct().forEach(t -> baseline.merge(t, 1, Integer::sum));
+ }
+
+ private synchronized void reportUsage(ModelSpec spec) {
+ passed++;
+ Set used = new HashSet<>();
+ for (Step s : spec.steps()) {
+ used.add(s instanceof Step.Call ? ((Step.Call) s).method().name() : s.getClass().getSimpleName());
+ }
+ used.forEach(u -> usage.merge(u, 1, Integer::sum));
+ }
+
+ /**
+ * @return the kinds of steps used by the journalized instances, and the instances whose sharing was not
+ * checked
+ */
+ synchronized String usageSummary() {
+ return String.join(",", usage.keySet()) + (sharingSkipped.isEmpty() ? "" : " [sharing not checked]");
+ }
+
+ @AfterClass(alwaysRun = true)
+ public void writeReport() throws IOException {
+ StringBuilder sb = new StringBuilder();
+ sb.append("# ModelSpec coverage\n\n")
+ .append("- journalized and equivalent: ").append(passed).append('\n')
+ .append("- not journalizable: ").append(skipped.size()).append("\n\n")
+ .append("## Unsupported operations (number of instances)\n\n");
+ unsupported.entrySet().stream().sorted(Map.Entry.comparingByValue().reversed())
+ .forEach(e -> sb.append("- ").append(e.getValue()).append(" × ").append(e.getKey()).append('\n'));
+ sb.append("\n## Not journalizable instances\n\n");
+ skipped.forEach(s -> sb.append("- ").append(s).append('\n'));
+ sb.append("\n## Shared objects not looked for (large models): ").append(sharingSkipped.size()).append("\n\n");
+ sharingSkipped.forEach(n -> sb.append("- ").append(n).append('\n'));
+ sb.append("\n## Objects shared by two models parsed independently (number of instances)\n\n");
+ baseline.forEach((k, v) -> sb.append("- ").append(k).append(": ").append(v).append('\n'));
+ sb.append("\n## Steps used by the journalized instances (number of instances)\n\n");
+ usage.forEach((k, v) -> sb.append("- ").append(k).append(": ").append(v).append('\n'));
+ Path out = Paths.get(System.getProperty("user.dir"), "target", "spec-coverage.md");
+ Files.createDirectories(out.getParent());
+ Files.writeString(out, sb.toString());
+ System.out.println(sb);
+ }
+}
diff --git a/parsers/src/test/java/org/chocosolver/parser/spec/SpecOverheadBench.java b/parsers/src/test/java/org/chocosolver/parser/spec/SpecOverheadBench.java
new file mode 100644
index 0000000000..1593d2dbff
--- /dev/null
+++ b/parsers/src/test/java/org/chocosolver/parser/spec/SpecOverheadBench.java
@@ -0,0 +1,156 @@
+/*
+ * This file is part of choco-parsers, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.parser.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.spec.ModelSpec;
+
+import java.io.PrintStream;
+import java.lang.ref.Reference;
+import java.lang.management.ManagementFactory;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Measures, on one instance, the overhead of recording the construction of a model:
+ *
+ * building time: parsing into a plain model ({@code create}) versus into a recorded model
+ * ({@code record});
+ * cost of {@code snapshot} and of {@code instantiate} (replay of the spec), compared to parsing;
+ * memory: the plain model, the recorded model (model and journal), and the spec alone.
+ *
+ * Times are medians over {@code spec.reps} repetitions (default 5), after a warm-up.
+ *
+ * To be launched in its own JVM (see {@code etc/spec-bench.sh}, with {@code SPEC_MAIN} set to this class). Prints one
+ * line: {@code RESULTstatusinstancesecondsmetrics}, metrics being {@code key=value} pairs
+ * separated by {@code ;}.
+ *
+ * Usage: {@code SpecOverheadBench }.
+ *
+ * @author Charles Prud'homme
+ */
+public class SpecOverheadBench {
+
+ private static final int REPS = Integer.getInteger("spec.reps", 5);
+ private static final int WARMUP = Integer.getInteger("spec.warmup", 2);
+
+ /**
+ * @return the used heap, in KB, after garbage collection
+ */
+ private static long heap() {
+ for (int i = 0; i < 3; i++) {
+ System.gc();
+ }
+ return ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed() >> 10;
+ }
+
+ private static double ms(long startNanos) {
+ return (System.nanoTime() - startNanos) / 1e6;
+ }
+
+ private static double median(double[] values) {
+ double[] v = values.clone();
+ Arrays.sort(v);
+ return v.length % 2 == 1 ? v[v.length / 2] : (v[v.length / 2 - 1] + v[v.length / 2]) / 2;
+ }
+
+ public static void main(String[] args) {
+ String file = args[0];
+ long limit = args.length > 1 ? Long.parseLong(args[1]) : 1800;
+ String name = Paths.get(file).getFileName().toString();
+ PrintStream out = System.out;
+ System.setOut(new PrintStream(java.io.OutputStream.nullOutputStream()));
+ long start = System.currentTimeMillis();
+ Thread watchdog = new Thread(() -> {
+ try {
+ Thread.sleep(limit * 1000);
+ result(out, "TIMEOUT", name, start, "exceeds " + limit + " s");
+ Runtime.getRuntime().halt(3);
+ } catch (InterruptedException ignored) {
+ // done in time
+ }
+ });
+ watchdog.setDaemon(true);
+ watchdog.start();
+ StringBuilder metrics = new StringBuilder();
+ int status = 0;
+ try {
+ for (int i = 0; i < WARMUP; i++) {
+ SpecParsers.parse(file, 1, false);
+ SpecParsers.parse(file, 1, true).getModel().getRecorder().snapshot().instantiate();
+ }
+ double[] create = new double[REPS];
+ double[] record = new double[REPS];
+ double[] snapshot = new double[REPS];
+ double[] instantiate = new double[REPS];
+ int steps = 0;
+ int vars = 0;
+ int cstrs = 0;
+ for (int r = 0; r < REPS; r++) {
+ heap();
+ long t = System.nanoTime();
+ Model plain = SpecParsers.parse(file, 1, false).getModel();
+ create[r] = ms(t);
+ vars = plain.getNbVars();
+ cstrs = plain.getNbCstrs();
+ plain = null;
+
+ heap();
+ t = System.nanoTime();
+ Model recorded = SpecParsers.parse(file, 1, true).getModel();
+ record[r] = ms(t);
+ t = System.nanoTime();
+ ModelSpec spec = recorded.getRecorder().snapshot();
+ snapshot[r] = ms(t);
+ steps = spec.steps().size();
+ recorded = null;
+
+ heap();
+ t = System.nanoTime();
+ spec.instantiate();
+ instantiate[r] = ms(t);
+ }
+ metrics.append("vars=").append(vars).append(";cstrs=").append(cstrs).append(";steps=").append(steps)
+ .append(String.format(Locale.US, ";create_ms=%.1f;record_ms=%.1f;snapshot_ms=%.2f;instantiate_ms=%.1f",
+ median(create), median(record), median(snapshot), median(instantiate)));
+
+ // memory: each object graph alone
+ long base = heap();
+ Model plain = SpecParsers.parse(file, 1, false).getModel();
+ long createKb = heap() - base;
+ Reference.reachabilityFence(plain);
+ plain = null;
+ base = heap();
+ Model recorded = SpecParsers.parse(file, 1, true).getModel();
+ long recordKb = heap() - base;
+ ModelSpec spec = recorded.getRecorder().snapshot();
+ Reference.reachabilityFence(recorded);
+ recorded = null;
+ long specKb = heap() - base;
+ Reference.reachabilityFence(spec);
+ metrics.append(";create_kb=").append(createKb).append(";record_kb=").append(recordKb)
+ .append(";spec_kb=").append(specKb);
+ result(out, "OK", name, start, metrics.toString());
+ } catch (OutOfMemoryError e) {
+ result(out, "OOM", name, start, metrics.toString());
+ status = 2;
+ } catch (Throwable e) {
+ result(out, "ERROR", name, start, metrics + ";error=" + e);
+ status = 1;
+ }
+ watchdog.interrupt();
+ out.flush();
+ Runtime.getRuntime().halt(status);
+ }
+
+ private static void result(PrintStream out, String status, String name, long start, String detail) {
+ String d = detail.replace('\t', ' ').replace('\n', ' ');
+ out.printf(Locale.US, "RESULT\t%s\t%s\t%.1f\t%s%n", status, name, (System.currentTimeMillis() - start) / 1000.0, d);
+ out.flush();
+ }
+}
diff --git a/parsers/src/test/java/org/chocosolver/parser/spec/SpecParsers.java b/parsers/src/test/java/org/chocosolver/parser/spec/SpecParsers.java
new file mode 100644
index 0000000000..df484cb80c
--- /dev/null
+++ b/parsers/src/test/java/org/chocosolver/parser/spec/SpecParsers.java
@@ -0,0 +1,65 @@
+/*
+ * This file is part of choco-parsers, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.parser.spec;
+
+import org.chocosolver.parser.RegParser;
+import org.chocosolver.parser.SetUpException;
+import org.chocosolver.parser.flatzinc.Flatzinc;
+import org.chocosolver.parser.xcsp.XCSP;
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.SettingsBuilder;
+
+/**
+ * Builds the models of an instance (FlatZinc or XCSP) for the tests and benchmarks of this package.
+ *
+ * @author Charles Prud'homme
+ */
+final class SpecParsers {
+
+ private SpecParsers() {
+ }
+
+ /**
+ * Parse file into plain models, or into recorded models ({@link Model#record}).
+ * The search declared by the instance is ignored (free search): it is not journalized.
+ *
+ * @param file a FlatZinc or XCSP instance
+ * @param cores number of models to build
+ * @param record true to record the models
+ * @return the parser, whose models are built
+ * @throws IllegalStateException if the parser cannot be set up
+ */
+ static RegParser parse(String file, int cores, boolean record) {
+ RegParser parser = file.endsWith(".fzn")
+ ? new Flatzinc(false, false, cores) {
+ @Override
+ protected Model newModel(String name, SettingsBuilder settings) {
+ return record ? Model.record(name, settings.build()) : super.newModel(name, settings);
+ }
+ }
+ : new XCSP() {
+ @Override
+ protected Model newModel(String name, SettingsBuilder settings) {
+ return record ? Model.record(name, settings.build()) : super.newModel(name, settings);
+ }
+ };
+ boolean ready;
+ try {
+ // no shutdown hook: it would retain the models until the end of the JVM
+ ready = parser.setUp(file, "-lvl", "SILENT", "-p", String.valueOf(cores), "-f", "true",
+ "--disable-shutdown-hook", "true");
+ } catch (SetUpException e) {
+ throw new IllegalStateException("cannot set up the parser", e);
+ }
+ if (!ready) {
+ throw new IllegalStateException("cannot set up the parser");
+ }
+ parser.createSolver();
+ parser.buildModel();
+ return parser;
+ }
+}
diff --git a/parsers/src/test/java/org/chocosolver/parser/spec/SpecPortfolioBench.java b/parsers/src/test/java/org/chocosolver/parser/spec/SpecPortfolioBench.java
new file mode 100644
index 0000000000..fec490d24a
--- /dev/null
+++ b/parsers/src/test/java/org/chocosolver/parser/spec/SpecPortfolioBench.java
@@ -0,0 +1,194 @@
+/*
+ * This file is part of choco-parsers, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.parser.spec;
+
+import org.chocosolver.parser.RegParser;
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.ParallelPortfolio;
+import org.chocosolver.solver.ResolutionPolicy;
+import org.chocosolver.solver.search.SearchState;
+import org.chocosolver.solver.spec.ModelSpec;
+
+import java.io.PrintStream;
+import java.lang.management.ManagementFactory;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+ * Compares, on one instance, the ways of building the n models of a portfolio:
+ *
+ * (a) as the parsers currently do: n parsings, one after the other;
+ * (a') n parsings run concurrently;
+ * (b) with a {@link ModelSpec}: one recorded parsing (the master, which also solves), then n -1 models
+ * instantiated concurrently.
+ *
+ * For each: the building time and the heap used by the n models. Optionally, (a) and (b) are solved with a
+ * {@link ParallelPortfolio} under the same time limit.
+ *
+ * To be launched in its own JVM (see {@code etc/spec-bench.sh}, with {@code SPEC_MAIN} set to this class). Prints one
+ * line: {@code RESULTstatusinstancesecondsmetrics}, metrics being
+ * {@code key=value} pairs separated by {@code ;}.
+ *
+ * Usage: {@code SpecPortfolioBench }; system properties: {@code spec.workers} (default 4),
+ * {@code spec.solve} (time limit of each resolution in seconds, default 0: no resolution).
+ *
+ * @author Charles Prud'homme
+ */
+public class SpecPortfolioBench {
+
+ private static final int N = Integer.getInteger("spec.workers", 4);
+ private static final int SOLVE = Integer.getInteger("spec.solve", 0);
+
+ /**
+ * @return the used heap, in MB, after garbage collection
+ */
+ private static long heap() {
+ for (int i = 0; i < 3; i++) {
+ System.gc();
+ }
+ return ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed() >> 20;
+ }
+
+ private static long ms(long startNanos) {
+ return (System.nanoTime() - startNanos) / 1_000_000;
+ }
+
+ /**
+ * Solve with a portfolio under the time limit.
+ *
+ * @return "time;best;complete"
+ */
+ private static String solve(ParallelPortfolio portfolio) {
+ for (Model m : portfolio.getModels()) {
+ m.getSolver().limitTime(SOLVE + "s");
+ }
+ long t = System.nanoTime();
+ Number best = null;
+ boolean found = false;
+ while (portfolio.solve()) {
+ found = true;
+ Model m = portfolio.getBestModel();
+ if (m.getResolutionPolicy() == ResolutionPolicy.SATISFACTION) {
+ break;
+ }
+ best = m.getSolver().getBestSolutionValue();
+ }
+ // a model which ended by itself (not stopped by a limit or by another model) completed the search
+ boolean complete = portfolio.getModels().stream()
+ .anyMatch(m -> m.getSolver().getSearchState() == SearchState.TERMINATED);
+ return String.format(Locale.US, "%.2f;%s;%s", ms(t) / 1000., found ? (best == null ? "sat" : best) : "none",
+ complete);
+ }
+
+ public static void main(String[] args) throws Exception {
+ String file = args[0];
+ long limit = args.length > 1 ? Long.parseLong(args[1]) : 1800;
+ String name = Paths.get(file).getFileName().toString();
+ PrintStream out = System.out;
+ System.setOut(new PrintStream(java.io.OutputStream.nullOutputStream()));
+ long start = System.currentTimeMillis();
+ Thread watchdog = new Thread(() -> {
+ try {
+ Thread.sleep(limit * 1000);
+ result(out, "TIMEOUT", name, start, "exceeds " + limit + " s");
+ Runtime.getRuntime().halt(3);
+ } catch (InterruptedException ignored) {
+ // done in time
+ }
+ });
+ watchdog.setDaemon(true);
+ watchdog.start();
+ StringBuilder metrics = new StringBuilder("workers=" + N);
+ int status = 0;
+ try {
+ // warm-up of both paths (class loading, JIT), not measured
+ Model warm = SpecParsers.parse(file, 1, true).getModel();
+ ParallelPortfolio.of(warm.getRecorder().snapshot(), 1);
+ SpecParsers.parse(file, 1, false);
+ warm = null;
+
+ long base = heap();
+ // (a) n parsings, one after the other, as the parsers do
+ long t = System.nanoTime();
+ RegParser a = SpecParsers.parse(file, N, false);
+ long buildA = ms(t);
+ long heapA = heap() - base;
+ List modelsA = new ArrayList<>(a.getModels());
+ metrics.append(";a_ms=").append(buildA).append(";a_mb=").append(heapA)
+ .append(";vars=").append(modelsA.get(0).getNbVars())
+ .append(";cstrs=").append(modelsA.get(0).getNbCstrs());
+ if (SOLVE > 0) {
+ ParallelPortfolio pa = new ParallelPortfolio();
+ modelsA.forEach(pa::addModel);
+ metrics.append(";a_solve=").append(solve(pa));
+ }
+ a = null;
+ modelsA = null;
+
+ // (a') n parsings run concurrently
+ base = heap();
+ ExecutorService pool = Executors.newFixedThreadPool(N);
+ t = System.nanoTime();
+ List> futures = new ArrayList<>();
+ for (int i = 0; i < N; i++) {
+ futures.add(pool.submit(() -> SpecParsers.parse(file, 1, false)));
+ }
+ List parsers = new ArrayList<>();
+ for (Future f : futures) {
+ parsers.add(f.get());
+ }
+ long buildA2 = ms(t);
+ pool.shutdownNow();
+ long heapA2 = heap() - base;
+ metrics.append(";a2_ms=").append(buildA2).append(";a2_mb=").append(heapA2);
+ parsers = null;
+
+ // (b) one recorded parsing, then n-1 instances built concurrently
+ base = heap();
+ t = System.nanoTime();
+ RegParser b = SpecParsers.parse(file, 1, true);
+ long parseB = ms(t);
+ Model master = b.getModel();
+ long t2 = System.nanoTime();
+ ModelSpec spec = master.getRecorder().snapshot();
+ long snapB = ms(t2);
+ t2 = System.nanoTime();
+ ParallelPortfolio pb = ParallelPortfolio.of(spec, N - 1);
+ long instB = ms(t2);
+ long buildB = ms(t);
+ pb.addModel(master);
+ long heapB = heap() - base;
+ metrics.append(";b_ms=").append(buildB).append(";b_parse_ms=").append(parseB)
+ .append(";b_snapshot_ms=").append(snapB).append(";b_instantiate_ms=").append(instB)
+ .append(";b_mb=").append(heapB).append(";steps=").append(spec.steps().size());
+ if (SOLVE > 0) {
+ metrics.append(";b_solve=").append(solve(pb));
+ }
+ result(out, "OK", name, start, metrics.toString());
+ } catch (OutOfMemoryError e) {
+ result(out, "OOM", name, start, metrics.toString());
+ status = 2;
+ } catch (Throwable e) {
+ result(out, "ERROR", name, start, metrics + ";error=" + e);
+ status = 1;
+ }
+ watchdog.interrupt();
+ out.flush();
+ Runtime.getRuntime().halt(status);
+ }
+
+ private static void result(PrintStream out, String status, String name, long start, String detail) {
+ String d = detail.replace('\t', ' ').replace('\n', ' ');
+ out.printf(Locale.US, "RESULT\t%s\t%s\t%.1f\t%s%n", status, name, (System.currentTimeMillis() - start) / 1000.0, d);
+ out.flush();
+ }
+}
diff --git a/parsers/src/test/resources/spec-instances.txt b/parsers/src/test/resources/spec-instances.txt
new file mode 100644
index 0000000000..6cf8e814a4
--- /dev/null
+++ b/parsers/src/test/resources/spec-instances.txt
@@ -0,0 +1,177 @@
+# Instances of SpecEquivalenceTest: one per family of problems (the fastest one), to keep the test short.
+# Paths relative to the test resources. Run the whole directories with -Dspec.dir=.
+flatzinc/2012/amaze+amaze+2012-07-04.fzn
+flatzinc/2012/carpet-cutting+cc_base+mzn_rnd_test.16.fzn
+flatzinc/2012/fast-food+fastfood+ff59.fzn
+flatzinc/2012/filters+filter+fir_1_1.fzn
+flatzinc/2012/league+league+model20-3-5.fzn
+flatzinc/2012/mspsp+mspsp+medium_02.fzn
+flatzinc/2012/nonogram+non+non_fast_4.fzn
+flatzinc/2012/parity-learning+parity-learning+44_22_5.3.fzn
+flatzinc/2012/pattern-set-mining-k2+pattern_set_mining_k2+audiology.fzn
+flatzinc/2012/project-planning+ProjectPlannertest_12_8.fzn
+flatzinc/2012/radiation+radiation+m07_07_20.fzn
+flatzinc/2012/ship-schedule+ship-schedule.cp+7ShipsMixed.fzn
+flatzinc/2012/solbat+sb+sb_14_14_6_0.fzn
+flatzinc/2012/still-life-wastage+still-life+09.fzn
+flatzinc/2012/tpp+tpp+tpp_5_3_20_1.fzn
+flatzinc/2012/train+train+instance.12.fzn
+flatzinc/2012/vrp+vrp+P-n20-k2.vrp.fzn
+flatzinc/2015/gfd-schedule+n120f5d50m50k20.fzn
+flatzinc/2016/nfc_12_2_10.fzn
+flatzinc/2017/tcgc2+k10_34.fzn
+flatzinc/2018/oocsp_racks+050_r1n.fzn
+flatzinc/2018/rotating-workforce+ex1479.fzn
+flatzinc/2018/steiner-tree+es10fst03.stp.fzn
+flatzinc/2018/test-scheduling+t30m10r3-15.fzn
+flatzinc/2019/ccmcp+3_20_0_25_5.fzn
+flatzinc/2019/fm3_3.fzn
+flatzinc/2019/foxgeesecorn+54.fzn
+flatzinc/2019/group+u6g1pref1.fzn
+flatzinc/2019/median_string_dp+p2_10_8-0.fzn
+flatzinc/2019/mknapsack_global+mknap2-1.fzn
+flatzinc/2019/zephyrus+12_6_6_3.fzn
+flatzinc/2020/bnn+inventory_4_8s.fzn
+flatzinc/2020/is+v1HjuSBQMb_new.fzn
+flatzinc/2020/lot_sizing_cp+pigment15b.psp.fzn
+flatzinc/2020/p1f-pjs+10_new.fzn
+flatzinc/2020/pentominoes+02.fzn
+flatzinc/2020/skill_allocation+mzn_1m_1.fzn
+flatzinc/2020/soccer+xIGData_22_12_22_5.fzn
+flatzinc/2020/stable-goods-solution+s-d6.fzn
+flatzinc/2021/atsp+instance5_0p15.fzn
+flatzinc/2021/connect__0018.fzn
+flatzinc/2021/feistel.fzn
+flatzinc/2021/mapping_mesh2x2_2.fzn
+flatzinc/2021/perfect_square+205.fzn
+flatzinc/2021/physician-scheduling.fzn
+flatzinc/2021/yumi-dynamic+p_10.fzn
+xcsp/2024/AztecDiamondSym-12_c24.xml.lzma
+xcsp/2024/BinPacking-n1c1w1a_c24.xml.lzma
+xcsp/2024/Cargo-01-0s-1913_m17.xml.lzma
+xcsp/2024/Charlotte-18-2_c24.xml.lzma
+xcsp/2024/Drinking-010000_c24.xml.lzma
+xcsp/2024/FoolSolitaire-dec2-0-2.xml.lzma
+xcsp/2024/SameQueensKnights-b-15_c24.xml.lzma
+xcsp/2024/StillLife-12-12_c24.xml.lzma
+xcsp/2024/TestScheduling-t020m10r03-1_c24.xml.lzma
+xcsp/2024/TravelingTournament-galaxy06_c24.xml.lzma
+flatzinc/2025/hitori_h5-1.fzn
+flatzinc/alpha/alpha.fzn
+xcsp/basics/AllInterval-005.xml.lzma
+xcsp/basics/Allergy.xml.lzma
+xcsp/basics/Auction-cnt-example_c18.xml.lzma
+xcsp/basics/Auction-sum-example_c18.xml.lzma
+xcsp/basics/Bacp-m1-06_c18.xml.lzma
+xcsp/basics/Bibd-sc-06-050-25-03-10.xml.lzma
+xcsp/basics/Bibd-sum-06-050-25-03-10.xml.lzma
+xcsp/basics/BinPacking-mdd-n1c1w4a.xml.lzma
+xcsp/basics/BinPacking-sum-n1c1w4a.xml.lzma
+xcsp/basics/BinPacking-tab-n1c1w4a.xml.lzma
+xcsp/basics/Blackhole-04-3-00.xml.lzma
+xcsp/basics/BusScheduling-cnt-t1.xml.lzma
+xcsp/basics/CarSequencing-dingbas.xml.lzma
+xcsp/basics/ChessboardColoration-07-07.xml.lzma
+xcsp/basics/ColouredQueens-07.xml.lzma
+xcsp/basics/CostasArray-12.xml.lzma
+xcsp/basics/Crossword-lex-vg-5-6.xml.lzma
+xcsp/basics/CrosswordDesign-03-4-rom_c18.xml.lzma
+xcsp/basics/CryptoPuzzle-cross-roads-danger.xml.lzma
+xcsp/basics/Cutstock-small.xml.lzma
+xcsp/basics/DistinctVectors-30-050-02.xml.lzma
+xcsp/basics/Domino-300-300.xml.lzma
+xcsp/basics/Fapp-m2s-ex2_c18.xml.lzma
+xcsp/basics/Fastfood-ff10.xml.lzma
+xcsp/basics/Filters-dct_2_3.xml.lzma
+xcsp/basics/FlexibleJobshop-easy01.xml.lzma
+xcsp/basics/Furniture.xml.lzma
+xcsp/basics/GolombRuler-09-a3.xml.lzma
+xcsp/basics/GracefulGraph-K02-P04.xml.lzma
+xcsp/basics/GraphColoring-3-fullins-4.xml.lzma
+xcsp/basics/GraphColoring-qwhdec-o5-h10-1.xml.lzma
+xcsp/basics/Hanoi-05.xml.lzma
+xcsp/basics/Kakuro-easy-000-ext.xml.lzma
+xcsp/basics/Kakuro-easy-000-sumdiff.xml.lzma
+xcsp/basics/Knapsack-30-100-00.xml.lzma
+xcsp/basics/KnightTour-06-ext03.xml.lzma
+xcsp/basics/KnightTour-06-int.xml.lzma
+xcsp/basics/Langford-3-10.xml.lzma
+xcsp/basics/LangfordBin-08.xml.lzma
+xcsp/basics/LowAutocorrelation-015.xml.lzma
+xcsp/basics/MSPSP-easy_01.xml.lzma
+xcsp/basics/MSPSP-hard_01.xml.lzma
+xcsp/basics/MagicSequence-008-ca.xml.lzma
+xcsp/basics/MagicSequence-008-co.xml.lzma
+xcsp/basics/MagicSquare-4-table.xml.lzma
+xcsp/basics/MagicSquare-6-mdd.xml.lzma
+xcsp/basics/MagicSquare-6-sum.xml.lzma
+xcsp/basics/MagicSquare-9-f10-01.xml.lzma
+xcsp/basics/Mario-easy-4.xml.lzma
+xcsp/basics/MarketSplit-01.xml.lzma
+xcsp/basics/MultiKnapsack-1-01.xml.lzma
+xcsp/basics/MultiKnapsack-1-0_X2.xml.lzma
+xcsp/basics/NFC-12_2_10.xml.lzma
+xcsp/basics/Nonogram-001-regular.xml.lzma
+xcsp/basics/Nonogram-001-table.xml.lzma
+xcsp/basics/NurseRostering-00_c18.xml.lzma
+xcsp/basics/Opd-07-007-003.xml.lzma
+xcsp/basics/Ortholatin-005.xml.lzma
+xcsp/basics/Pb-gr-05.xml.lzma
+xcsp/basics/Pb-robin08.xml.lzma
+xcsp/basics/PeacableArmies-m2-05_c18.xml.lzma
+xcsp/basics/PizzaVoucher-10a_c18.xml.lzma
+xcsp/basics/Primes-15-20-2-1.xml.lzma
+xcsp/basics/PrizeCollecting-15-3-5-0.xml.lzma
+xcsp/basics/QuadraticAssignment-bur26a.xml.lzma
+xcsp/basics/QuadraticAssignment-qap.xml.lzma
+xcsp/basics/QuadraticAssignment-qap_c18.xml.lzma
+xcsp/basics/QuasiGroup-3-04.xml.lzma
+xcsp/basics/QueenAttacking-06.xml.lzma
+xcsp/basics/Queens-0008-m1.xml.lzma
+xcsp/basics/RadarSurveillance-8-24-3-2-00.xml.lzma
+xcsp/basics/Ramsey-12.xml.lzma
+xcsp/basics/Rcpsp-j30-01-01_c18.xml.lzma
+xcsp/basics/Rlfap-graph-04-opt_c18.xml.lzma
+xcsp/basics/RoomMate-sr0050-int.xml.lzma
+xcsp/basics/Sat-flat200-00-clause.xml.lzma
+xcsp/basics/SocialGolfers-4-3-4-cp.xml.lzma
+xcsp/basics/Sonet-s2ring02.xml.lzma
+xcsp/basics/SportsScheduling-08.xml.lzma
+xcsp/basics/SteelMillSlab-m1-simple_c18.xml.lzma
+xcsp/basics/SteelMillSlab-m2s-mini-simple_c18.xml.lzma
+xcsp/basics/Steiner3-08.xml.lzma
+xcsp/basics/StillLife-03-06.xml.lzma
+xcsp/basics/StillLife-wastage-03.xml.lzma
+xcsp/basics/StripPacking-C1P1.xml.lzma
+xcsp/basics/Subisomorphism-A-10.xml.lzma
+xcsp/basics/Sudoku-s01a-alldiff.xml.lzma
+xcsp/basics/SumColoring-myciel4_c18.xml.lzma
+xcsp/basics/Taillard-js-015-15-0.xml.lzma
+xcsp/basics/Taillard-os-04-04-0.xml.lzma
+xcsp/basics/Tal-01_c18.xml.lzma
+xcsp/basics/TeamAssignment-data1_4_6.xml.lzma
+xcsp/basics/TemplateDesign-m1s-1_c18.xml.lzma
+xcsp/basics/TemplateDesign-m2-1_c18.xml.lzma
+xcsp/basics/TestSchedulingM18-t30m10r3-15.xml.lzma
+xcsp/basics/Tpp-3-3-20-1.xml.lzma
+xcsp/basics/TravelingTournament-a3-galaxy04_c18.xml.lzma
+xcsp/basics/TravellingSalesman-13-13-0.xml.lzma
+xcsp/basics/Vrp-A-n32-k5.xml.lzma
+xcsp/basics/Vrp-P-n16-k8.xml.lzma
+xcsp/basics/Warehouse-opl.xml.lzma
+xcsp/basics/Zebra.xml.lzma
+flatzinc/basics/all_different+test_globals.fzn
+flatzinc/basics/basic+test_basic.fzn
+flatzinc/basics/basic2+test_basic2+large.fzn
+flatzinc/basics/bool_indomain_max+input_order.fzn
+flatzinc/basics/circuit+test_globals.fzn
+flatzinc/basics/cumulative+test_globals.fzn
+xcsp/basics/driverlogw-09.xml.lzma
+flatzinc/basics/int_indomain_median+anti_first_fail.fzn
+xcsp/basics/qcp-15-120-00_X2.xml.lzma
+xcsp/basics/qwh-o30-h374-01.xml.lzma
+flatzinc/basics/seq_search_1.fzn
+xcsp/basics/testExtension2.xml.lzma
+xcsp/basics/testObjective1.xml.lzma
+xcsp/basics/testPrimitive.xml.lzma
+flatzinc/basics/test_set_lt_2.fzn
diff --git a/pom.xml b/pom.xml
index 140ba4bf54..8554e1e7d3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -58,6 +58,7 @@
+ codegen
solver
parsers
examples
diff --git a/solver/pom.xml b/solver/pom.xml
index e3b678fd11..abe156e867 100644
--- a/solver/pom.xml
+++ b/solver/pom.xml
@@ -36,6 +36,14 @@
+
+
+ org.choco-solver
+ choco-codegen
+ ${project.version}
+ provided
+ true
+
dk.brics.automaton
automaton
@@ -61,6 +69,19 @@
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.choco-solver
+ choco-codegen
+ ${project.version}
+
+
+
+
maven-assembly-plugin
${maven.assembly.plugin.version}
diff --git a/solver/src/main/java/module-info.java b/solver/src/main/java/module-info.java
index 0c697c0c8f..4f378291c0 100644
--- a/solver/src/main/java/module-info.java
+++ b/solver/src/main/java/module-info.java
@@ -92,6 +92,7 @@
exports org.chocosolver.solver.search.strategy.selectors.values.graph.node;
exports org.chocosolver.solver.search.strategy.selectors.values.graph.priority;
exports org.chocosolver.solver.search.strategy.strategy;
+ exports org.chocosolver.solver.spec;
exports org.chocosolver.solver.variables;
exports org.chocosolver.solver.variables.delta;
//exports org.chocosolver.solver.variables.delta.monitor;
diff --git a/solver/src/main/java/org/chocosolver/solver/Model.java b/solver/src/main/java/org/chocosolver/solver/Model.java
index 4e48e165cf..ebb801a392 100755
--- a/solver/src/main/java/org/chocosolver/solver/Model.java
+++ b/solver/src/main/java/org/chocosolver/solver/Model.java
@@ -19,6 +19,8 @@
import org.chocosolver.solver.objective.IObjectiveManager;
import org.chocosolver.solver.objective.ObjectiveFactory;
import org.chocosolver.solver.propagation.PropagationEngine;
+import org.chocosolver.solver.spec.Recorder;
+import org.chocosolver.solver.spec.RecordingModel;
import org.chocosolver.solver.variables.*;
import org.chocosolver.util.tools.ArrayUtils;
import org.chocosolver.util.tools.VariableUtils;
@@ -256,6 +258,95 @@ public Model() {
this("Model-" + nextModelNum());
}
+ /**
+ * Creates a model, as {@link #Model(String, Settings)}. Such a model cannot be duplicated: see
+ * {@link #record(String, Settings)}.
+ *
+ * @param name name of the model
+ * @param settings settings of the model
+ * @return a new model
+ */
+ public static Model create(String name, Settings settings) {
+ return new Model(name, settings);
+ }
+
+ /**
+ * Creates a model, as {@link #Model(String)}. Such a model cannot be duplicated: see {@link #record(String)}.
+ *
+ * @param name name of the model
+ * @return a new model
+ */
+ public static Model create(String name) {
+ return new Model(name);
+ }
+
+ /**
+ * Creates a model, as {@link #Model(Settings)}. Such a model cannot be duplicated: see {@link #record(Settings)}.
+ *
+ * @param settings settings of the model
+ * @return a new model
+ */
+ public static Model create(Settings settings) {
+ return new Model(settings);
+ }
+
+ /**
+ * Creates a model, as {@link #Model()}. Such a model cannot be duplicated: see {@link #record()}.
+ *
+ * @return a new model
+ */
+ public static Model create() {
+ return new Model();
+ }
+
+ /**
+ * Creates a model whose construction is recorded, so that it can be duplicated: build it as usual, then call
+ * {@link #duplicate()} to get independent copies, or use {@link ParallelPortfolio#of(Model, int)} to solve it with
+ * several threads (it is then the first worker). The copies themselves are not recorded, hence cannot be
+ * duplicated. Only the construction of the model is recorded, not the configuration of its solver (search,
+ * limits...), and a model cannot be duplicated once its resolution has started.
+ *
+ * A custom constraint must be built with {@link #custom(String, Variable[], java.util.function.Function)} (or its
+ * variant with data), not directly from propagators.
+ *
+ * @param name name of the model
+ * @param settings settings of the model
+ * @return a model which can be duplicated
+ * @see org.chocosolver.solver.spec.RecordingModel
+ */
+ public static Model record(String name, Settings settings) {
+ return new RecordingModel(name, settings);
+ }
+
+ /**
+ * Creates a model whose construction is recorded, see {@link #record(String, Settings)}.
+ *
+ * @param name name of the model
+ * @return a model which can be duplicated
+ */
+ public static Model record(String name) {
+ return new RecordingModel(name);
+ }
+
+ /**
+ * Creates a model whose construction is recorded, see {@link #record(String, Settings)}.
+ *
+ * @param settings settings of the model
+ * @return a model which can be duplicated
+ */
+ public static Model record(Settings settings) {
+ return new RecordingModel(settings);
+ }
+
+ /**
+ * Creates a model whose construction is recorded, see {@link #record(String, Settings)}.
+ *
+ * @return a model which can be duplicated
+ */
+ public static Model record() {
+ return new RecordingModel();
+ }
+
/**
* For autonumbering anonymous models.
*/
@@ -968,6 +1059,7 @@ private void _post(boolean permanent, Constraint... cs) throws SolverException {
}
// specific behavior for dynamic addition and/or reified constraints
for (Constraint c : cs) {
+ assert ownsVariablesOf(c) : "Constraint " + c.getName() + " involves variables of another model";
for (Propagator> p : c.getPropagators()) {
if (p.isPassive()) {
throw new SolverException("Try to add a constraint with a passive propagator");
@@ -983,6 +1075,21 @@ private void _post(boolean permanent, Constraint... cs) throws SolverException {
}
}
+ /**
+ * @return true if all the variables of c belong to this model (e.g., a constraint built by a
+ * lambda must not capture the variables of another model)
+ */
+ private boolean ownsVariablesOf(Constraint c) {
+ for (Propagator> p : c.getPropagators()) {
+ for (Variable v : p.getVars()) {
+ if (v.getModel() != this) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
/**
* Posts constraints cs temporary, that is, they will be unposted upon backtrack.
*
@@ -1137,6 +1244,34 @@ public void displayPropagatorOccurrences() {
//////////////////////////////////////////// RELATED TO MODELING FACTORIES /////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ /**
+ * Message of the exception thrown when duplicating a model which is not recorded.
+ */
+ static final String NOT_RECORDED = "This model cannot be duplicated: create it with Model.record(...)";
+
+ /**
+ * Internal use only.
+ *
+ * @return the recorder of this model, or null if this model is not a
+ * {@link org.chocosolver.solver.spec.RecordingModel}
+ */
+ public Recorder getRecorder() {
+ return null;
+ }
+
+ /**
+ * Creates an independent copy of this model: same variables, same constraints, same objective, in the same
+ * order, hence the same resolution with a deterministic search. The copy can be solved in another thread.
+ * The copy itself cannot be duplicated.
+ *
+ * @return a copy of this model
+ * @throws SolverException if this model was not created with {@link #record(String)}, or if its resolution
+ * has started, or if one of its constraints was not built by a factory method
+ */
+ public Model duplicate() {
+ throw new SolverException(NOT_RECORDED);
+ }
+
@Override
public Model ref() {
return this;
diff --git a/solver/src/main/java/org/chocosolver/solver/ParallelPortfolio.java b/solver/src/main/java/org/chocosolver/solver/ParallelPortfolio.java
index 956fbd551a..3f07945ee5 100644
--- a/solver/src/main/java/org/chocosolver/solver/ParallelPortfolio.java
+++ b/solver/src/main/java/org/chocosolver/solver/ParallelPortfolio.java
@@ -18,21 +18,31 @@
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.spec.ModelSpec;
+import org.chocosolver.solver.spec.Recorder;
+import org.chocosolver.solver.spec.Resolver;
+import org.chocosolver.solver.spec.SpecSolution;
+import org.chocosolver.solver.spec.Variant;
import org.chocosolver.solver.variables.IntVar;
import org.chocosolver.solver.variables.Variable;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.IdentityHashMap;
import java.util.List;
+import java.util.Map;
import java.util.Spliterator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
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.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@@ -140,6 +150,17 @@ public class ParallelPortfolio {
*/
private Model finder;
+ /**
+ * For a portfolio built from a {@link ModelSpec}: the resolver of each model, which maps the identifiers of the
+ * spec to its objects.
+ */
+ private final Map resolvers = new IdentityHashMap<>();
+
+ /**
+ * For a portfolio built from a recorded model: that model, in whose terms the solutions are expressed.
+ */
+ private Model origin;
+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////// CONSTRUCTOR //////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -169,10 +190,175 @@ public ParallelPortfolio() {
this(true);
}
+ /**
+ * Creates a portfolio of n workers: model itself (the first worker) and n -1 copies of it,
+ * built concurrently. The search heuristics of the copies are diversified, as well as the one of model
+ * unless a search strategy was declared on it. {@link #getBestSolution()} expresses the solutions found with the
+ * variables of model .
+ *
+ * Once its resolution has started, model cannot be duplicated anymore: duplicate it beforehand if needed.
+ * Note that a worker may not even start if another one completes the search first.
+ *
{@code
+ * Model model = Model.record("pb");
+ * // ... variables and constraints ...
+ * ParallelPortfolio portfolio = ParallelPortfolio.of(model, 4);
+ * if (portfolio.solve()) {
+ * Solution s = portfolio.getBestSolution();
+ * }
+ * }
+ *
+ * @param model a model created with {@link Model#record(String)}
+ * @param n number of workers, model included
+ * @return a portfolio of n workers
+ * @throws SolverException if model cannot be duplicated
+ */
+ public static ParallelPortfolio of(Model model, int n) {
+ return of(model, Collections.nCopies(Math.max(0, n - 1), Variant.IDENTITY));
+ }
+
+ /**
+ * Creates a portfolio whose workers are model itself (the first worker) and one copy of it per variant,
+ * built concurrently, see {@link #of(Model, int)} and {@link #of(ModelSpec, List)}.
+ *
+ * @param model a model created with {@link Model#record(String)}
+ * @param variants one variant per copy
+ * @return a portfolio of variants.size() +1 workers
+ * @throws SolverException if model cannot be duplicated
+ */
+ public static ParallelPortfolio of(Model model, List variants) {
+ Recorder recorder = model.getRecorder();
+ if (recorder == null) {
+ throw new SolverException(Model.NOT_RECORDED);
+ }
+ List specs = specs(recorder.snapshot(), variants);
+ ParallelPortfolio portfolio = new ParallelPortfolio();
+ portfolio.origin = model;
+ portfolio.addModel(model, model.getSolver().getSearch() != null, true);
+ portfolio.resolvers.put(model, recorder);
+ portfolio.add(specs, instantiate(specs), 2);
+ return portfolio;
+ }
+
+ /**
+ * Creates a portfolio of n identical models built from spec , whose search heuristics are then
+ * diversified by the portfolio (see {@link #addModel(Model)}). This is equivalent to adding n models built
+ * the same way, but the problem is described once and the models are built concurrently.
+ *
+ * @param spec the spec of the problem
+ * @param n number of models
+ * @return a portfolio of n models
+ */
+ public static ParallelPortfolio of(ModelSpec spec, int n) {
+ return of(spec, Collections.nCopies(n, Variant.IDENTITY));
+ }
+
+ /**
+ * Creates a portfolio with one model per variant of spec , built concurrently.
+ * A model whose variant declares a search ({@link ModelSpec#search()}) keeps it; the search heuristics of the
+ * other models are diversified by the portfolio.
+ *
+ * The solutions found can be read with the identifiers of the spec: see {@link #getBestSpecSolution()} and
+ * {@link #resolverOf(Model)}.
+ *
+ * @param spec the spec of the problem
+ * @param variants one variant per model (see {@link Variant#IDENTITY})
+ * @return a portfolio with one model per variant
+ */
+ public static ParallelPortfolio of(ModelSpec spec, List variants) {
+ List specs = specs(spec, variants);
+ ParallelPortfolio portfolio = new ParallelPortfolio();
+ portfolio.add(specs, instantiate(specs), 1);
+ return portfolio;
+ }
+
+ private static List specs(ModelSpec spec, List variants) {
+ return variants.stream().map(spec::with).collect(Collectors.toList());
+ }
+
+ /**
+ * Add the instances of specs , numbered from rank .
+ */
+ private void add(List specs, List instances, int rank) {
+ for (int i = 0; i < instances.size(); i++) {
+ Model m = instances.get(i).model();
+ m.setName(m.getName() + "_" + (rank + i));
+ addModel(m, specs.get(i).search() != null, true);
+ resolvers.put(m, instances.get(i));
+ }
+ }
+
+ /**
+ * @return the instances of specs , built concurrently
+ */
+ private static List instantiate(List specs) {
+ if (specs.isEmpty()) {
+ return List.of();
+ }
+ ExecutorService pool = Executors.newFixedThreadPool(
+ Math.min(specs.size(), Runtime.getRuntime().availableProcessors()));
+ try {
+ List> futures = new ArrayList<>(specs.size());
+ for (ModelSpec s : specs) {
+ futures.add(pool.submit(() -> s.instantiate()));
+ }
+ List instances = new ArrayList<>(specs.size());
+ for (Future f : futures) {
+ instances.add(f.get());
+ }
+ return instances;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new SolverException("Interrupted while building the models of the portfolio");
+ } catch (ExecutionException e) {
+ throw e.getCause() instanceof RuntimeException re ? re : new SolverException(e.getCause().getMessage());
+ } finally {
+ pool.shutdownNow();
+ }
+ }
+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////// API //////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ /**
+ * @param model a model of this portfolio
+ * @return the resolver of model if this portfolio was built from a {@link ModelSpec}, null otherwise
+ */
+ public Resolver resolverOf(Model model) {
+ return resolvers.get(model);
+ }
+
+ /**
+ * To be called once {@link #solve()} returned true .
+ *
+ * @return the (best) solution found, expressed with the variables of the recorded model for a portfolio built
+ * with {@link #of(Model, int)}, with the variables of the model which found it otherwise
+ * @throws SolverException if no solution was found
+ */
+ public Solution getBestSolution() {
+ if (finder == null) {
+ throw new SolverException("No solution found");
+ }
+ return origin == null || finder == origin
+ ? new Solution(finder).record()
+ : getBestSpecSolution().toSolution(origin.getRecorder());
+ }
+
+ /**
+ * To be called once {@link #solve()} returned true , on a portfolio built from a {@link ModelSpec} (or a
+ * recorded model).
+ *
+ * @return the (best) solution found, with the identifiers of the spec: it can be read from any model of the spec
+ * @throws SolverException if this portfolio was not built from a spec, or if no solution was found
+ */
+ public SpecSolution getBestSpecSolution() {
+ Resolver r = finder == null ? null : resolvers.get(finder);
+ if (r == null) {
+ throw new SolverException(finder == null ? "No solution found" : "The portfolio was not built from a ModelSpec");
+ }
+ return SpecSolution.record(r);
+ }
+
/**
* 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
diff --git a/solver/src/main/java/org/chocosolver/solver/Solver.java b/solver/src/main/java/org/chocosolver/solver/Solver.java
index f317203442..48c3330b0d 100644
--- a/solver/src/main/java/org/chocosolver/solver/Solver.java
+++ b/solver/src/main/java/org/chocosolver/solver/Solver.java
@@ -1376,6 +1376,9 @@ public void setSearch(AbstractStrategy... strategies) {
strategies = Arrays.stream(strategies).filter(Objects::nonNull)
.flatMap(s -> (s instanceof StrategiesSequencer) ? Arrays.stream(((StrategiesSequencer>) s).getStrategies()) : Stream.of(s))
.toArray(AbstractStrategy[]::new);
+ assert Arrays.stream(strategies).allMatch(s -> s.getVariables() == null
+ || Arrays.stream(s.getVariables()).allMatch(v -> v == null || v.getModel() == mModel))
+ : "A search strategy involves variables of another model";
if (strategies.length == 0) {
M.removeStrategy();
} else if (strategies.length == 1) {
diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/Constraint.java b/solver/src/main/java/org/chocosolver/solver/constraints/Constraint.java
index 307b742a7b..d8400e9842 100755
--- a/solver/src/main/java/org/chocosolver/solver/constraints/Constraint.java
+++ b/solver/src/main/java/org/chocosolver/solver/constraints/Constraint.java
@@ -10,6 +10,8 @@
import org.chocosolver.solver.constraints.reification.Opposite;
import org.chocosolver.solver.exception.SolverException;
import org.chocosolver.solver.search.SearchState;
+import org.chocosolver.solver.spec.Recorder;
+import org.chocosolver.solver.spec.Step;
import org.chocosolver.solver.variables.BoolVar;
import org.chocosolver.util.ESat;
@@ -185,6 +187,15 @@ public final boolean isReified() {
* @param bool the variable to reify with
*/
public void reifyWith(BoolVar bool) {
+ Recorder r = recorder();
+ if (r != null) {
+ r.link(Step.Link.Kind.REIFY_WITH, this, bool, () -> doReifyWith(bool));
+ } else {
+ doReifyWith(bool);
+ }
+ }
+
+ private void doReifyWith(BoolVar bool) {
if (boolReif != null) {
if (opposite == null) {
throw new SolverException("try to reify an implied constraint");
@@ -219,6 +230,11 @@ public void reifyWith(BoolVar bool) {
* @return the boolean reifying the constraint
*/
public final BoolVar reify() {
+ Recorder r = recorder();
+ return r != null ? r.reify(this, this::doReify) : doReify();
+ }
+
+ private BoolVar doReify() {
if (boolReif == null) {
Model model = propagators[0].getModel();
reifyWith(model.boolVar(model.generateName("REIF_")));
@@ -243,7 +259,12 @@ public final BoolVar reify() {
* @param r a boolean variable
*/
public final void implies(BoolVar r) {
- this.reify().imp(r).post();
+ Recorder rec = recorder();
+ if (rec != null) {
+ rec.link(Step.Link.Kind.IMPLIES, this, r, () -> this.reify().imp(r).post());
+ } else {
+ this.reify().imp(r).post();
+ }
}
/**
@@ -262,6 +283,15 @@ public final void implies(BoolVar r) {
* @param r a boolean variable
*/
public final void impliedBy(BoolVar r) {
+ Recorder rec = recorder();
+ if (rec != null) {
+ rec.link(Step.Link.Kind.IMPLIED_BY, this, r, () -> doImpliedBy(r));
+ } else {
+ doImpliedBy(r);
+ }
+ }
+
+ private void doImpliedBy(BoolVar r) {
if (boolReif == null) {
boolReif = r;
if (boolReif.isInstantiatedTo(1)
@@ -275,6 +305,15 @@ public final void impliedBy(BoolVar r) {
}
}
+ /**
+ * @return the recorder of the model of this constraint if the current operation must be journalized (the model
+ * is recorded and the operation is not triggered by another journalized one), null otherwise
+ */
+ private Recorder recorder() {
+ Recorder r = propagators.length > 0 ? propagators[0].getModel().getRecorder() : null;
+ return r == null || r.busy() ? null : r;
+ }
+
/**
* Posts the constraint to its model so that the constraint must be satisfied.
* This should not be reified.
diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/IConstraintFactory.java b/solver/src/main/java/org/chocosolver/solver/constraints/IConstraintFactory.java
index 87c9da500e..b589032fb5 100644
--- a/solver/src/main/java/org/chocosolver/solver/constraints/IConstraintFactory.java
+++ b/solver/src/main/java/org/chocosolver/solver/constraints/IConstraintFactory.java
@@ -9,6 +9,7 @@
import org.chocosolver.solver.constraints.reification.PropConditional;
import org.chocosolver.solver.variables.Variable;
+import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -45,4 +46,45 @@ default Constraint conditional(
new PropConditional(scope, checker, provider)
);
}
+
+ /**
+ * Create a custom constraint, made of the propagator built by {@code propagator} on {@code vars}.
+ *
+ * Prefer this factory to {@code new Constraint(name, propagator)}: a model created with
+ * {@link org.chocosolver.solver.Model#record()} can then be duplicated, provided that {@code propagator} captures
+ * nothing (e.g., a constructor reference such as {@code PropFoo::new}), since it is shared by all the copies.
+ *
+ * @param name name of the constraint
+ * @param vars variables of the constraint
+ * @param propagator builds the propagator from the variables; must capture nothing
+ * @param type of the variables
+ * @return a custom constraint
+ */
+ default Constraint custom(String name, V[] vars, Function> propagator) {
+ return new Constraint(name, propagator.apply(vars));
+ }
+
+ /**
+ * Create a custom constraint, made of the propagator built by {@code propagator} on {@code vars} and
+ * {@code data}, e.g. {@code custom("atMostK", x, 3, PropAtMostK::new)}.
+ *
+ * Prefer this factory to {@code new Constraint(name, propagator)}: a model created with
+ * {@link org.chocosolver.solver.Model#record()} can then be duplicated, provided that {@code propagator} captures
+ * nothing (e.g., a constructor reference such as {@code PropFoo::new}), since it is shared by all the copies.
+ * What the propagator needs, besides the variables, is passed in {@code data}, which is journalized as any
+ * constant argument: immutable values and arrays of them, tuples, or a {@code record} of such values (several
+ * parameters). An array of variables is mapped to the variables of each copy.
+ *
+ * @param name name of the constraint
+ * @param vars variables of the constraint
+ * @param data the other parameters of the propagator
+ * @param propagator builds the propagator from the variables and the data; must capture nothing
+ * @param type of the variables
+ * @param type of the data
+ * @return a custom constraint
+ */
+ default Constraint custom(String name, V[] vars, D data,
+ BiFunction> propagator) {
+ return new Constraint(name, propagator.apply(vars, data));
+ }
}
diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/extension/Tuples.java b/solver/src/main/java/org/chocosolver/solver/constraints/extension/Tuples.java
index 617968f8e2..9b03f82c83 100644
--- a/solver/src/main/java/org/chocosolver/solver/constraints/extension/Tuples.java
+++ b/solver/src/main/java/org/chocosolver/solver/constraints/extension/Tuples.java
@@ -34,6 +34,10 @@ public class Tuples {
private final boolean allowStar;
private final int star;
private SoftReference cachedMatrix;
+ /**
+ * Once frozen, the tuples cannot be modified anymore and can be safely shared among models.
+ */
+ private volatile boolean frozen;
//***********************************************************************************
// CONSTRUCTOR
@@ -155,6 +159,7 @@ public ESat check(IntVar... vars) {
* @throws org.chocosolver.solver.exception.SolverException if the size of the tuple added does not correspond to a the previous ones (if any).
*/
public void add(int... tuple) {
+ checkNotFrozen();
if (tuples.isEmpty()) {
arity = tuple.length;
ranges = new int[2 * arity];
@@ -244,21 +249,52 @@ public String toString() {
* @return an array of tuples, each tuple is an int array
*/
public int[][] toMatrix() {
- if (cachedMatrix == null) {
+ // the cache may have been cleared by the GC, or be computed concurrently when the tuples are shared
+ SoftReference ref = cachedMatrix;
+ int[][] matrix = ref == null ? null : ref.get();
+ if (matrix == null) {
int i = 0;
- int[][] matrix = new int[tuples.size()][];
+ matrix = new int[tuples.size()][];
for (int[] tuple : tuples) {
matrix[i++] = tuple.clone();
}
cachedMatrix = new SoftReference<>(matrix);
}
- return cachedMatrix.get();
+ return matrix;
}
public void sort() {
+ checkNotFrozen();
tuples.sort(new TupleComparator());
}
+ /**
+ * Freeze this set of tuples: any further modification ({@link #add(int...)}, {@link #sort()}) will throw a
+ * {@link SolverException}.
+ * A frozen set of tuples can be safely shared among models solved concurrently.
+ * Freezing is irreversible and idempotent.
+ *
+ * @return this
+ */
+ public Tuples freeze() {
+ this.frozen = true;
+ return this;
+ }
+
+ /**
+ * @return true if this set of tuples is frozen, false otherwise
+ * @see #freeze()
+ */
+ public boolean isFrozen() {
+ return frozen;
+ }
+
+ private void checkNotFrozen() {
+ if (frozen) {
+ throw new SolverException("Cannot modify a frozen set of tuples");
+ }
+ }
+
private static class TupleComparator implements Comparator {
@Override
diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/extension/hybrid/HybridTuples.java b/solver/src/main/java/org/chocosolver/solver/constraints/extension/hybrid/HybridTuples.java
index 321f143387..4c067bb716 100644
--- a/solver/src/main/java/org/chocosolver/solver/constraints/extension/hybrid/HybridTuples.java
+++ b/solver/src/main/java/org/chocosolver/solver/constraints/extension/hybrid/HybridTuples.java
@@ -50,6 +50,10 @@ public class HybridTuples {
* For sanity check only
*/
private int arity;
+ /**
+ * Once frozen, the tuples cannot be modified anymore and can be safely shared among models.
+ */
+ private volatile boolean frozen;
/**
* Create an empty structure that stores hybrid tuples
@@ -68,6 +72,9 @@ public HybridTuples() {
* @throws SolverException if the tuple does not the match the arity of previously declared ones.
*/
public void add(ISupportable... tuple) {
+ if (frozen) {
+ throw new SolverException("Cannot modify frozen hybrid tuples");
+ }
if (hybridTuples.size() == 0) {
arity = tuple.length;
} else if (arity != tuple.length) {
@@ -133,6 +140,26 @@ public void add(ISupportable[]... tuples) {
}
}
+ /**
+ * Freeze these hybrid tuples: any further modification will throw a {@link SolverException}.
+ * Frozen hybrid tuples can be safely shared among models solved concurrently.
+ * Freezing is irreversible and idempotent.
+ *
+ * @return this
+ */
+ public HybridTuples freeze() {
+ this.frozen = true;
+ return this;
+ }
+
+ /**
+ * @return true if these hybrid tuples are frozen
+ * @see #freeze()
+ */
+ public boolean isFrozen() {
+ return frozen;
+ }
+
/**
* @return the current tuples as an array of expressions.
*/
diff --git a/solver/src/main/java/org/chocosolver/solver/constraints/nary/automata/FA/FiniteAutomaton.java b/solver/src/main/java/org/chocosolver/solver/constraints/nary/automata/FA/FiniteAutomaton.java
index 8c79805933..84d14c5458 100644
--- a/solver/src/main/java/org/chocosolver/solver/constraints/nary/automata/FA/FiniteAutomaton.java
+++ b/solver/src/main/java/org/chocosolver/solver/constraints/nary/automata/FA/FiniteAutomaton.java
@@ -37,7 +37,7 @@ public class FiniteAutomaton implements IAutomaton {
private ArrayList states;
private TIntHashSet alphabet;
private int nbStates;
- private final HashSet nexts = new HashSet<>();
+ private HashSet nexts = new HashSet<>();
private int min = Character.MIN_VALUE;
private int max = Character.MAX_VALUE;
@@ -595,6 +595,8 @@ public ArrayList _removeSymbolFromAutomaton(int alpha) {
public FiniteAutomaton clone() throws CloneNotSupportedException {
FiniteAutomaton auto = (FiniteAutomaton) super.clone();
+ // working buffer of delta(int, int, TIntHashSet): must not be shared with the clone
+ auto.nexts = new HashSet<>();
auto.representedBy = new Automaton();
auto.states = new ArrayList<>();
auto.stateToIndex = new TObjectIntHashMap<>();
@@ -613,6 +615,7 @@ public FiniteAutomaton clone() throws CloneNotSupportedException {
for (int[] t : transitions) {
auto.addTransition(t[0], t[1], t[2]);
}
+ auto.representedBy.setDeterministic(this.representedBy.isDeterministic());
return auto;
}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/AbstractRecordingModel.java b/solver/src/main/java/org/chocosolver/solver/spec/AbstractRecordingModel.java
new file mode 100644
index 0000000000..941d51f263
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/AbstractRecordingModel.java
@@ -0,0 +1,211 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.IModel;
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.Settings;
+import org.chocosolver.solver.SettingsBuilder;
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.constraints.nary.cnf.SatConstraint;
+import org.chocosolver.solver.constraints.real.IbexHandler;
+import org.chocosolver.solver.exception.SolverException;
+import org.chocosolver.solver.variables.Group;
+import org.chocosolver.solver.variables.Variable;
+
+import java.util.Set;
+
+/**
+ * The hand-written part of {@link RecordingModel}: journalization of the operations of {@link Model} which are
+ * not factory methods.
+ *
+ * {@link RecordingModel}, which journalizes the factory methods, is generated at compile time.
+ *
+ * @author Charles Prud'homme
+ */
+@GenerateRecordingModel(name = "RecordingModel", factories = IModel.class,
+ excluded = {"checkIntDomainRange", "checkRealDomainRange", "toBoolVar", "getDomainUnion"})
+public abstract class AbstractRecordingModel extends Model {
+
+ /**
+ * Hooks managed internally by the solver: never journalized.
+ */
+ private static final Set INTERNAL_HOOKS = Set.of("cinstances",
+ MINISAT_HOOK_NAME, IBEX_HOOK_NAME, TASK_SET_HOOK_NAME);
+
+ private final Recorder recorder;
+
+ protected AbstractRecordingModel(String name, Settings settings) {
+ super(name, settings);
+ this.recorder = new Recorder(this, new Header(name, settings, getSeed()));
+ }
+
+ protected AbstractRecordingModel(String name) {
+ this(name, SettingsBuilder.init().build());
+ }
+
+ protected AbstractRecordingModel(Settings settings) {
+ this("RecordingModel", settings);
+ }
+
+ protected AbstractRecordingModel() {
+ this("RecordingModel");
+ }
+
+ /**
+ * @return the recorder of this model
+ */
+ @Override
+ public final Recorder getRecorder() {
+ return recorder;
+ }
+
+ /**
+ * @return the spec of the model built so far
+ * @throws SpecException if some operations could not be journalized, or if the resolution has started
+ * @see Recorder#snapshot()
+ */
+ public final ModelSpec snapshot() {
+ return recorder.snapshot();
+ }
+
+ @Override
+ public final Model duplicate() {
+ return snapshot().instantiate().model();
+ }
+
+ /**
+ * @return true if the operation must be journalized
+ */
+ protected final boolean topLevel() {
+ // null while the constructor of Model runs
+ return recorder != null && !recorder.busy();
+ }
+
+ @Override
+ public void post(Constraint... cs) throws SolverException {
+ if (topLevel()) {
+ recorder.post(cs, () -> super.post(cs));
+ } else {
+ super.post(cs);
+ }
+ }
+
+ @Override
+ public void unpost(Constraint... constraints) throws SolverException {
+ if (topLevel()) {
+ recorder.unpost(constraints, () -> super.unpost(constraints));
+ } else {
+ super.unpost(constraints);
+ }
+ }
+
+ @Override
+ public void addHook(String hookName, Object hookObject) {
+ if (topLevel() && !INTERNAL_HOOKS.contains(hookName)) {
+ recorder.hook(hookName, hookObject, () -> super.addHook(hookName, hookObject));
+ } else {
+ super.addHook(hookName, hookObject);
+ }
+ }
+
+ @Override
+ public void removeHook(String hookName) {
+ if (topLevel() && !INTERNAL_HOOKS.contains(hookName)) {
+ recorder.hook(hookName, null, () -> super.removeHook(hookName));
+ } else {
+ super.removeHook(hookName);
+ }
+ }
+
+ @Override
+ public void removeAllHooks() {
+ if (topLevel()) {
+ recorder.removeAllHooks(super::removeAllHooks);
+ } else {
+ super.removeAllHooks();
+ }
+ }
+
+ @Override
+ public void setName(String name) {
+ if (topLevel()) {
+ recorder.name(name, () -> super.setName(name));
+ } else {
+ super.setName(name);
+ }
+ }
+
+ @Override
+ public void addGroup(Group g) {
+ if (topLevel()) {
+ recorder.group(g.getName(), g.getVariables(), () -> super.addGroup(g));
+ } else {
+ super.addGroup(g);
+ }
+ }
+
+ @Override
+ public void setPrecision(double p) {
+ if (topLevel()) {
+ recorder.unsupported("setPrecision", "real variables are not supported");
+ }
+ super.setPrecision(p);
+ }
+
+ @Override
+ public void setSeed(long seed) {
+ if (topLevel()) {
+ recorder.seed(seed, () -> super.setSeed(seed));
+ } else {
+ super.setSeed(seed);
+ }
+ }
+
+ @Override
+ public void setObjective(boolean maximize, Variable objective) {
+ if (topLevel()) {
+ recorder.objective(maximize, objective, () -> super.setObjective(maximize, objective));
+ } else {
+ super.setObjective(maximize, objective);
+ }
+ }
+
+ @Override
+ public void clearObjective() {
+ if (topLevel()) {
+ recorder.objective(false, null, super::clearObjective);
+ } else {
+ super.clearObjective();
+ }
+ }
+
+ // The following methods are not factories: what they do internally must not be journalized.
+ // Their effects are replayed by the factories which need them.
+
+ @Override
+ public SatConstraint getMinisat() {
+ // also called by search monitors (nogoods), which are not part of the model: never journalized
+ return topLevel() ? recorder.silently(super::getMinisat) : super.getMinisat();
+ }
+
+ @Override
+ public void removeMinisat() {
+ if (topLevel()) {
+ recorder.unsupported("removeMinisat", "not supported");
+ }
+ super.removeMinisat();
+ }
+
+ @Override
+ public IbexHandler getIbexHandler() {
+ if (topLevel()) {
+ recorder.unsupported("getIbexHandler", "real constraints are not supported");
+ }
+ return super.getIbexHandler();
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/Arg.java b/solver/src/main/java/org/chocosolver/solver/spec/Arg.java
new file mode 100644
index 0000000000..2bbec32b67
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/Arg.java
@@ -0,0 +1,242 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.constraints.nary.cnf.LogOp;
+import org.chocosolver.solver.variables.BoolVar;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.function.IntConsumer;
+
+/**
+ * An argument of a {@link Step.Call}.
+ * Objects of the model (variables, constraints, tasks) are referred to by identifier, never by name.
+ *
+ * Arguments are immutable: public accessors return copies, the package reads the arrays directly.
+ *
+ * @author Charles Prud'homme
+ */
+public sealed interface Arg permits Arg.Ref, Arg.RefArray, Arg.RefMatrix, Arg.RefList, Arg.Const, Arg.Logic,
+ Arg.Literal {
+
+ /**
+ * Apply action on each identifier this argument refers to.
+ */
+ default void forEachRef(IntConsumer action) {
+ }
+
+ /**
+ * A reference to one object of the model.
+ */
+ record Ref(int id) implements Arg {
+ public Ref {
+ if (id < 0) throw new SpecException("Negative identifier: " + id);
+ }
+
+ @Override
+ public void forEachRef(IntConsumer action) {
+ action.accept(id);
+ }
+ }
+
+ /**
+ * An array of objects of the model, e.g. an {@code IntVar[]}.
+ *
+ * @param componentType type of the elements of the array to build, e.g. {@code IntVar.class}
+ * @param ids identifiers of the elements
+ */
+ record RefArray(Class> componentType, int[] ids) implements Arg {
+ public RefArray {
+ Objects.requireNonNull(componentType);
+ ids = ids.clone();
+ }
+
+ @Override
+ public int[] ids() {
+ return ids.clone();
+ }
+
+ int[] rawIds() {
+ return ids;
+ }
+
+ @Override
+ public void forEachRef(IntConsumer action) {
+ for (int id : ids) action.accept(id);
+ }
+
+ @Override
+ public String toString() {
+ return componentType.getSimpleName() + Arrays.toString(ids);
+ }
+ }
+
+ /**
+ * A matrix of objects of the model, e.g. an {@code IntVar[][]}.
+ *
+ * @param componentType type of the elements of the matrix to build, e.g. {@code IntVar.class}
+ * @param ids identifiers of the elements, row by row
+ */
+ record RefMatrix(Class> componentType, int[][] ids) implements Arg {
+ public RefMatrix {
+ Objects.requireNonNull(componentType);
+ ids = (int[][]) Values.capture(ids);
+ }
+
+ @Override
+ public int[][] ids() {
+ return (int[][]) Values.materialize(ids);
+ }
+
+ int[][] rawIds() {
+ return ids;
+ }
+
+ @Override
+ public void forEachRef(IntConsumer action) {
+ for (int[] row : ids) for (int id : row) action.accept(id);
+ }
+
+ @Override
+ public String toString() {
+ return componentType.getSimpleName() + Arrays.deepToString(ids);
+ }
+ }
+
+ /**
+ * A {@link java.util.List} of objects of the model.
+ */
+ record RefList(int[] ids) implements Arg {
+ public RefList {
+ ids = ids.clone();
+ }
+
+ @Override
+ public int[] ids() {
+ return ids.clone();
+ }
+
+ int[] rawIds() {
+ return ids;
+ }
+
+ @Override
+ public void forEachRef(IntConsumer action) {
+ for (int id : ids) action.accept(id);
+ }
+
+ @Override
+ public String toString() {
+ return "List" + Arrays.toString(ids);
+ }
+ }
+
+ /**
+ * A constant, captured according to the sharing policy of {@link Values}.
+ */
+ record Const(Object value) implements Arg {
+ public Const {
+ value = Values.capture(value);
+ }
+
+ /**
+ * @return the value, shared or copied according to the sharing policy of {@link Values}
+ */
+ @Override
+ public Object value() {
+ return Values.materialize(value);
+ }
+
+ /**
+ * @return the captured value, as stored in the spec (not to be modified)
+ */
+ Object rawValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ if (value instanceof int[] a) return Arrays.toString(a);
+ if (value instanceof Object[] a) return Arrays.deepToString(a);
+ return String.valueOf(value);
+ }
+ }
+
+ /**
+ * A logical tree ({@link LogOp}) over boolean variables.
+ *
+ * @param and true for a conjunction, false for a disjunction
+ * @param negated true for the negation of the conjunction (resp. disjunction)
+ * @param children either references to {@link BoolVar} or nested {@link Logic}
+ */
+ record Logic(boolean and, boolean negated, Arg[] children) implements Arg {
+ public Logic {
+ children = children.clone();
+ for (Arg c : children) {
+ if (!(c instanceof Ref) && !(c instanceof Logic)) {
+ throw new SpecException("A logical tree only accepts references and logical trees, not " + c);
+ }
+ }
+ }
+
+ @Override
+ public Arg[] children() {
+ return children.clone();
+ }
+
+ Arg[] rawChildren() {
+ return children;
+ }
+
+ @Override
+ public void forEachRef(IntConsumer action) {
+ for (Arg c : children) c.forEachRef(action);
+ }
+
+ @Override
+ public String toString() {
+ return (negated ? "n" : "") + (and ? "and" : "or") + Arrays.toString(children);
+ }
+ }
+
+ /**
+ * A {@link org.chocosolver.sat.Literalizer}, which binds a variable to a literal of the SAT solver.
+ * Literalizers are mutable (the SAT solver numbers them): a new one is built for each instance.
+ *
+ * @param kind the kind of literalizer
+ * @param var identifier of the variable
+ * @param value the value (for {@link Kind#INT_EQ} and {@link Kind#INT_LE} only)
+ */
+ record Literal(Kind kind, int var, int value) implements Arg {
+
+ /**
+ * The kinds of literalizers: {@code BoolLit}, {@code IntEqLit} and {@code IntLeLit}.
+ */
+ public enum Kind {
+ BOOL, INT_EQ, INT_LE
+ }
+
+ public Literal {
+ Objects.requireNonNull(kind);
+ if (var < 0) throw new SpecException("Negative identifier: " + var);
+ }
+
+ @Override
+ public void forEachRef(IntConsumer action) {
+ action.accept(var);
+ }
+ }
+
+ static Logic and(Arg... children) {
+ return new Logic(true, false, children);
+ }
+
+ static Logic or(Arg... children) {
+ return new Logic(false, false, children);
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/FactoryMethod.java b/solver/src/main/java/org/chocosolver/solver/spec/FactoryMethod.java
new file mode 100644
index 0000000000..ef9f4df4cf
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/FactoryMethod.java
@@ -0,0 +1,123 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.IModel;
+import org.chocosolver.solver.Model;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * A factory method of {@link IModel}, identified by its exact signature (the overload is never ambiguous).
+ * It is replayed on a {@link Model} through a {@link MethodHandle}.
+ *
+ * Instances are immutable and can be shared among threads.
+ *
+ * @author Charles Prud'homme
+ */
+public final class FactoryMethod {
+
+ private static final ConcurrentHashMap CACHE = new ConcurrentHashMap<>();
+
+ private final Method method;
+ private final Class>[] types;
+ /**
+ * (Model, Object[]) -> Object: the arguments are spread, the result boxed (null for void).
+ */
+ private final MethodHandle invoker;
+
+ private FactoryMethod(Method method) {
+ if (Modifier.isStatic(method.getModifiers())
+ || !method.getDeclaringClass().isInterface()
+ || !method.getDeclaringClass().isAssignableFrom(IModel.class)) {
+ throw new SpecException(method + " is not a factory method of IModel");
+ }
+ this.method = method;
+ this.types = method.getParameterTypes();
+ try {
+ // fixed arity: a varargs parameter receives the array built from the spec, as is
+ this.invoker = MethodHandles.publicLookup().unreflect(method).asFixedArity()
+ .asSpreader(Object[].class, types.length)
+ .asType(MethodType.methodType(Object.class, Model.class, Object[].class));
+ } catch (IllegalAccessException e) {
+ throw new SpecException("Cannot access " + method, e);
+ }
+ }
+
+ /**
+ * @param method a (non-static) method declared by one of the factory interfaces of {@link IModel}
+ * @return the corresponding factory method
+ * @throws SpecException if method is not a factory method
+ */
+ public static FactoryMethod of(Method method) {
+ return CACHE.computeIfAbsent(method, FactoryMethod::new);
+ }
+
+ /**
+ * @param name name of the factory method
+ * @param paramTypes exact parameter types, which select the overload
+ * @return the corresponding factory method
+ * @throws SpecException if no such factory method exists
+ */
+ public static FactoryMethod of(String name, Class>... paramTypes) {
+ try {
+ return of(IModel.class.getMethod(name, paramTypes));
+ } catch (NoSuchMethodException e) {
+ throw new SpecException("No factory method " + signature(name, paramTypes), e);
+ }
+ }
+
+ public String name() {
+ return method.getName();
+ }
+
+ public Class>[] parameterTypes() {
+ return types.clone();
+ }
+
+ Class>[] rawParameterTypes() {
+ return types;
+ }
+
+ public int parameterCount() {
+ return types.length;
+ }
+
+ public Class> returnType() {
+ return method.getReturnType();
+ }
+
+ Object invoke(Model target, Object[] args) throws Throwable {
+ return (Object) invoker.invokeExact(target, args);
+ }
+
+ private static String signature(String name, Class>[] types) {
+ return name + Arrays.stream(types).map(Class::getSimpleName).collect(Collectors.joining(", ", "(", ")"));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return this == o || (o instanceof FactoryMethod f && method.equals(f.method));
+ }
+
+ @Override
+ public int hashCode() {
+ return method.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return signature(method.getName(), types);
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/GenerateRecordingModel.java b/solver/src/main/java/org/chocosolver/solver/spec/GenerateRecordingModel.java
new file mode 100644
index 0000000000..a54c7a3e02
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/GenerateRecordingModel.java
@@ -0,0 +1,38 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Asks the annotation processor {@code org.chocosolver.codegen.RecordingModelProcessor} (module choco-codegen) to
+ * generate, at compile time, a subclass of the annotated class which journalizes every factory method.
+ *
+ * @author Charles Prud'homme
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target(ElementType.TYPE)
+@interface GenerateRecordingModel {
+
+ /**
+ * @return the simple name of the generated class, in the package of the annotated class
+ */
+ String name();
+
+ /**
+ * @return the interface whose default methods are the factory methods to journalize
+ */
+ Class> factories();
+
+ /**
+ * @return names of the factory methods without effect on the model, which are not journalized
+ */
+ String[] excluded() default {};
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/Header.java b/solver/src/main/java/org/chocosolver/solver/spec/Header.java
new file mode 100644
index 0000000000..a35ae3119c
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/Header.java
@@ -0,0 +1,56 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Settings;
+import org.chocosolver.solver.SettingsBuilder;
+
+import java.util.Objects;
+
+/**
+ * What must be set on a model before replaying the steps of a {@link ModelSpec}.
+ *
+ * @param name name of the model
+ * @param settings settings of the model; immutable, hence shared by all instances, which may call its functions
+ * ({@code modelChecker}, {@code defaultSearch}, {@code environmentSupplier}...) concurrently: they
+ * must be stateless
+ * @param seed seed of the model; some propagators draw their random numbers from it when created
+ * @author Charles Prud'homme
+ */
+public record Header(String name, Settings settings, long seed) {
+
+ public Header {
+ Objects.requireNonNull(name);
+ Objects.requireNonNull(settings);
+ }
+
+ /**
+ * @return a header with default settings and seed 0
+ */
+ public static Header of(String name) {
+ return of(name, SettingsBuilder.init().build());
+ }
+
+ /**
+ * @return a header with seed 0
+ */
+ public static Header of(String name, Settings settings) {
+ return new Header(name, settings, 0L);
+ }
+
+ public Header withName(String name) {
+ return new Header(name, settings, seed);
+ }
+
+ public Header withSeed(long seed) {
+ return new Header(name, settings, seed);
+ }
+
+ public Header withSettings(Settings settings) {
+ return new Header(name, settings, seed);
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/IdTable.java b/solver/src/main/java/org/chocosolver/solver/spec/IdTable.java
new file mode 100644
index 0000000000..2bd60d332b
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/IdTable.java
@@ -0,0 +1,82 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Model;
+
+import java.util.Arrays;
+import java.util.IdentityHashMap;
+
+/**
+ * The mapping from identifiers to the objects of a model, shared by {@link Recorder} and {@link Replayer}.
+ * The reverse mapping ({@link #idOf(Object)}) is built on demand.
+ *
+ * @author Charles Prud'homme
+ */
+abstract class IdTable implements Resolver {
+
+ private final Model model;
+ private Object[] objects = new Object[64];
+ private int capacity;
+ private IdentityHashMap reverse;
+
+ IdTable(Model model) {
+ this.model = model;
+ }
+
+ /**
+ * Bind id to o .
+ *
+ * @return the object previously bound to id , or null
+ */
+ final Object put(int id, Object o) {
+ if (id >= objects.length) {
+ objects = Arrays.copyOf(objects, Math.max(id + 1, objects.length * 2));
+ }
+ Object prev = objects[id];
+ objects[id] = o;
+ capacity = Math.max(capacity, id + 1);
+ reverse = null;
+ return prev;
+ }
+
+ @Override
+ public final Model model() {
+ return model;
+ }
+
+ @Override
+ public final Object get(int id) {
+ if (!isBound(id)) {
+ throw new SpecException("Unbound identifier " + id);
+ }
+ return objects[id];
+ }
+
+ @Override
+ public final int capacity() {
+ return capacity;
+ }
+
+ @Override
+ public final boolean isBound(int id) {
+ return id >= 0 && id < capacity && objects[id] != null;
+ }
+
+ @Override
+ public int idOf(Object o) {
+ if (reverse == null) {
+ reverse = new IdentityHashMap<>();
+ for (int id = 0; id < capacity; id++) {
+ if (objects[id] != null) {
+ reverse.putIfAbsent(objects[id], id);
+ }
+ }
+ }
+ return reverse.getOrDefault(o, -1);
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/ModelSpec.java b/solver/src/main/java/org/chocosolver/solver/spec/ModelSpec.java
new file mode 100644
index 0000000000..d424bdb96b
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/ModelSpec.java
@@ -0,0 +1,135 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.Settings;
+
+import java.util.BitSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * An immutable description of a model: a {@link Header} and an ordered journal of {@link Step}s.
+ *
+ * Each call to {@link #instantiate()} builds a new, independent {@link Model}. Distinct instances can be built and
+ * solved concurrently, in distinct threads: they only share immutable data (settings, frozen tuples, ...).
+ *
+ * Replaying the same spec twice produces two models with identical structure (same variables, same constraints,
+ * in the same order), hence identical search when the search is deterministic.
+ *
+ * @param header what must be set on a model before replaying the steps
+ * @param steps the steps, replayed in order
+ * @param search the search, applied once the steps are replayed; null for the default search
+ * @author Charles Prud'homme
+ */
+public record ModelSpec(Header header, List steps, SearchDecl search) {
+
+ /**
+ * @param header what must be set on a model before replaying the steps
+ * @param steps the steps, replayed in order
+ * @param search the search, applied once the steps are replayed; null for the default search
+ */
+ public ModelSpec {
+ Objects.requireNonNull(header);
+ steps = List.copyOf(steps);
+ checkIdentifiers(steps);
+ }
+
+ /**
+ * A spec with the default search.
+ */
+ public ModelSpec(Header header, List steps) {
+ this(header, steps, null);
+ }
+
+ /**
+ * @return this spec with another search
+ */
+ public ModelSpec withSearch(SearchDecl search) {
+ return new ModelSpec(header, steps, search);
+ }
+
+ /**
+ * @return this spec with another header
+ */
+ public ModelSpec withHeader(Header header) {
+ return new ModelSpec(header, steps, search);
+ }
+
+ /**
+ * @return this spec with other steps
+ */
+ public ModelSpec withSteps(List steps) {
+ return new ModelSpec(header, steps, search);
+ }
+
+ /**
+ * @return the spec rewritten by variant
+ */
+ public ModelSpec with(Variant variant) {
+ return variant.apply(this);
+ }
+
+ /**
+ * Build a new model from a variant of this spec. When several models are built from the same variant, prefer
+ * {@code with(variant)} once, then {@link #instantiate()} on the result.
+ *
+ * @return the mapping from the identifiers to the objects of the new model ({@link Resolver#model()})
+ */
+ public Resolver instantiate(Variant variant) {
+ return with(variant).instantiate();
+ }
+
+ /**
+ * Record the construction of a model.
+ *
+ * @param name name of the model
+ * @param settings settings of the model
+ * @param builder builds the model
+ * @return the spec of the model
+ * @throws SpecException if some operations could not be journalized
+ * @see RecordingModel
+ */
+ public static ModelSpec record(String name, Settings settings, Consumer builder) {
+ RecordingModel model = new RecordingModel(name, settings);
+ builder.accept(model);
+ return model.snapshot();
+ }
+
+ /**
+ * Build a new model from this spec.
+ *
+ * @return the mapping from the identifiers to the objects of the new model ({@link Resolver#model()})
+ * @throws SpecException if a step cannot be replayed
+ */
+ public Resolver instantiate() {
+ Resolver resolver = new Replayer(header).replay(steps);
+ if (search != null) {
+ search.configure(resolver);
+ }
+ return resolver;
+ }
+
+ /**
+ * Check that each identifier is defined before being used.
+ */
+ private static void checkIdentifiers(List steps) {
+ BitSet defined = new BitSet();
+ for (int i = 0; i < steps.size(); i++) {
+ Step step = steps.get(i);
+ final int s = i;
+ step.forEachUse(id -> {
+ if (!defined.get(id)) {
+ throw new SpecException("Step #" + s + " (" + step + ") uses undefined identifier " + id);
+ }
+ });
+ step.forEachDef(defined::set);
+ }
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/Recorder.java b/solver/src/main/java/org/chocosolver/solver/spec/Recorder.java
new file mode 100644
index 0000000000..f6f208eb6d
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/Recorder.java
@@ -0,0 +1,516 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.sat.Literalizer;
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.constraints.nary.cnf.ILogical;
+import org.chocosolver.solver.constraints.nary.cnf.LogOp;
+import org.chocosolver.solver.search.SearchState;
+import org.chocosolver.solver.variables.BoolVar;
+import org.chocosolver.solver.variables.IntVar;
+import org.chocosolver.solver.variables.Task;
+import org.chocosolver.solver.variables.Variable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * Journalizes the construction of a {@link RecordingModel} into a {@link ModelSpec}.
+ *
+ * Only top-level operations are journalized: a depth counter ensures that the operations triggered internally by a
+ * factory (decompositions, intermediate variables, internal posts...) are not, since replaying the top-level
+ * operation replays them too.
+ *
+ * An operation that cannot be journalized (unknown object, unsupported argument...) does not interrupt the
+ * construction of the model: it is reported, and {@link #snapshot()} fails.
+ *
+ * The recorder is also the {@link Resolver} of the recorded model: identifiers of the spec map to its objects.
+ *
+ * Not thread-safe: a model is built by a single thread.
+ *
+ * @author Charles Prud'homme
+ */
+public final class Recorder extends IdTable {
+
+ /**
+ * An operation which could not be journalized.
+ *
+ * @param step index of the step at which the operation occurred
+ * @param operation the operation
+ * @param reason why it could not be journalized
+ */
+ public record Unsupported(int step, String operation, String reason) {
+ @Override
+ public String toString() {
+ return "#" + step + " " + operation + ": " + reason;
+ }
+ }
+
+ /**
+ * Thrown when an argument cannot be captured.
+ */
+ private static final class Uncapturable extends Exception {
+ Uncapturable(String message) {
+ super(message, null, false, false);
+ }
+ }
+
+ private static final FactoryMethod INT_CONSTANT = FactoryMethod.of("intVar", int.class);
+ private static final FactoryMethod BOOL_NOT_VIEW = FactoryMethod.of("boolNotView", BoolVar.class);
+
+ private Header header;
+ private final List steps = new ArrayList<>();
+ private final List unsupported = new ArrayList<>();
+ /**
+ * Reverse mapping, maintained eagerly: it is queried for each argument recorded.
+ */
+ private final IdentityHashMap ids = new IdentityHashMap<>();
+ private int depth;
+
+ Recorder(Model model, Header header) {
+ super(model);
+ this.header = header;
+ }
+
+ /**
+ * @return true when an operation is being journalized: nested operations must not be journalized
+ */
+ public boolean busy() {
+ return depth > 0;
+ }
+
+ /**
+ * @return the operations which could not be journalized so far
+ */
+ public List unsupported() {
+ return Collections.unmodifiableList(unsupported);
+ }
+
+ /**
+ * @param o an object of the model
+ * @return the identifier of o in the spec, or -1 if o is unknown
+ */
+ @Override
+ public int idOf(Object o) {
+ return ids.getOrDefault(o, -1);
+ }
+
+ /**
+ * @param vars variables of the recorded model
+ * @return their identifiers in the spec, e.g. to declare a {@link SearchDecl}
+ * @throws SpecException if a variable is unknown
+ */
+ public int[] idsOf(Variable... vars) {
+ int[] r = new int[vars.length];
+ for (int i = 0; i < vars.length; i++) {
+ r[i] = idOf(vars[i]);
+ if (r[i] < 0) {
+ throw new SpecException("Unknown variable " + vars[i].getName());
+ }
+ }
+ return r;
+ }
+
+ /**
+ * @return the spec of the model built so far
+ * @throws SpecException if some operations could not be journalized, or if the resolution has started
+ */
+ public ModelSpec snapshot() {
+ if (model().getSolver().getSearchState() != SearchState.NEW) {
+ throw new SpecException("Cannot snapshot a model whose resolution has started");
+ }
+ if (!unsupported.isEmpty()) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(unsupported.size()).append(" operation(s) cannot be journalized:");
+ unsupported.stream().limit(10).forEach(u -> sb.append("\n\t").append(u));
+ if (unsupported.size() > 10) {
+ sb.append("\n\t...");
+ }
+ throw new SpecException(sb.toString());
+ }
+ return new ModelSpec(header, steps);
+ }
+
+ //***********************************************************************************
+ // OPERATIONS
+ //***********************************************************************************
+
+ /**
+ * Journalize a call to a factory method.
+ *
+ * @param method the factory method
+ * @param body the actual call
+ * @param args the arguments of the call
+ * @return the result of the call
+ */
+ R call(FactoryMethod method, Supplier body, Object[] args) {
+ Arg[] captured = new Arg[args.length];
+ String failure = null;
+ try {
+ for (int i = 0; i < args.length; i++) {
+ captured[i] = capture(args[i]);
+ }
+ } catch (Uncapturable e) {
+ failure = e.getMessage();
+ }
+ R result = nested(body);
+ int[] out = outputs(result);
+ if (failure == null) {
+ steps.add(new Step.Call(out, method, captured));
+ } else {
+ unsupported(method.toString(), failure);
+ }
+ return result;
+ }
+
+ /**
+ * Journalize a call to a factory method which returns nothing.
+ */
+ void run(FactoryMethod method, Runnable body, Object[] args) {
+ call(method, () -> {
+ body.run();
+ return null;
+ }, args);
+ }
+
+ void post(Constraint[] cstrs, Runnable body) {
+ int[] cids = constraintIds("post", cstrs);
+ nested(body);
+ if (cids != null) {
+ steps.add(new Step.Post(cids));
+ }
+ }
+
+ void unpost(Constraint[] cstrs, Runnable body) {
+ int[] cids = constraintIds("unpost", cstrs);
+ nested(body);
+ if (cids != null) {
+ steps.add(new Step.Unpost(cids));
+ }
+ }
+
+ /**
+ * Journalize {@link Constraint#reify()}.
+ */
+ public BoolVar reify(Constraint cstr, Supplier body) {
+ int cid = ref("reify", cstr);
+ BoolVar b = nested(body);
+ int out = idFor(b);
+ if (cid >= 0) {
+ steps.add(new Step.Reify(cid, out));
+ }
+ return b;
+ }
+
+ /**
+ * Journalize {@link Constraint#reifyWith}, {@link Constraint#implies} or {@link Constraint#impliedBy}.
+ */
+ public void link(Step.Link.Kind kind, Constraint cstr, BoolVar bool, Runnable body) {
+ int[] r = refs(kind.name(), cstr, bool);
+ nested(body);
+ if (r != null) {
+ steps.add(new Step.Link(kind, r[0], r[1]));
+ }
+ }
+
+ /**
+ * Journalize the creation of a task with its canonical constructor (the task is already built).
+ *
+ * @param performed the performed variable of an optional task, null for a mandatory task
+ */
+ public void newTask(Task task, IntVar start, IntVar duration, IntVar end, BoolVar performed) {
+ if (busy()) {
+ return;
+ }
+ int[] r = performed == null
+ ? refs("new Task", start, duration, end)
+ : refs("new OptionalTask", start, duration, end, performed);
+ int out = idFor(task);
+ if (r != null) {
+ steps.add(new Step.NewTask(out, r[0], r[1], r[2], performed == null ? -1 : r[3]));
+ }
+ }
+
+ void hook(String name, Object value, Runnable body) {
+ Arg captured = tryCapture("addHook(" + name + ")", value);
+ nested(body);
+ if (captured != null) {
+ steps.add(new Step.Hook(name, captured));
+ }
+ }
+
+ void removeAllHooks(Runnable body) {
+ nested(body);
+ steps.add(new Step.Hook(null, new Arg.Const(null)));
+ }
+
+ void name(String name, Runnable body) {
+ nested(body);
+ header = header.withName(name);
+ }
+
+ void group(String name, Variable[] vars, Runnable body) {
+ Arg captured = tryCapture("addGroup(" + name + ")", vars);
+ nested(body);
+ if (captured instanceof Arg.RefArray a) {
+ steps.add(new Step.Group(name, a));
+ } else if (captured != null) {
+ unsupported("addGroup(" + name + ")", "not an array of variables");
+ }
+ }
+
+ void seed(long seed, Runnable body) {
+ nested(body);
+ steps.add(new Step.Seed(seed));
+ }
+
+ void objective(boolean maximize, Variable objective, Runnable body) {
+ int id = objective == null ? -1 : ref("setObjective", objective);
+ nested(body);
+ if (objective == null || id >= 0) {
+ steps.add(new Step.Objective(maximize, id));
+ }
+ }
+
+ /**
+ * Run body without journalizing what it does.
+ */
+ R silently(Supplier body) {
+ return nested(body);
+ }
+
+ void unsupported(String operation, String reason) {
+ unsupported.add(new Unsupported(steps.size(), operation, reason));
+ }
+
+ //***********************************************************************************
+ // INTERNALS
+ //***********************************************************************************
+
+ private R nested(Supplier body) {
+ depth++;
+ try {
+ return body.get();
+ } finally {
+ depth--;
+ }
+ }
+
+ private void nested(Runnable body) {
+ nested(() -> {
+ body.run();
+ return null;
+ });
+ }
+
+ /**
+ * @return the captured argument, or null (and the operation is reported) if it cannot be captured
+ */
+ private Arg tryCapture(String operation, Object value) {
+ try {
+ return capture(value);
+ } catch (Uncapturable e) {
+ unsupported(operation, e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * @return the identifier of o , or -1 (and the operation is reported) if o is unknown
+ */
+ private int ref(String operation, Object o) {
+ try {
+ return refOf(o);
+ } catch (Uncapturable e) {
+ unsupported(operation, e.getMessage());
+ return -1;
+ }
+ }
+
+ /**
+ * @return the identifiers of objects , or null (and the operation is reported) if one is unknown
+ */
+ private int[] refs(String operation, Object... objects) {
+ int[] r = new int[objects.length];
+ for (int i = 0; i < objects.length; i++) {
+ r[i] = ref(operation, objects[i]);
+ if (r[i] < 0) {
+ return null;
+ }
+ }
+ return r;
+ }
+
+ private int[] constraintIds(String operation, Constraint[] cstrs) {
+ return cstrs == null ? Specs.NO_INT : refs(operation, (Object[]) cstrs);
+ }
+
+ private static boolean isModelType(Class> c) {
+ return Variable.class.isAssignableFrom(c) || Constraint.class.isAssignableFrom(c) || Task.class.isAssignableFrom(c);
+ }
+
+ private static boolean isModelObject(Object o) {
+ return o != null && isModelType(o.getClass());
+ }
+
+ private Arg capture(Object v) throws Uncapturable {
+ if (isModelObject(v)) {
+ return new Arg.Ref(refOf(v));
+ }
+ if (v instanceof LogOp op) {
+ return logic(op);
+ }
+ if (v instanceof Literalizer.BoolLit l) {
+ return new Arg.Literal(Arg.Literal.Kind.BOOL, refOf(l.cpVar), 0);
+ }
+ if (v instanceof Literalizer.IntEqLit l) {
+ return new Arg.Literal(Arg.Literal.Kind.INT_EQ, refOf(l.cpVar), l.val);
+ }
+ if (v instanceof Literalizer.IntLeLit l) {
+ return new Arg.Literal(Arg.Literal.Kind.INT_LE, refOf(l.cpVar), l.val);
+ }
+ if (v instanceof List> list) {
+ int[] lids = new int[list.size()];
+ for (int i = 0; i < lids.length; i++) {
+ if (!isModelObject(list.get(i))) {
+ throw new Uncapturable("list of " + (list.get(i) == null ? "null" : list.get(i).getClass().getSimpleName()));
+ }
+ lids[i] = refOf(list.get(i));
+ }
+ return new Arg.RefList(lids);
+ }
+ if (v != null && v.getClass().isArray()) {
+ Class> ct = v.getClass().getComponentType();
+ if (isModelType(ct)) {
+ return new Arg.RefArray(ct, refsOf((Object[]) v));
+ }
+ if (ct.isArray() && isModelType(ct.getComponentType())) {
+ Object[][] m = (Object[][]) v;
+ int[][] mids = new int[m.length][];
+ for (int i = 0; i < m.length; i++) {
+ mids[i] = refsOf(m[i]);
+ }
+ return new Arg.RefMatrix(ct.getComponentType(), mids);
+ }
+ }
+ try {
+ return new Arg.Const(v);
+ } catch (SpecException e) {
+ throw new Uncapturable(e.getMessage());
+ }
+ }
+
+ private Arg.Logic logic(LogOp op) throws Uncapturable {
+ ILogical[] children = op.getChildren();
+ Arg[] args = new Arg[children.length];
+ for (int i = 0; i < children.length; i++) {
+ if (children[i] instanceof LogOp sub) {
+ args[i] = logic(sub);
+ } else if (children[i] instanceof BoolVar) {
+ args[i] = new Arg.Ref(refOf(children[i]));
+ } else {
+ throw new Uncapturable("logical tree with a " + children[i].getClass().getSimpleName());
+ }
+ }
+ return new Arg.Logic(op.is(LogOp.Operator.AND), op.isNot(), args);
+ }
+
+ private int[] refsOf(Object[] objects) throws Uncapturable {
+ if (objects == null) {
+ throw new Uncapturable("null array");
+ }
+ int[] r = new int[objects.length];
+ for (int i = 0; i < objects.length; i++) {
+ r[i] = refOf(objects[i]);
+ }
+ return r;
+ }
+
+ /**
+ * @return the identifier of o ; an unknown object is derived, when possible, from known objects
+ * (cached constant, negation of a known boolean variable)
+ */
+ private int refOf(Object o) throws Uncapturable {
+ if (o == null) {
+ throw new Uncapturable("null object");
+ }
+ Integer id = ids.get(o);
+ if (id != null) {
+ return id;
+ }
+ if (o instanceof Variable v) {
+ if (v.getModel() != model()) {
+ throw new Uncapturable("variable " + v.getName() + " belongs to another model");
+ }
+ if (v instanceof IntVar iv && v.isAConstant()) {
+ int value = iv.getValue();
+ if (model().getCachedConstants().get(value) == v) {
+ return derive(INT_CONSTANT, v, new Arg.Const(value));
+ }
+ }
+ if (v instanceof BoolVar b && b.hasNot()) {
+ Integer nid = ids.get(b.not());
+ if (nid != null) {
+ return derive(BOOL_NOT_VIEW, v, new Arg.Ref(nid));
+ }
+ }
+ throw new Uncapturable("unknown variable " + v.getName()
+ + " (created outside of the factories, or internally by a factory)");
+ }
+ if (o instanceof Constraint c) {
+ throw new Uncapturable("unknown constraint " + c.getName()
+ + " (custom constraint not built with Model.custom(...), or created internally by a factory)");
+ }
+ throw new Uncapturable("unknown " + o.getClass().getSimpleName());
+ }
+
+ /**
+ * Journalize a call which returns the already existing object o (e.g., a cached constant).
+ */
+ private int derive(FactoryMethod method, Object o, Arg arg) {
+ int id = idFor(o);
+ steps.add(new Step.Call(new int[]{id}, method, new Arg[]{arg}));
+ return id;
+ }
+
+ /**
+ * @return the identifier of o , a new one if o is unknown
+ */
+ private int idFor(Object o) {
+ Integer id = ids.get(o);
+ if (id == null) {
+ id = capacity();
+ ids.put(o, id);
+ put(id, o);
+ }
+ return id;
+ }
+
+ /**
+ * @return the identifiers of the model objects returned by a call (flattened for arrays)
+ */
+ private int[] outputs(Object result) {
+ if (isModelObject(result)) {
+ return new int[]{idFor(result)};
+ }
+ if (result instanceof Object[] array && isModelType(Specs.elementType(array.getClass()))) {
+ List flat = new ArrayList<>();
+ Specs.flatten(array, flat);
+ int[] out = new int[flat.size()];
+ for (int i = 0; i < out.length; i++) {
+ out[i] = flat.get(i) == null ? -1 : idFor(flat.get(i));
+ }
+ return out;
+ }
+ return Specs.NO_INT;
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/Replayer.java b/solver/src/main/java/org/chocosolver/solver/spec/Replayer.java
new file mode 100644
index 0000000000..bd7c66c2ec
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/Replayer.java
@@ -0,0 +1,215 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.sat.Literalizer;
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.constraints.nary.cnf.ILogical;
+import org.chocosolver.solver.constraints.nary.cnf.LogOp;
+import org.chocosolver.solver.variables.Group;
+import org.chocosolver.solver.variables.IntVar;
+import org.chocosolver.solver.variables.OptionalTask;
+import org.chocosolver.solver.variables.Task;
+import org.chocosolver.solver.variables.Variable;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Replays the steps of a {@link ModelSpec} on a new model.
+ * Not thread-safe: one replayer per instantiation.
+ *
+ * @author Charles Prud'homme
+ */
+final class Replayer extends IdTable {
+
+ Replayer(Header header) {
+ super(new Model(header.name(), header.settings()));
+ model().setSeed(header.seed());
+ }
+
+ Resolver replay(List steps) {
+ for (int i = 0; i < steps.size(); i++) {
+ Step step = steps.get(i);
+ try {
+ apply(step);
+ } catch (VirtualMachineError e) {
+ // e.g., OutOfMemoryError: not a problem of the spec
+ throw e;
+ } catch (Throwable t) {
+ throw new SpecException("Step #" + i + " (" + step + ") failed: " + t.getMessage(), t);
+ }
+ }
+ return this;
+ }
+
+ private void apply(Step step) throws Throwable {
+ Model model = model();
+ if (step instanceof Step.Call c) {
+ Class>[] types = c.method().rawParameterTypes();
+ Arg[] args = c.rawArgs();
+ Object[] values = new Object[args.length];
+ for (int k = 0; k < args.length; k++) {
+ values[k] = materialize(args[k], types[k]);
+ }
+ bindOutputs(c.rawOut(), c.method().invoke(model, values));
+ } else if (step instanceof Step.Post p) {
+ model.post(constraints(p.rawCstrs()));
+ } else if (step instanceof Step.Reify r) {
+ bind(r.out(), constraint(r.cstr()).reify());
+ } else if (step instanceof Step.Link l) {
+ Constraint c = constraint(l.cstr());
+ switch (l.kind()) {
+ case REIFY_WITH -> c.reifyWith(boolVar(l.bool()));
+ case IMPLIES -> c.implies(boolVar(l.bool()));
+ case IMPLIED_BY -> c.impliedBy(boolVar(l.bool()));
+ }
+ } else if (step instanceof Step.Unpost u) {
+ model.unpost(constraints(u.rawCstrs()));
+ } else if (step instanceof Step.NewTask t) {
+ IntVar s = intVar(t.start());
+ IntVar d = intVar(t.duration());
+ IntVar e = intVar(t.end());
+ bind(t.out(), t.performed() < 0 ? new Task(s, d, e) : new OptionalTask(s, d, e, boolVar(t.performed())));
+ } else if (step instanceof Step.Custom c) {
+ Constraint cstr = c.factory().apply(this);
+ if (cstr == null) {
+ throw new SpecException("Custom constraint " + c.name() + " built nothing");
+ }
+ if (c.out() >= 0) {
+ bind(c.out(), cstr);
+ }
+ } else if (step instanceof Step.Hook h) {
+ Object value = materialize(h.value(), Object.class);
+ if (h.name() == null) {
+ model.removeAllHooks();
+ } else if (value == null) {
+ model.removeHook(h.name());
+ } else {
+ model.addHook(h.name(), value);
+ }
+ } else if (step instanceof Step.Seed s) {
+ model.setSeed(s.seed());
+ } else if (step instanceof Step.Objective o) {
+ if (o.var() < 0) {
+ model.clearObjective();
+ } else {
+ model.setObjective(o.maximize(), get(o.var(), Variable.class));
+ }
+ } else if (step instanceof Step.Group g) {
+ model.addGroup(new Group<>(g.name(), (Variable[]) materialize(g.vars(), Variable[].class)));
+ } else {
+ throw new SpecException("Unknown step " + step);
+ }
+ }
+
+ private Constraint[] constraints(int[] cids) {
+ Constraint[] cs = new Constraint[cids.length];
+ for (int i = 0; i < cids.length; i++) {
+ cs[i] = constraint(cids[i]);
+ }
+ return cs;
+ }
+
+ private Object materialize(Arg arg, Class> type) {
+ Object value;
+ if (arg instanceof Arg.Ref r) {
+ value = get(r.id());
+ } else if (arg instanceof Arg.RefArray a) {
+ value = array(a.componentType(), a.rawIds());
+ } else if (arg instanceof Arg.RefMatrix a) {
+ int[][] rows = a.rawIds();
+ Object[] matrix = (Object[]) Array.newInstance(a.componentType().arrayType(), rows.length);
+ for (int i = 0; i < rows.length; i++) {
+ matrix[i] = array(a.componentType(), rows[i]);
+ }
+ value = matrix;
+ } else if (arg instanceof Arg.RefList l) {
+ List list = new ArrayList<>(l.rawIds().length);
+ for (int id : l.rawIds()) {
+ list.add(get(id));
+ }
+ value = list;
+ } else if (arg instanceof Arg.Const c) {
+ value = c.value();
+ } else if (arg instanceof Arg.Logic l) {
+ value = logic(l);
+ } else if (arg instanceof Arg.Literal l) {
+ value = switch (l.kind()) {
+ case BOOL -> new Literalizer.BoolLit(boolVar(l.var()));
+ case INT_EQ -> new Literalizer.IntEqLit(intVar(l.var()), l.value());
+ case INT_LE -> new Literalizer.IntLeLit(intVar(l.var()), l.value());
+ };
+ } else {
+ throw new SpecException("Unknown argument " + arg);
+ }
+ if (value == null ? type.isPrimitive() : !Specs.wrap(type).isInstance(value)) {
+ throw new SpecException("Argument " + arg + " is not a " + type.getSimpleName());
+ }
+ return value;
+ }
+
+ private Object[] array(Class> componentType, int[] aids) {
+ Object[] array = (Object[]) Array.newInstance(componentType, aids.length);
+ for (int i = 0; i < aids.length; i++) {
+ try {
+ array[i] = get(aids[i]);
+ } catch (ArrayStoreException e) {
+ throw new SpecException("Identifier " + aids[i] + " is bound to a "
+ + get(aids[i]).getClass().getSimpleName() + ", not a " + componentType.getSimpleName());
+ }
+ }
+ return array;
+ }
+
+ private LogOp logic(Arg.Logic l) {
+ Arg[] children = l.rawChildren();
+ ILogical[] ops = new ILogical[children.length];
+ for (int i = 0; i < children.length; i++) {
+ ops[i] = children[i] instanceof Arg.Logic sub ? logic(sub) : boolVar(((Arg.Ref) children[i]).id());
+ }
+ if (l.and()) {
+ return l.negated() ? LogOp.nand(ops) : LogOp.and(ops);
+ }
+ return l.negated() ? LogOp.nor(ops) : LogOp.or(ops);
+ }
+
+ private void bindOutputs(int[] out, Object result) {
+ if (out.length == 0) {
+ return;
+ }
+ if (result instanceof Object[] array) {
+ List flat = new ArrayList<>(out.length);
+ Specs.flatten(array, flat);
+ if (flat.size() != out.length) {
+ throw new SpecException("Expected " + out.length + " outputs, got " + flat.size());
+ }
+ for (int i = 0; i < out.length; i++) {
+ if (out[i] >= 0) {
+ bind(out[i], flat.get(i));
+ }
+ }
+ } else {
+ if (out.length != 1) {
+ throw new SpecException("Expected " + out.length + " outputs, got 1");
+ }
+ bind(out[0], result);
+ }
+ }
+
+ private void bind(int id, Object o) {
+ if (o == null) {
+ throw new SpecException("Cannot bind identifier " + id + " to null");
+ }
+ Object prev = put(id, o);
+ if (prev != null && prev != o) {
+ throw new SpecException("Divergence: identifier " + id + " is bound to " + prev + ", now to " + o);
+ }
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/Resolver.java b/solver/src/main/java/org/chocosolver/solver/spec/Resolver.java
new file mode 100644
index 0000000000..4d5325bf9d
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/Resolver.java
@@ -0,0 +1,89 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.variables.BoolVar;
+import org.chocosolver.solver.variables.IntVar;
+import org.chocosolver.solver.variables.Task;
+
+/**
+ * Maps the identifiers of a {@link ModelSpec} to the objects of one instantiated model.
+ *
+ * @author Charles Prud'homme
+ */
+public interface Resolver {
+
+ /**
+ * @return the model under instantiation (or instantiated)
+ */
+ Model model();
+
+ /**
+ * @param id an identifier
+ * @return the object bound to id
+ * @throws SpecException if id is not bound
+ */
+ Object get(int id);
+
+ /**
+ * @param id an identifier
+ * @param type expected type of the object
+ * @return the object bound to id
+ * @throws SpecException if id is not bound or not of the expected type
+ */
+ default T get(int id, Class type) {
+ Object o = get(id);
+ if (!type.isInstance(o)) {
+ throw new SpecException("Identifier " + id + " is bound to a " + o.getClass().getSimpleName()
+ + ", not a " + type.getSimpleName());
+ }
+ return type.cast(o);
+ }
+
+ /**
+ * @return an upper bound (exclusive) of the identifiers
+ */
+ int capacity();
+
+ /**
+ * @param id an identifier
+ * @return true if id is bound to an object
+ */
+ boolean isBound(int id);
+
+ /**
+ * @param o an object of the model
+ * @return the (first) identifier bound to o , or -1 if none
+ */
+ int idOf(Object o);
+
+ default IntVar intVar(int id) {
+ return get(id, IntVar.class);
+ }
+
+ default IntVar[] intVars(int... ids) {
+ IntVar[] vars = new IntVar[ids.length];
+ for (int i = 0; i < ids.length; i++) {
+ vars[i] = intVar(ids[i]);
+ }
+ return vars;
+ }
+
+ default BoolVar boolVar(int id) {
+ return get(id, BoolVar.class);
+ }
+
+ default Constraint constraint(int id) {
+ return get(id, Constraint.class);
+ }
+
+ default Task task(int id) {
+ return get(id, Task.class);
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/SearchDecl.java b/solver/src/main/java/org/chocosolver/solver/spec/SearchDecl.java
new file mode 100644
index 0000000000..551ce179c2
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/SearchDecl.java
@@ -0,0 +1,109 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.Solver;
+import org.chocosolver.solver.search.strategy.Search;
+import org.chocosolver.solver.search.strategy.SearchParams;
+import org.chocosolver.solver.search.strategy.strategy.AbstractStrategy;
+import org.chocosolver.solver.variables.IntVar;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * The search of a {@link ModelSpec}: configures the solver of each instance, once all the steps are replayed.
+ *
+ * A search declaration is shared by all the instances, possibly configured concurrently: it must be stateless and
+ * must only use the objects of the instance, obtained from the {@link Resolver} (never objects of another model,
+ * which is checked by assertions when the strategy is set).
+ *
+ * Search strategies cannot be journalized: they are declared, either from the catalog of this interface
+ * (heuristics and identifiers of variables), or as a function of the model or of the {@link Resolver}.
+ *
+ * @author Charles Prud'homme
+ */
+@FunctionalInterface
+public interface SearchDecl {
+
+ /**
+ * Configure the solver of an instance.
+ *
+ * @param resolver maps the identifiers of the spec to the objects of the instance
+ */
+ void configure(Resolver resolver);
+
+ /**
+ * @return a declaration which applies this one, then next
+ */
+ default SearchDecl andThen(SearchDecl next) {
+ Objects.requireNonNull(next);
+ return r -> {
+ configure(r);
+ next.configure(r);
+ };
+ }
+
+ /**
+ * A declaration based on the model only, e.g. {@code SearchDecl.of(Search::defaultSearch)} or
+ * {@code SearchDecl.of(BlackBoxConfigurator.forCOP()::make)}: the model of each instance holds the same
+ * variables, groups, hooks and objective as the recorded one.
+ *
+ * @param configurer configures a model; must be stateless
+ */
+ static SearchDecl of(Consumer configurer) {
+ Objects.requireNonNull(configurer);
+ return r -> configurer.accept(r.model());
+ }
+
+ /**
+ * An integer search strategy over the variables identified by ids .
+ *
+ * @param varSel variable selection
+ * @param valSel value selection
+ * @param ids identifiers of the decision variables, in order
+ */
+ static SearchDecl intVarSearch(SearchParams.VarSelConf varSel, SearchParams.ValSelConf valSel, int... ids) {
+ Objects.requireNonNull(varSel);
+ Objects.requireNonNull(valSel);
+ int[] vids = ids.clone();
+ return r -> {
+ IntVar[] vars = r.intVars(vids);
+ r.model().getSolver().setSearch(varSel.make().apply(vars, valSel.make().apply(r.model())));
+ };
+ }
+
+ /**
+ * Add a restart policy.
+ *
+ * @param conf the restart policy
+ */
+ static SearchDecl restarts(SearchParams.ResConf conf) {
+ Objects.requireNonNull(conf);
+ return r -> {
+ Solver solver = r.model().getSolver();
+ solver.addRestarter(conf.make().apply(solver));
+ };
+ }
+
+ /**
+ * Wrap the current search strategy with last conflict.
+ *
+ * @param k number of conflicting variables to remember
+ */
+ static SearchDecl lastConflict(int k) {
+ return r -> {
+ Solver solver = r.model().getSolver();
+ AbstractStrategy> current = solver.getSearch();
+ if (current == null) {
+ throw new SpecException("lastConflict requires a search strategy to wrap");
+ }
+ solver.setSearch(Search.lastConflict(current, k));
+ };
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/SharedObjects.java b/solver/src/main/java/org/chocosolver/solver/spec/SharedObjects.java
new file mode 100644
index 0000000000..33afd410cf
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/SharedObjects.java
@@ -0,0 +1,252 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Settings;
+import org.chocosolver.solver.constraints.nary.automata.FA.utils.ICounter;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.ref.Reference;
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiConsumer;
+
+/**
+ * Diagnostic tool: finds the objects shared by two object graphs (typically, two models instantiated from the same
+ * {@link ModelSpec}), which must be immutable for the models to be solved concurrently.
+ *
+ * Objects are compared by identity. The traversal follows instance fields (static fields are ignored), array
+ * elements, the elements of collections and maps, and referents. Objects of classes which cannot be introspected
+ * (e.g., of the JDK) are compared but not traversed.
+ *
+ * Are neither reported nor traversed: immutable objects (classes, methods, the values shared according to the policy
+ * of {@link Values}), and the data shared by design ({@link Settings}, {@link ICounter}, {@link FactoryMethod}, frozen
+ * tuples and diagrams). What the latter own is not reported either, even when reached by another path.
+ *
+ * @author Charles Prud'homme
+ */
+public final class SharedObjects {
+
+ private static final Map, Field[]> FIELDS = new ConcurrentHashMap<>();
+
+ private SharedObjects() {
+ }
+
+ /**
+ * A shared object.
+ *
+ * @param type type of the object
+ * @param path a path to the object from the root of the second graph
+ */
+ public record Shared(Class> type, String path) {
+ @Override
+ public String toString() {
+ return type.getName() + " at " + path;
+ }
+ }
+
+ /**
+ * @param a root of the first graph
+ * @param b root of the second graph
+ * @param allowed types whose instances may be shared (neither reported nor traversed)
+ * @return the mutable objects reachable from both a and b , one example per type
+ */
+ public static List between(Object a, Object b, Set> allowed) {
+ return between(a, b, allowed, List.of());
+ }
+
+ /**
+ * @param a root of the first graph
+ * @param b root of the second graph
+ * @param allowed types whose instances may be shared (neither reported nor traversed)
+ * @param byDesign objects which may be shared, e.g. the constants of a {@link ModelSpec} (see
+ * {@link #constantsOf(ModelSpec)}): neither they nor what they own are reported
+ * @return the mutable objects reachable from both a and b , one example per type
+ */
+ public static List between(Object a, Object b, Set> allowed, Collection> byDesign) {
+ Set common = common(a, b, allowed, byDesign);
+ if (common.isEmpty()) {
+ return List.of();
+ }
+ // a path to one shared object per type
+ Map, Shared> shared = new LinkedHashMap<>();
+ traverse(b, allowed, identitySet(), identitySet(), (o, node) -> {
+ if (common.contains(o)) {
+ shared.putIfAbsent(o.getClass(), new Shared(o.getClass(), node.path()));
+ }
+ }, true);
+ return new ArrayList<>(shared.values());
+ }
+
+ /**
+ * @return the mutable objects reachable from both a and b , but those shared by design and what they
+ * own (see {@link #between(Object, Object, Set, Collection)})
+ */
+ public static Set common(Object a, Object b, Set> allowed, Collection> byDesign) {
+ Set designed = identitySet();
+ designed.addAll(byDesign);
+ // no path kept, to save memory
+ Set fromA = identitySet();
+ traverse(a, allowed, fromA, designed, null, false);
+ Set common = identitySet();
+ traverse(b, allowed, identitySet(), designed, (o, node) -> {
+ if (fromA.contains(o)) {
+ common.add(o);
+ }
+ }, false);
+ fromA.clear();
+ // what the data shared by design own may be reached by other paths (e.g., the matrix of frozen tuples
+ // referenced by a propagator): it is shared by design too
+ if (!common.isEmpty()) {
+ Set owned = identitySet();
+ for (Object d : designed) {
+ traverse(d, null, owned, null, null, false);
+ }
+ common.removeAll(owned);
+ }
+ return common;
+ }
+
+ /**
+ * @return the constants of spec , which are shared by design by its instances
+ */
+ public static List constantsOf(ModelSpec spec) {
+ List constants = new ArrayList<>();
+ for (Step s : spec.steps()) {
+ if (s instanceof Step.Call c) {
+ for (Arg arg : c.rawArgs()) {
+ if (arg instanceof Arg.Const k && k.rawValue() != null) {
+ constants.add(k.rawValue());
+ }
+ }
+ }
+ }
+ return constants;
+ }
+
+ private static Set identitySet() {
+ return Collections.newSetFromMap(new IdentityHashMap<>());
+ }
+
+ private record Node(Object object, Node parent, String edge) {
+ String path() {
+ return parent == null ? edge : parent.path() + edge;
+ }
+ }
+
+ /**
+ * Traverse the graph of root . When allowed is not null, immutable objects are skipped; when
+ * byDesign is not null, the data shared by design are skipped and collected in it. Paths from the root
+ * are kept only if paths is set.
+ */
+ private static void traverse(Object root, Set> allowed, Set seen, Set byDesign,
+ BiConsumer visitor, boolean paths) {
+ Deque stack = new ArrayDeque<>();
+ stack.push(new Node(root, null, root.getClass().getSimpleName()));
+ while (!stack.isEmpty()) {
+ Node node = stack.pop();
+ Object o = node.object;
+ if (o == null || !seen.add(o)) {
+ continue;
+ }
+ if (allowed != null && isImmutable(o, allowed)) {
+ continue;
+ }
+ if (byDesign != null && (byDesign.contains(o) || isSharedByDesign(o))) {
+ byDesign.add(o);
+ continue;
+ }
+ if (visitor != null) {
+ visitor.accept(o, node);
+ }
+ Node parent = paths ? node : null;
+ Class> c = o.getClass();
+ if (c.isArray()) {
+ if (!c.getComponentType().isPrimitive()) {
+ int n = Array.getLength(o);
+ for (int i = 0; i < n; i++) {
+ stack.push(new Node(Array.get(o, i), parent, paths ? "[" + i + "]" : null));
+ }
+ }
+ } else if (o instanceof Collection> col) {
+ try {
+ for (Object e : col) {
+ stack.push(new Node(e, parent, paths ? "{}" : null));
+ }
+ } catch (RuntimeException ignored) {
+ // concurrent or lazy collection: elements not traversed
+ }
+ } else if (o instanceof Reference> ref) {
+ stack.push(new Node(ref.get(), parent, paths ? ".get()" : null));
+ } else if (o instanceof Map, ?> map) {
+ try {
+ for (Map.Entry, ?> e : map.entrySet()) {
+ stack.push(new Node(e.getKey(), parent, paths ? "{key}" : null));
+ stack.push(new Node(e.getValue(), parent, paths ? "{" + e.getKey() + "}" : null));
+ }
+ } catch (RuntimeException ignored) {
+ // elements not traversed
+ }
+ } else {
+ for (Field f : fields(c)) {
+ try {
+ stack.push(new Node(f.get(o), parent, paths ? "." + f.getName() : null));
+ } catch (IllegalAccessException ignored) {
+ // not traversed
+ }
+ }
+ }
+ }
+ }
+
+ private static boolean isSharedByDesign(Object o) {
+ return o instanceof Settings || o instanceof ICounter || o instanceof FactoryMethod || Values.isShared(o);
+ }
+
+ private static boolean isImmutable(Object o, Set> allowed) {
+ if (o instanceof String || o instanceof Number || o instanceof Boolean || o instanceof Character
+ || o instanceof Enum || o instanceof Class || o instanceof Method || o instanceof MethodHandle) {
+ return true;
+ }
+ for (Class> a : allowed) {
+ if (a.isInstance(o)) {
+ return true;
+ }
+ }
+ return !(o instanceof Collection) && !(o instanceof Map) && Specs.isStateless(o.getClass());
+ }
+
+ /**
+ * @return the accessible instance fields of c and its superclasses
+ */
+ private static Field[] fields(Class> c) {
+ return FIELDS.computeIfAbsent(c, k -> {
+ List list = new ArrayList<>();
+ for (Class> x = k; x != null; x = x.getSuperclass()) {
+ for (Field f : x.getDeclaredFields()) {
+ if (!Modifier.isStatic(f.getModifiers()) && !f.getType().isPrimitive() && f.trySetAccessible()) {
+ list.add(f);
+ }
+ }
+ }
+ return list.toArray(new Field[0]);
+ });
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/SpecBuilder.java b/solver/src/main/java/org/chocosolver/solver/spec/SpecBuilder.java
new file mode 100644
index 0000000000..518d897103
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/SpecBuilder.java
@@ -0,0 +1,322 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.sat.Literalizer;
+import org.chocosolver.solver.IModel;
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.constraints.nary.cnf.LogOp;
+import org.chocosolver.solver.variables.BoolVar;
+import org.chocosolver.solver.variables.OptionalTask;
+import org.chocosolver.solver.variables.Task;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Helps building a {@link ModelSpec} by hand: allocates identifiers and resolves the overload of factory methods
+ * from the types of the arguments.
+ *
+ * Arguments passed to {@code call*} methods are either {@link Arg}s or constants (wrapped in {@link Arg.Const}).
+ *
{@code
+ * SpecBuilder b = new SpecBuilder(Header.of("demo"));
+ * int[] x = b.callArray(3, "intVarArray", "x", 3, 1, 3);
+ * b.post(b.call("allDifferent", b.refs(IntVar.class, x)));
+ * ModelSpec spec = b.build();
+ * }
+ *
+ * @author Charles Prud'homme
+ */
+public final class SpecBuilder {
+
+ /**
+ * The (non-static) methods of {@link IModel}, by name.
+ */
+ private static final Map> METHODS = Arrays.stream(IModel.class.getMethods())
+ .filter(m -> !Modifier.isStatic(m.getModifiers()))
+ .collect(Collectors.groupingBy(Method::getName));
+
+ private final Header header;
+ private final List steps = new ArrayList<>();
+ /**
+ * Static type of each identifier, to resolve overloads.
+ */
+ private final Map> types = new HashMap<>();
+ private int nextId;
+ private SearchDecl search;
+
+ public SpecBuilder(Header header) {
+ this.header = header;
+ }
+
+ /**
+ * @return a builder which continues spec : its steps, identifiers and search are kept
+ */
+ public static SpecBuilder from(ModelSpec spec) {
+ SpecBuilder b = new SpecBuilder(spec.header());
+ b.search = spec.search();
+ for (Step s : spec.steps()) {
+ b.steps.add(s);
+ if (s instanceof Step.Call c) {
+ Class> t = Specs.elementType(c.method().returnType());
+ for (int o : c.rawOut()) {
+ if (o >= 0) {
+ b.types.put(o, t);
+ }
+ }
+ } else if (s instanceof Step.Reify r) {
+ b.types.put(r.out(), BoolVar.class);
+ } else if (s instanceof Step.NewTask t) {
+ b.types.put(t.out(), t.performed() < 0 ? Task.class : OptionalTask.class);
+ } else if (s instanceof Step.Custom c) {
+ b.types.put(c.out(), Constraint.class);
+ }
+ s.forEachDef(id -> b.nextId = Math.max(b.nextId, id + 1));
+ }
+ return b;
+ }
+
+ /**
+ * Set the search of the spec.
+ */
+ public SpecBuilder search(SearchDecl search) {
+ this.search = search;
+ return this;
+ }
+
+ /**
+ * @return a reference to identifier id
+ */
+ public Arg.Ref ref(int id) {
+ return new Arg.Ref(id);
+ }
+
+ /**
+ * @return a reference to an array of objects of type componentType
+ */
+ public Arg.RefArray refs(Class> componentType, int... ids) {
+ return new Arg.RefArray(componentType, ids);
+ }
+
+ /**
+ * @return a reference to a matrix of objects of type componentType
+ */
+ public Arg.RefMatrix refs(Class> componentType, int[][] ids) {
+ return new Arg.RefMatrix(componentType, ids);
+ }
+
+ /**
+ * Call a factory method whose result is ignored (or which returns nothing).
+ */
+ public SpecBuilder effect(String name, Object... args) {
+ return effect(resolve(name, args), args);
+ }
+
+ public SpecBuilder effect(FactoryMethod m, Object... args) {
+ steps.add(new Step.Call(Specs.NO_INT, m, toArgs(args)));
+ return this;
+ }
+
+ /**
+ * Call a factory method which returns an object (variable, constraint, task...).
+ *
+ * @return the identifier of the object
+ */
+ public int call(String name, Object... args) {
+ return call(resolve(name, args), args);
+ }
+
+ public int call(FactoryMethod m, Object... args) {
+ int id = define(m.returnType());
+ steps.add(new Step.Call(new int[]{id}, m, toArgs(args)));
+ return id;
+ }
+
+ /**
+ * Call a factory method which returns an array (or a matrix, flattened row by row) of size objects.
+ *
+ * @return the identifiers of the objects
+ */
+ public int[] callArray(int size, String name, Object... args) {
+ return callArray(size, resolve(name, args), args);
+ }
+
+ public int[] callArray(int size, FactoryMethod m, Object... args) {
+ int[] out = new int[size];
+ Class> component = Specs.elementType(m.returnType());
+ for (int i = 0; i < size; i++) {
+ out[i] = define(component);
+ }
+ steps.add(new Step.Call(out, m, toArgs(args)));
+ return out;
+ }
+
+ public SpecBuilder post(int... cstrs) {
+ steps.add(new Step.Post(cstrs));
+ return this;
+ }
+
+ /**
+ * @return the identifier of the boolean variable reifying cstr
+ */
+ public int reify(int cstr) {
+ int id = define(BoolVar.class);
+ steps.add(new Step.Reify(cstr, id));
+ return id;
+ }
+
+ public SpecBuilder reifyWith(int cstr, int bool) {
+ return link(Step.Link.Kind.REIFY_WITH, cstr, bool);
+ }
+
+ public SpecBuilder implies(int cstr, int bool) {
+ return link(Step.Link.Kind.IMPLIES, cstr, bool);
+ }
+
+ public SpecBuilder impliedBy(int cstr, int bool) {
+ return link(Step.Link.Kind.IMPLIED_BY, cstr, bool);
+ }
+
+ private SpecBuilder link(Step.Link.Kind kind, int cstr, int bool) {
+ steps.add(new Step.Link(kind, cstr, bool));
+ return this;
+ }
+
+ public SpecBuilder unpost(int... cstrs) {
+ steps.add(new Step.Unpost(cstrs));
+ return this;
+ }
+
+ /**
+ * @return the identifier of the new task
+ */
+ public int newTask(int start, int duration, int end) {
+ int id = define(Task.class);
+ steps.add(new Step.NewTask(id, start, duration, end));
+ return id;
+ }
+
+ /**
+ * @return the identifier of the new optional task
+ */
+ public int newOptionalTask(int start, int duration, int end, int performed) {
+ int id = define(OptionalTask.class);
+ steps.add(new Step.NewTask(id, start, duration, end, performed));
+ return id;
+ }
+
+ /**
+ * @return the identifier of the custom constraint
+ */
+ public int custom(String name, Function factory) {
+ int id = define(Constraint.class);
+ steps.add(new Step.Custom(id, name, factory));
+ return id;
+ }
+
+ public ModelSpec build() {
+ return new ModelSpec(header, steps, search);
+ }
+
+ /**
+ * @return a new identifier, of static type type
+ */
+ private int define(Class> type) {
+ int id = nextId++;
+ types.put(id, type);
+ return id;
+ }
+
+ private static Arg[] toArgs(Object[] args) {
+ Arg[] res = new Arg[args.length];
+ for (int i = 0; i < args.length; i++) {
+ res[i] = args[i] instanceof Arg a ? a : new Arg.Const(args[i]);
+ }
+ return res;
+ }
+
+ /**
+ * Resolve the overload of name which accepts args , choosing the most specific one as javac would.
+ */
+ private FactoryMethod resolve(String name, Object[] args) {
+ List candidates = new ArrayList<>();
+ for (Method m : METHODS.getOrDefault(name, List.of())) {
+ if (m.getParameterCount() == args.length && accepts(m.getParameterTypes(), args)) {
+ candidates.add(m);
+ }
+ }
+ List mostSpecific = new ArrayList<>();
+ for (Method m : candidates) {
+ if (candidates.stream().allMatch(o -> moreSpecific(m, o))) {
+ mostSpecific.add(m);
+ }
+ }
+ if (mostSpecific.size() != 1) {
+ throw new SpecException((candidates.isEmpty() ? "No" : "Ambiguous") + " factory method " + name
+ + " for arguments " + Arrays.stream(args).map(this::describe).collect(Collectors.joining(", ", "(", ")"))
+ + (candidates.isEmpty() ? "" : ": " + candidates));
+ }
+ return FactoryMethod.of(mostSpecific.get(0));
+ }
+
+ private static boolean moreSpecific(Method m, Method o) {
+ Class>[] pm = m.getParameterTypes();
+ Class>[] po = o.getParameterTypes();
+ for (int i = 0; i < pm.length; i++) {
+ if (!po[i].isAssignableFrom(pm[i])) return false;
+ }
+ return true;
+ }
+
+ private boolean accepts(Class>[] params, Object[] args) {
+ for (int i = 0; i < params.length; i++) {
+ Class> t = typeOf(args[i]);
+ if (t == null ? params[i].isPrimitive() : !Specs.wrap(params[i]).isAssignableFrom(t)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @return the static type of an argument, null for a null constant
+ */
+ private Class> typeOf(Object arg) {
+ if (arg instanceof Arg.Ref r) {
+ Class> t = types.get(r.id());
+ if (t == null) {
+ throw new SpecException("Undefined identifier " + r.id());
+ }
+ return t;
+ } else if (arg instanceof Arg.RefArray a) {
+ return a.componentType().arrayType();
+ } else if (arg instanceof Arg.RefMatrix a) {
+ return a.componentType().arrayType().arrayType();
+ } else if (arg instanceof Arg.RefList) {
+ return List.class;
+ } else if (arg instanceof Arg.Logic) {
+ return LogOp.class;
+ } else if (arg instanceof Arg.Literal) {
+ return Literalizer.class;
+ } else if (arg instanceof Arg.Const c) {
+ return typeOf(c.rawValue());
+ }
+ return arg == null ? null : arg.getClass();
+ }
+
+ private String describe(Object arg) {
+ Class> t = typeOf(arg);
+ return t == null ? "null" : t.getSimpleName();
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/SpecException.java b/solver/src/main/java/org/chocosolver/solver/spec/SpecException.java
new file mode 100644
index 0000000000..c9a6057f75
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/SpecException.java
@@ -0,0 +1,26 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.exception.SolverException;
+
+/**
+ * Thrown when a {@link ModelSpec} is ill-formed or cannot be instantiated.
+ *
+ * @author Charles Prud'homme
+ */
+public class SpecException extends SolverException {
+
+ public SpecException(String message) {
+ super(message);
+ }
+
+ public SpecException(String message, Throwable cause) {
+ super(message);
+ initCause(cause);
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/SpecSolution.java b/solver/src/main/java/org/chocosolver/solver/spec/SpecSolution.java
new file mode 100644
index 0000000000..46b38fd6b2
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/SpecSolution.java
@@ -0,0 +1,102 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Solution;
+import org.chocosolver.solver.variables.IntVar;
+
+import java.util.Arrays;
+
+/**
+ * A solution expressed with the identifiers of a {@link ModelSpec}: found by one instance, it can be read by any
+ * other instance, or by the recorded model.
+ *
+ * Only integer (and boolean) variables are stored. Immutable, hence shareable among threads.
+ *
+ * @author Charles Prud'homme
+ */
+public final class SpecSolution {
+
+ /**
+ * Value of each identifier, meaningful only when stored.
+ */
+ private final int[] values;
+ private final boolean[] stored;
+
+ private SpecSolution(int[] values, boolean[] stored) {
+ this.values = values;
+ this.stored = stored;
+ }
+
+ /**
+ * Record the current values of the instantiated integer variables of an instance, typically when a solution
+ * is found.
+ *
+ * @param resolver the resolver of the instance
+ * @return the solution
+ */
+ public static SpecSolution record(Resolver resolver) {
+ int n = resolver.capacity();
+ int[] values = new int[n];
+ boolean[] stored = new boolean[n];
+ for (int id = 0; id < n; id++) {
+ if (resolver.isBound(id) && resolver.get(id) instanceof IntVar v && v.isInstantiated()) {
+ values[id] = v.getValue();
+ stored[id] = true;
+ }
+ }
+ return new SpecSolution(values, stored);
+ }
+
+ /**
+ * @param id an identifier
+ * @return true if this solution holds a value for id
+ */
+ public boolean contains(int id) {
+ return id >= 0 && id < stored.length && stored[id];
+ }
+
+ /**
+ * @param id identifier of an integer variable
+ * @return its value in this solution
+ * @throws SpecException if this solution holds no value for id
+ */
+ public int valueOf(int id) {
+ if (!contains(id)) {
+ throw new SpecException("No value for identifier " + id);
+ }
+ return values[id];
+ }
+
+ /**
+ * Express this solution in terms of the variables of a model which follows the spec (an instance, or the
+ * recorded model).
+ *
+ * @param resolver identifies the variables of the target model
+ * @return a solution of the target model
+ */
+ public Solution toSolution(Resolver resolver) {
+ Solution sol = new Solution(resolver.model());
+ for (int id = 0; id < stored.length; id++) {
+ if (stored[id] && resolver.isBound(id) && resolver.get(id) instanceof IntVar v) {
+ sol.setIntVal(v, values[id]);
+ }
+ }
+ return sol;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return this == o || (o instanceof SpecSolution s
+ && Arrays.equals(values, s.values) && Arrays.equals(stored, s.stored));
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * Arrays.hashCode(values) + Arrays.hashCode(stored);
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/Specs.java b/solver/src/main/java/org/chocosolver/solver/spec/Specs.java
new file mode 100644
index 0000000000..ed104d2645
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/Specs.java
@@ -0,0 +1,82 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.List;
+
+/**
+ * Helpers shared by the classes of this package.
+ *
+ * @author Charles Prud'homme
+ */
+final class Specs {
+
+ static final int[] NO_INT = new int[0];
+
+ /**
+ * Whether the instances of a class have no instance field.
+ */
+ private static final ClassValue STATELESS = new ClassValue<>() {
+ @Override
+ protected Boolean computeValue(Class> c) {
+ if (c.isArray()) {
+ return false;
+ }
+ for (Class> k = c; k != null; k = k.getSuperclass()) {
+ for (Field f : k.getDeclaredFields()) {
+ if (!Modifier.isStatic(f.getModifiers())) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ };
+
+ private Specs() {
+ }
+
+ /**
+ * @return true if the instances of c have no instance field, hence are immutable
+ */
+ static boolean isStateless(Class> c) {
+ return STATELESS.get(c);
+ }
+
+ /**
+ * @return the type of the elements of c , whatever the number of dimensions (c if not an array)
+ */
+ static Class> elementType(Class> c) {
+ while (c.isArray()) {
+ c = c.getComponentType();
+ }
+ return c;
+ }
+
+ /**
+ * Add the elements of array , row by row whatever the number of dimensions, to flat .
+ */
+ static void flatten(Object[] array, List flat) {
+ for (Object o : array) {
+ if (o instanceof Object[] sub) {
+ flatten(sub, flat);
+ } else {
+ flat.add(o);
+ }
+ }
+ }
+
+ /**
+ * @return the wrapper class of c if primitive, c otherwise
+ */
+ static Class> wrap(Class> c) {
+ return c.isPrimitive() ? MethodType.methodType(c).wrap().returnType() : c;
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/Step.java b/solver/src/main/java/org/chocosolver/solver/spec/Step.java
new file mode 100644
index 0000000000..48ec7ce8d4
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/Step.java
@@ -0,0 +1,300 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.variables.OptionalTask;
+import org.chocosolver.solver.variables.Task;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.IntConsumer;
+
+/**
+ * A step of a {@link ModelSpec}. Steps are replayed in order.
+ *
+ * A step may define identifiers (its outputs) and use identifiers defined by previous steps.
+ * An identifier may be defined more than once, provided that the replay binds it to the same object each time
+ * (e.g., a cached constant).
+ *
+ * Steps are immutable: public accessors return copies, the package reads the arrays directly.
+ *
+ * @author Charles Prud'homme
+ */
+public sealed interface Step permits Step.Call, Step.Post, Step.Reify, Step.Link, Step.Unpost, Step.NewTask,
+ Step.Custom, Step.Hook, Step.Seed, Step.Objective, Step.Group {
+
+ /**
+ * Apply action on each identifier this step uses.
+ */
+ default void forEachUse(IntConsumer action) {
+ }
+
+ /**
+ * Apply action on each identifier this step defines.
+ */
+ default void forEachDef(IntConsumer action) {
+ }
+
+ /**
+ * A call to a factory method of the model.
+ *
+ * @param out identifiers of the outputs: empty if the result is ignored, one identifier for an object,
+ * one identifier per element for an array (row by row for a matrix), -1 to skip an element
+ * @param method the factory method
+ * @param args the arguments, one per parameter of method
+ */
+ record Call(int[] out, FactoryMethod method, Arg[] args) implements Step {
+ public Call {
+ Objects.requireNonNull(method);
+ out = out.clone();
+ args = args.clone();
+ if (args.length != method.parameterCount()) {
+ throw new SpecException(method + " expects " + method.parameterCount()
+ + " arguments, got " + args.length);
+ }
+ if (out.length > 0 && method.returnType() == void.class) {
+ throw new SpecException(method + " returns nothing");
+ }
+ }
+
+ @Override
+ public int[] out() {
+ return out.clone();
+ }
+
+ @Override
+ public Arg[] args() {
+ return args.clone();
+ }
+
+ int[] rawOut() {
+ return out;
+ }
+
+ Arg[] rawArgs() {
+ return args;
+ }
+
+ @Override
+ public void forEachUse(IntConsumer action) {
+ for (Arg a : args) a.forEachRef(action);
+ }
+
+ @Override
+ public void forEachDef(IntConsumer action) {
+ for (int o : out) if (o >= 0) action.accept(o);
+ }
+
+ @Override
+ public String toString() {
+ return Arrays.toString(out) + " = " + method.name() + Arrays.toString(args);
+ }
+ }
+
+ /**
+ * Post constraints, as {@link org.chocosolver.solver.Model#post(Constraint...)}.
+ */
+ record Post(int[] cstrs) implements Step {
+ public Post {
+ cstrs = cstrs.clone();
+ }
+
+ @Override
+ public int[] cstrs() {
+ return cstrs.clone();
+ }
+
+ int[] rawCstrs() {
+ return cstrs;
+ }
+
+ @Override
+ public void forEachUse(IntConsumer action) {
+ for (int c : cstrs) action.accept(c);
+ }
+
+ @Override
+ public String toString() {
+ return "post" + Arrays.toString(cstrs);
+ }
+ }
+
+ /**
+ * Reify a constraint, as {@link Constraint#reify()}, and bind the resulting boolean variable to out .
+ */
+ record Reify(int cstr, int out) implements Step {
+ @Override
+ public void forEachUse(IntConsumer action) {
+ action.accept(cstr);
+ }
+
+ @Override
+ public void forEachDef(IntConsumer action) {
+ action.accept(out);
+ }
+ }
+
+ /**
+ * Link a constraint to a boolean variable: {@link Constraint#reifyWith}, {@link Constraint#implies} or
+ * {@link Constraint#impliedBy}.
+ */
+ record Link(Kind kind, int cstr, int bool) implements Step {
+
+ /**
+ * The kinds of links between a constraint and a boolean variable.
+ */
+ public enum Kind {
+ REIFY_WITH, IMPLIES, IMPLIED_BY
+ }
+
+ public Link {
+ Objects.requireNonNull(kind);
+ }
+
+ @Override
+ public void forEachUse(IntConsumer action) {
+ action.accept(cstr);
+ action.accept(bool);
+ }
+ }
+
+ /**
+ * Unpost constraints, as {@link org.chocosolver.solver.Model#unpost(Constraint...)}.
+ */
+ record Unpost(int[] cstrs) implements Step {
+ public Unpost {
+ cstrs = cstrs.clone();
+ }
+
+ @Override
+ public int[] cstrs() {
+ return cstrs.clone();
+ }
+
+ int[] rawCstrs() {
+ return cstrs;
+ }
+
+ @Override
+ public void forEachUse(IntConsumer action) {
+ for (int c : cstrs) action.accept(c);
+ }
+
+ @Override
+ public String toString() {
+ return "unpost" + Arrays.toString(cstrs);
+ }
+ }
+
+ /**
+ * Create a {@link Task} (or an {@link OptionalTask} when performed is not -1) with its canonical
+ * constructor, from existing variables.
+ *
+ * @param out identifier of the task
+ * @param start identifier of the start variable
+ * @param duration identifier of the duration variable
+ * @param end identifier of the end variable
+ * @param performed identifier of the boolean variable of an optional task, -1 for a mandatory task
+ */
+ record NewTask(int out, int start, int duration, int end, int performed) implements Step {
+
+ public NewTask(int out, int start, int duration, int end) {
+ this(out, start, duration, end, -1);
+ }
+
+ @Override
+ public void forEachUse(IntConsumer action) {
+ action.accept(start);
+ action.accept(duration);
+ action.accept(end);
+ if (performed >= 0) action.accept(performed);
+ }
+
+ @Override
+ public void forEachDef(IntConsumer action) {
+ action.accept(out);
+ }
+ }
+
+ /**
+ * A custom constraint, built by factory on the model under instantiation, for specs built by hand
+ * ({@link SpecBuilder#custom}) or extended by a variant ({@link Variant#extend}); a recorded model journalizes
+ * its custom constraints as calls to {@code IConstraintFactory#custom}.
+ * The factory must only use objects obtained from the {@link Resolver}.
+ * The constraint is neither posted nor reified: use subsequent steps.
+ *
+ * @param out identifier of the constraint, -1 to ignore it
+ * @param name name of the custom constraint (for error messages)
+ * @param factory builds the constraint; must be stateless as it may be called concurrently
+ */
+ record Custom(int out, String name, Function factory) implements Step {
+ public Custom {
+ Objects.requireNonNull(name);
+ Objects.requireNonNull(factory);
+ }
+
+ @Override
+ public void forEachDef(IntConsumer action) {
+ if (out >= 0) action.accept(out);
+ }
+ }
+
+ /**
+ * Add a hook to the model, as {@link org.chocosolver.solver.Model#addHook(String, Object)}, or remove it
+ * when value is a null constant; remove all the hooks, as
+ * {@link org.chocosolver.solver.Model#removeAllHooks()}, when name is null.
+ */
+ record Hook(String name, Arg value) implements Step {
+ public Hook {
+ Objects.requireNonNull(value);
+ if (name == null && !(value instanceof Arg.Const c && c.value() == null)) {
+ throw new IllegalArgumentException("removing all the hooks takes no value");
+ }
+ }
+
+ @Override
+ public void forEachUse(IntConsumer action) {
+ value.forEachRef(action);
+ }
+ }
+
+ /**
+ * Set the seed of the model, as {@link org.chocosolver.solver.Model#setSeed(long)}.
+ */
+ record Seed(long seed) implements Step {
+ }
+
+ /**
+ * Set the objective of the model, as {@link org.chocosolver.solver.Model#setObjective(boolean,
+ * org.chocosolver.solver.variables.Variable)}, or clear it when var is -1.
+ */
+ record Objective(boolean maximize, int var) implements Step {
+ @Override
+ public void forEachUse(IntConsumer action) {
+ if (var >= 0) action.accept(var);
+ }
+ }
+
+ /**
+ * Declare a group of variables, as
+ * {@link org.chocosolver.solver.Model#addGroup(org.chocosolver.solver.variables.Group)}.
+ * Groups are used by some search strategies to select the decision variables.
+ */
+ record Group(String name, Arg.RefArray vars) implements Step {
+ public Group {
+ Objects.requireNonNull(name);
+ Objects.requireNonNull(vars);
+ }
+
+ @Override
+ public void forEachUse(IntConsumer action) {
+ vars.forEachRef(action);
+ }
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/Values.java b/solver/src/main/java/org/chocosolver/solver/spec/Values.java
new file mode 100644
index 0000000000..2c31f0cf1e
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/Values.java
@@ -0,0 +1,168 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.constraints.extension.Tuples;
+import org.chocosolver.solver.constraints.extension.hybrid.HybridTuples;
+import org.chocosolver.solver.constraints.nary.automata.FA.FiniteAutomaton;
+import org.chocosolver.util.objects.graphs.MultivaluedDecisionDiagram;
+import org.chocosolver.util.objects.setDataStructures.iterable.IntIterableRangeSet;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.RecordComponent;
+import java.util.function.UnaryOperator;
+
+/**
+ * Sharing policy of the constant arguments of a {@link ModelSpec}: the single place which decides what is shared by
+ * the instances of a spec.
+ *
+ * immutable values (boxed primitives, strings, enums) are shared;
+ * heavy read-only data ({@link Tuples}, {@link HybridTuples}, {@link MultivaluedDecisionDiagram}) are frozen on
+ * capture, then shared;
+ * objects without instance field (e.g., non-capturing lambdas, stateless singletons such as
+ * {@code Condition.EXCEPT_0}) cannot capture anything: they are shared;
+ * arrays (of any dimension, whose elements are supported) and {@link IntIterableRangeSet} are copied on capture
+ * and on each instantiation;
+ * automata ({@link FiniteAutomaton} and subclasses) are not thread-safe: copied on capture and on each
+ * instantiation;
+ * records whose components are supported: shared if all their components are, otherwise rebuilt with
+ * their canonical constructor from copied components.
+ *
+ * Any other type is rejected, in particular capturing lambdas (they may capture objects of the recorded model).
+ *
+ * @author Charles Prud'homme
+ */
+final class Values {
+
+ private Values() {
+ }
+
+ /**
+ * Capture a constant when building a spec, detaching it from the caller.
+ *
+ * @param value a constant
+ * @return the value to store in the spec
+ * @throws SpecException if the type of value is not supported
+ */
+ static Object capture(Object value) {
+ if (value instanceof Tuples t) {
+ return t.freeze();
+ }
+ if (value instanceof HybridTuples t) {
+ return t.freeze();
+ }
+ if (value instanceof MultivaluedDecisionDiagram mdd) {
+ return mdd.freeze();
+ }
+ return isShared(value) ? value : copy(value, Values::capture);
+ }
+
+ /**
+ * @param value a captured constant
+ * @return the value to pass to a model under instantiation
+ */
+ static Object materialize(Object value) {
+ return isShared(value) ? value : copy(value, Values::materialize);
+ }
+
+ /**
+ * @return true if value can be shared by models solved concurrently
+ */
+ static boolean isShared(Object value) {
+ return value == null
+ || value instanceof Number
+ || value instanceof Boolean
+ || value instanceof Character
+ || value instanceof String
+ || value instanceof Enum
+ || (value instanceof Tuples t && t.isFrozen())
+ || (value instanceof HybridTuples t && t.isFrozen())
+ || (value instanceof MultivaluedDecisionDiagram mdd && mdd.isFrozen())
+ || Specs.isStateless(value.getClass())
+ || (value instanceof Record r && allShared(r));
+ }
+
+ private static boolean allShared(Record r) {
+ try {
+ for (RecordComponent rc : r.getClass().getRecordComponents()) {
+ if (!isShared(component(r, rc))) {
+ return false;
+ }
+ }
+ return true;
+ } catch (SpecException e) {
+ // a record which cannot be read is not shared: copying it reports the problem
+ return false;
+ }
+ }
+
+ private static Object component(Record r, RecordComponent rc) {
+ try {
+ rc.getAccessor().setAccessible(true);
+ return rc.getAccessor().invoke(r);
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ throw new SpecException("Cannot read the component " + rc.getName() + " of " + r.getClass().getName()
+ + " (is its package open to org.chocosolver.solver?)", e);
+ }
+ }
+
+ /**
+ * @param elements copies the elements of an array or the components of a record
+ */
+ private static Object copy(Object value, UnaryOperator elements) {
+ Class> c = value.getClass();
+ if (c.isArray()) {
+ int n = Array.getLength(value);
+ Object dst = Array.newInstance(c.getComponentType(), n);
+ if (c.getComponentType().isPrimitive()) {
+ System.arraycopy(value, 0, dst, 0, n);
+ } else {
+ Object[] src = (Object[]) value;
+ Object[] d = (Object[]) dst;
+ for (int i = 0; i < n; i++) {
+ d[i] = elements.apply(src[i]);
+ }
+ }
+ return dst;
+ }
+ if (value instanceof IntIterableRangeSet set) {
+ return set.duplicate();
+ }
+ if (value instanceof FiniteAutomaton fa) {
+ // reading an automaton may lazily modify its internal (dk.brics) state
+ synchronized (fa) {
+ try {
+ return fa.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new SpecException("Cannot copy automaton", e);
+ }
+ }
+ }
+ if (value instanceof Record r) {
+ RecordComponent[] rcs = c.getRecordComponents();
+ Object[] args = new Object[rcs.length];
+ Class>[] types = new Class>[rcs.length];
+ for (int i = 0; i < rcs.length; i++) {
+ args[i] = elements.apply(component(r, rcs[i]));
+ types[i] = rcs[i].getType();
+ }
+ try {
+ Constructor> canonical = c.getDeclaredConstructor(types);
+ canonical.setAccessible(true);
+ return canonical.newInstance(args);
+ } catch (ReflectiveOperationException | RuntimeException e) {
+ throw new SpecException("Cannot copy the record " + c.getName(), e);
+ }
+ }
+ if (c.isSynthetic() || c.isHidden()) {
+ throw new SpecException("Unsupported capturing lambda " + c.getName()
+ + ": it may capture objects of the recorded model; use a non-capturing lambda");
+ }
+ throw new SpecException("Unsupported constant of type " + c.getName());
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/spec/Variant.java b/solver/src/main/java/org/chocosolver/solver/spec/Variant.java
new file mode 100644
index 0000000000..d095e159a3
--- /dev/null
+++ b/solver/src/main/java/org/chocosolver/solver/spec/Variant.java
@@ -0,0 +1,213 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Settings;
+import org.chocosolver.solver.constraints.extension.Tuples;
+import org.chocosolver.solver.variables.IntVar;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+
+/**
+ * A variant of a {@link ModelSpec}: a rewriting of the spec, applied once, whose result is instantiated by as many
+ * workers as needed (see {@link ModelSpec#with(Variant)}).
+ *
+ * A variant may change:
+ *
+ * the header: {@link #seed(long)}, {@link #settings(Settings)};
+ * the calls to the factories, e.g. the consistency of {@code allDifferent} ({@link #allDifferent(String)})
+ * or the algorithm of {@code table} ({@link #tableAlgorithm(String)}), or any call ({@link #mapCalls});
+ * the constraints: redundant constraints or symmetry breaking ({@link #extend(Consumer)}), removals
+ * ({@link #unpostIf(Predicate)});
+ * the search ({@link #search(SearchDecl)}).
+ *
+ * Variants compose with {@link #andThen(Variant)}.
+ *
+ * @author Charles Prud'homme
+ */
+@FunctionalInterface
+public interface Variant {
+
+ /**
+ * The variant which changes nothing: a carbon copy.
+ */
+ Variant IDENTITY = spec -> spec;
+
+ /**
+ * @param spec a spec
+ * @return the rewritten spec
+ */
+ ModelSpec apply(ModelSpec spec);
+
+ /**
+ * @return a variant which applies this one, then next
+ */
+ default Variant andThen(Variant next) {
+ Objects.requireNonNull(next);
+ return spec -> next.apply(apply(spec));
+ }
+
+ //***********************************************************************************
+ // HEADER AND SEARCH
+ //***********************************************************************************
+
+ /**
+ * Change the seed: propagators drawing random numbers and randomized search strategies behave differently.
+ * Note that it may also change the encoding of expressions which are sampled ({@code ReExpression#embody}).
+ * The seeds set during the construction ({@link Step.Seed}) are removed, otherwise they would override this one.
+ */
+ static Variant seed(long seed) {
+ return spec -> spec.withHeader(spec.header().withSeed(seed))
+ .withSteps(spec.steps().stream().filter(s -> !(s instanceof Step.Seed)).toList());
+ }
+
+ /**
+ * Change the settings (e.g., enable views or LCG), which may change the way constraints are built.
+ */
+ static Variant settings(Settings settings) {
+ Objects.requireNonNull(settings);
+ return spec -> spec.withHeader(spec.header().withSettings(settings));
+ }
+
+ /**
+ * Change the search.
+ */
+ static Variant search(SearchDecl search) {
+ return spec -> spec.withSearch(search);
+ }
+
+ //***********************************************************************************
+ // CALLS
+ //***********************************************************************************
+
+ /**
+ * Rewrite each call to a factory method.
+ *
+ * @param rewriter returns the call to replay instead (possibly the same), which must define the same
+ * identifiers
+ */
+ static Variant mapCalls(UnaryOperator rewriter) {
+ Objects.requireNonNull(rewriter);
+ return spec -> {
+ List steps = new ArrayList<>(spec.steps().size());
+ for (Step s : spec.steps()) {
+ steps.add(s instanceof Step.Call c ? rewriter.apply(c) : s);
+ }
+ return spec.withSteps(steps);
+ };
+ }
+
+ /**
+ * Set the consistency of every {@code allDifferent} constraint.
+ *
+ * @param consistency among "DEFAULT", "AC", "AC_REGIN", "AC_ZHANG", "BC", "FC", "NEQS"
+ */
+ static Variant allDifferent(String consistency) {
+ return withOption("allDifferent", consistency, IntVar[].class);
+ }
+
+ /**
+ * Set the algorithm of every table constraint of arity greater than 2.
+ *
+ * @param algo e.g. "CT+", "STR2+", "GAC3rm", "MDD+", "FC" (see {@code IIntConstraintFactory#table})
+ */
+ static Variant tableAlgorithm(String algo) {
+ return withOption("table", algo, IntVar[].class, Tuples.class);
+ }
+
+ /**
+ * Set the algorithm of every binary table constraint.
+ *
+ * @param algo e.g. "CT+", "STR2+", "AC3rm", "AC2001", "FC" (see {@code IIntConstraintFactory#table})
+ */
+ static Variant binaryTableAlgorithm(String algo) {
+ return withOption("table", algo, IntVar.class, IntVar.class, Tuples.class);
+ }
+
+ /**
+ * Rewrite the calls to a factory method which exists with and without a trailing {@code String} option (e.g.
+ * {@code allDifferent(IntVar[])} and {@code allDifferent(IntVar[], String)}) so that they all use
+ * option .
+ */
+ private static Variant withOption(String name, String option, Class>... base) {
+ Objects.requireNonNull(option);
+ FactoryMethod without = FactoryMethod.of(name, base);
+ Class>[] withTypes = Arrays.copyOf(base, base.length + 1);
+ withTypes[base.length] = String.class;
+ FactoryMethod with = FactoryMethod.of(name, withTypes);
+ Arg opt = new Arg.Const(option);
+ return mapCalls(c -> {
+ boolean hasOption = c.method().equals(with);
+ if (!hasOption && !c.method().equals(without)) {
+ return c;
+ }
+ Arg[] args = c.rawArgs();
+ Arg[] a = Arrays.copyOf(args, hasOption ? args.length : args.length + 1);
+ a[a.length - 1] = opt;
+ return new Step.Call(c.rawOut(), with, a);
+ });
+ }
+
+ //***********************************************************************************
+ // CONSTRAINTS
+ //***********************************************************************************
+
+ /**
+ * Add steps at the end of the spec, e.g. redundant constraints or symmetry breaking constraints.
+ * The builder knows the identifiers of the spec (see {@link Recorder#idsOf} to get those of the variables of a
+ * recorded model).
+ */
+ static Variant extend(Consumer extension) {
+ Objects.requireNonNull(extension);
+ return spec -> {
+ SpecBuilder b = SpecBuilder.from(spec);
+ extension.accept(b);
+ return b.build();
+ };
+ }
+
+ /**
+ * Do not post the constraints built by the calls which satisfy filter (the calls are still replayed,
+ * with their side effects, e.g. intermediate variables).
+ */
+ static Variant unpostIf(Predicate filter) {
+ Objects.requireNonNull(filter);
+ return spec -> {
+ BitSet removed = new BitSet();
+ for (Step s : spec.steps()) {
+ if (s instanceof Step.Call c && filter.test(c)) {
+ s.forEachDef(removed::set);
+ }
+ }
+ List steps = new ArrayList<>(spec.steps().size());
+ for (Step s : spec.steps()) {
+ // a constraint no longer posted cannot be unposted either
+ if (s instanceof Step.Post p) {
+ int[] kept = Arrays.stream(p.rawCstrs()).filter(c -> !removed.get(c)).toArray();
+ if (kept.length > 0) {
+ steps.add(new Step.Post(kept));
+ }
+ } else if (s instanceof Step.Unpost u) {
+ int[] kept = Arrays.stream(u.rawCstrs()).filter(c -> !removed.get(c)).toArray();
+ if (kept.length > 0) {
+ steps.add(new Step.Unpost(kept));
+ }
+ } else {
+ steps.add(s);
+ }
+ }
+ return spec.withSteps(steps);
+ };
+ }
+}
diff --git a/solver/src/main/java/org/chocosolver/solver/variables/OptionalTask.java b/solver/src/main/java/org/chocosolver/solver/variables/OptionalTask.java
index 78b161f5a7..ac9ee35efb 100644
--- a/solver/src/main/java/org/chocosolver/solver/variables/OptionalTask.java
+++ b/solver/src/main/java/org/chocosolver/solver/variables/OptionalTask.java
@@ -10,6 +10,7 @@
import org.chocosolver.solver.ICause;
import org.chocosolver.solver.Model;
import org.chocosolver.solver.exception.ContradictionException;
+import org.chocosolver.solver.spec.Recorder;
/**
* Container representing an optional task:
@@ -58,8 +59,11 @@ public OptionalTask(Model model, int est, int lst, int d, int ect, int lct) {
* @param performed performed variable
*/
public OptionalTask(Model model, int est, int lst, int d, int ect, int lct, BoolVar performed) {
- super(model, est, lst, d, ect, lct);
- this.performed = performed;
+ this(buildVars(model, est, lst, d, ect, lct), performed);
+ }
+
+ private OptionalTask(IntVar[] vars, BoolVar performed) {
+ this(vars[0], vars[1], vars[2], performed);
}
/**
@@ -82,8 +86,7 @@ public OptionalTask(IntVar s, int d) {
* @param performed performed variable
*/
public OptionalTask(IntVar s, int d, BoolVar performed) {
- super(s, d);
- this.performed = performed;
+ this(s, s.getModel().intVar(d), s.getModel().offset(s, d), performed);
}
/**
@@ -106,8 +109,7 @@ public OptionalTask(IntVar s, IntVar d) {
* @param performed performed variable
*/
public OptionalTask(IntVar s, IntVar d, BoolVar performed) {
- super(s, d);
- this.performed = performed;
+ this(s, d, endOf(s, d), performed);
}
/**
@@ -132,8 +134,7 @@ public OptionalTask(IntVar s, int d, IntVar e) {
* @param performed performed variable
*/
public OptionalTask(IntVar s, int d, IntVar e, BoolVar performed) {
- super(s, d, e);
- this.performed = performed;
+ this(s, s.getModel().intVar(d), e, performed);
}
/**
@@ -160,6 +161,13 @@ public OptionalTask(IntVar s, IntVar d, IntVar e) {
public OptionalTask(IntVar s, IntVar d, IntVar e, BoolVar performed) {
super(s, d, e);
this.performed = performed;
+ // every constructor ends here: journalize the creation of this task, if its model is recorded
+ if (getClass() == OptionalTask.class) {
+ Recorder r = s.getModel().getRecorder();
+ if (r != null) {
+ r.newTask(this, s, d, e, performed);
+ }
+ }
}
//***********************************************************************************
diff --git a/solver/src/main/java/org/chocosolver/solver/variables/Task.java b/solver/src/main/java/org/chocosolver/solver/variables/Task.java
index 3f85ec41b2..02204fcb57 100644
--- a/solver/src/main/java/org/chocosolver/solver/variables/Task.java
+++ b/solver/src/main/java/org/chocosolver/solver/variables/Task.java
@@ -14,6 +14,7 @@
import org.chocosolver.solver.constraints.Propagator;
import org.chocosolver.solver.constraints.PropagatorPriority;
import org.chocosolver.solver.exception.ContradictionException;
+import org.chocosolver.solver.spec.Recorder;
import org.chocosolver.solver.variables.events.PropagatorEventType;
import org.chocosolver.solver.variables.view.integer.IntAffineView;
import org.chocosolver.util.ESat;
@@ -61,7 +62,7 @@ public Task(Model model, int est, int lst, int d, int ect, int lct) {
this(buildVars(model, est, lst, d, ect, lct));
}
- private static IntVar[] buildVars(Model model, int est, int lst, int d, int ect, int lct) {
+ static IntVar[] buildVars(Model model, int est, int lst, int d, int ect, int lct) {
IntVar start = model.intVar(Math.max(est, ect - d), Math.min(lst, lct - d));
IntVar duration = model.intVar(d);
IntVar end = start.getModel().offset(start, d);
@@ -87,12 +88,15 @@ public Task(IntVar s, int d) {
* @param d duration value
*/
public Task(IntVar s, IntVar d) {
- this(
- s,
- d,
- d.isInstantiated() ? s.getModel().offset(s, d.getValue())
- : s.getModel().intVar(s.getLB() + d.getLB(), s.getUB() + d.getUB())
- );
+ this(s, d, endOf(s, d));
+ }
+
+ /**
+ * @return the end variable of a task of start s and duration d
+ */
+ static IntVar endOf(IntVar s, IntVar d) {
+ return d.isInstantiated() ? s.getModel().offset(s, d.getValue())
+ : s.getModel().intVar(s.getLB() + d.getLB(), s.getUB() + d.getUB());
}
/**
@@ -124,6 +128,12 @@ public Task(IntVar s, IntVar d, IntVar e) {
setActive();
setPassive();
}
+ if (getClass() == Task.class) {
+ Recorder r = s.getModel().getRecorder();
+ if (r != null) {
+ r.newTask(this, s, d, e, null);
+ }
+ }
}
public void post() {
diff --git a/solver/src/main/java/org/chocosolver/util/objects/graphs/MultivaluedDecisionDiagram.java b/solver/src/main/java/org/chocosolver/util/objects/graphs/MultivaluedDecisionDiagram.java
index 36c3c458f5..1843126adb 100644
--- a/solver/src/main/java/org/chocosolver/util/objects/graphs/MultivaluedDecisionDiagram.java
+++ b/solver/src/main/java/org/chocosolver/util/objects/graphs/MultivaluedDecisionDiagram.java
@@ -9,6 +9,7 @@
import gnu.trove.list.array.TIntArrayList;
import gnu.trove.map.hash.TIntIntHashMap;
import org.chocosolver.solver.constraints.extension.Tuples;
+import org.chocosolver.solver.exception.SolverException;
import org.chocosolver.solver.variables.IntVar;
import java.util.ArrayList;
@@ -80,6 +81,10 @@ public enum Compact {
private final Compact compact;
private final boolean sortTuples;
+ /**
+ * Once frozen, the diagram cannot be modified anymore and can be safely shared among models.
+ */
+ private volatile boolean frozen;
// TEMPORARY DATA STRUCTURE, PREFIX WITH "_", CLEARED AFTER USAGE
private TIntIntHashMap _nodesToRemove; // store the nodes to remove and the size of each node
@@ -208,6 +213,26 @@ private void init(Tuples TUPLES) {
}
}
+ /**
+ * Freeze this diagram: any further modification will throw a {@link SolverException}.
+ * A frozen diagram can be safely shared among models solved concurrently.
+ * Freezing is irreversible and idempotent.
+ *
+ * @return this
+ */
+ public MultivaluedDecisionDiagram freeze() {
+ this.frozen = true;
+ return this;
+ }
+
+ /**
+ * @return true if this diagram is frozen
+ * @see #freeze()
+ */
+ public boolean isFrozen() {
+ return frozen;
+ }
+
/**
* Add all tuples within the MDD
*
@@ -228,6 +253,9 @@ public boolean addTuples(Tuples TUPLES) {
* @param TUPLE tuple to add
*/
public boolean addTuple(int[] TUPLE) {
+ if (frozen) {
+ throw new SolverException("Cannot modify a frozen diagram");
+ }
for (int i = 0; i < nbLayers; i++) {
// if the tuple is out of declared domain
if (TUPLE[i] < offsets[i] || TUPLE[i] >= offsets[i] + sizes[i]) {
diff --git a/solver/src/test/java/org/chocosolver/solver/spec/CustomConstraintTest.java b/solver/src/test/java/org/chocosolver/solver/spec/CustomConstraintTest.java
new file mode 100644
index 0000000000..b74a4aad20
--- /dev/null
+++ b/solver/src/test/java/org/chocosolver/solver/spec/CustomConstraintTest.java
@@ -0,0 +1,119 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.constraints.Propagator;
+import org.chocosolver.solver.exception.ContradictionException;
+import org.chocosolver.solver.variables.IntVar;
+import org.chocosolver.util.ESat;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.util.Arrays;
+
+/**
+ * Tests of the custom constraints ({@code Model.custom}) built with parameters.
+ *
+ * @author Charles Prud'homme
+ */
+public class CustomConstraintTest {
+
+ /**
+ * A custom propagator with parameters: each variable is at most its bound.
+ */
+ static final class PropUpperBounds extends Propagator {
+ final int[] bounds;
+
+ PropUpperBounds(IntVar[] vars, int[] bounds) {
+ super(vars);
+ this.bounds = bounds;
+ }
+
+ @Override
+ public void propagate(int evtmask) throws ContradictionException {
+ for (int i = 0; i < vars.length; i++) {
+ vars[i].updateUpperBound(bounds[i], this);
+ }
+ }
+
+ @Override
+ public ESat isEntailed() {
+ boolean all = true;
+ for (int i = 0; i < vars.length; i++) {
+ if (vars[i].getLB() > bounds[i]) {
+ return ESat.FALSE;
+ }
+ all &= vars[i].getUB() <= bounds[i];
+ }
+ return all ? ESat.TRUE : ESat.UNDEFINED;
+ }
+ }
+
+ /**
+ * Several parameters, in a record.
+ */
+ record Bounds(int[] ub, int shift) {
+ }
+
+ /**
+ * A record which holds a variable: not supported, variables must be passed in {@code vars}.
+ */
+ record WithVar(IntVar var) {
+ }
+
+ private static PropUpperBounds of(IntVar[] vs, Bounds b) {
+ return new PropUpperBounds(vs, Arrays.stream(b.ub()).map(u -> u + b.shift()).toArray());
+ }
+
+ private static Model build(Model m) {
+ IntVar[] x = m.intVarArray("x", 3, 0, 9);
+ m.custom("int", x, 7, (vs, k) -> new PropUpperBounds(vs, new int[]{k, k, k})).post();
+ m.custom("array", x, new int[]{6, 5, 8}, PropUpperBounds::new).post();
+ m.custom("record", x, new Bounds(new int[]{3, 7, 7}, 1), CustomConstraintTest::of).post();
+ // data holding variables: mapped to those of each copy
+ m.custom("vars", new IntVar[]{x[0]}, new IntVar[]{x[1], x[2]},
+ (vs, ws) -> new PropUpperBounds(new IntVar[]{vs[0], ws[0], ws[1]}, new int[]{9, 9, 9})).post();
+ m.sum(x, "=", 14).post();
+ return m;
+ }
+
+ private static int[] bounds(Model m, String name) {
+ Constraint c = Arrays.stream(m.getCstrs()).filter(k -> k.getName().equals(name)).findFirst().orElseThrow();
+ return ((PropUpperBounds) c.getPropagator(0)).bounds;
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testParametersAreReplayed() {
+ String expected = SpecTestUtils.structure(build(new Model("m")));
+ String stats = SpecTestUtils.solve(build(new Model("m")), 100_000);
+ Model original = build(Model.record("m"));
+ ModelSpec spec = original.getRecorder().snapshot();
+ Model a = spec.instantiate().model();
+ Model b = spec.instantiate().model();
+ for (Model m : new Model[]{a, b}) {
+ Assert.assertEquals(SpecTestUtils.structure(m), expected);
+ Assert.assertEquals(bounds(m, "int"), new int[]{7, 7, 7});
+ Assert.assertEquals(bounds(m, "array"), new int[]{6, 5, 8});
+ Assert.assertEquals(bounds(m, "record"), new int[]{4, 8, 8});
+ }
+ // mutable data are not shared
+ Assert.assertNotSame(bounds(a, "array"), bounds(original, "array"));
+ Assert.assertNotSame(bounds(a, "array"), bounds(b, "array"));
+ Assert.assertEquals(SpecTestUtils.solve(a, 100_000), stats);
+ Assert.assertEquals(SpecTestUtils.solve(original, 100_000), stats);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testRecordOfVariablesIsReported() {
+ Model m = Model.record("m");
+ IntVar[] x = m.intVarArray("x", 2, 0, 3);
+ m.custom("withVar", x, new WithVar(x[0]), (vs, w) -> new PropUpperBounds(vs, new int[]{3, 3})).post();
+ Assert.assertThrows(SpecException.class, m::duplicate);
+ }
+}
diff --git a/solver/src/test/java/org/chocosolver/solver/spec/IntFactoryCoverageTest.java b/solver/src/test/java/org/chocosolver/solver/spec/IntFactoryCoverageTest.java
new file mode 100644
index 0000000000..b2ce78bea4
--- /dev/null
+++ b/solver/src/test/java/org/chocosolver/solver/spec/IntFactoryCoverageTest.java
@@ -0,0 +1,683 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.IModel;
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.Settings;
+import org.chocosolver.solver.SettingsBuilder;
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.constraints.binary.PropGreaterOrEqualX_Y;
+import org.chocosolver.solver.constraints.ISchedulingFactory;
+import org.chocosolver.solver.constraints.extension.Tuples;
+import org.chocosolver.solver.constraints.extension.hybrid.HybridTuples;
+import org.chocosolver.solver.constraints.nary.alldifferent.conditions.Condition;
+import org.chocosolver.solver.constraints.nary.automata.FA.CostAutomaton;
+import org.chocosolver.solver.constraints.nary.automata.FA.FiniteAutomaton;
+import org.chocosolver.solver.constraints.nary.circuit.CircuitConf;
+import org.chocosolver.solver.constraints.nary.cnf.LogOp;
+import org.chocosolver.solver.variables.BoolVar;
+import org.chocosolver.solver.variables.IntVar;
+import org.chocosolver.solver.variables.Task;
+import org.chocosolver.solver.variables.Variable;
+import org.chocosolver.util.objects.graphs.MultivaluedDecisionDiagram;
+import org.chocosolver.util.objects.setDataStructures.iterable.IntIterableRangeSet;
+import org.testng.Assert;
+import org.testng.SkipException;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Exhaustive check that the factory methods of {@link IModel} dealing only with integer and boolean variables
+ * (variables, views, constraints, reifications, clauses, decompositions, tasks) are supported by
+ * {@link ModelSpec}: each one is called by a case, and each case built directly, recorded then replayed gives
+ * the same structure and the same search (all solutions). Constraints are also checked reified.
+ *
+ * {@link #testEveryIntFactoryMethodIsCovered()} fails if a factory method of the scope is called by no case: add
+ * a case when adding a factory method.
+ *
+ * @author Charles Prud'homme
+ */
+public class IntFactoryCoverageTest {
+
+ /**
+ * Factory methods not implemented by the solver (they always throw an exception).
+ */
+ private static final Set NOT_IMPLEMENTED = Set.of("addTable(IntVar[],HybridTuples)");
+
+ /**
+ * Types out of the scope (set, real and graph variables and constraints).
+ */
+ private static final List OUT_OF_SCOPE = List.of("SetVar", "RealVar", "GraphVar", "DirectedGraph",
+ "UndirectedGraph", "ISet", "IGraph", "RealConstraint", "NodeInduced");
+
+ private record Case(String name, Consumer body) {
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ private static final List CASES = new ArrayList<>();
+
+ /**
+ * A case which posts the constraint, and a case which reifies it.
+ */
+ private static void c(String name, Function f) {
+ CASES.add(new Case(name, m -> f.apply(m).post()));
+ CASES.add(new Case(name + " [reified]", m -> f.apply(m).reify()));
+ }
+
+ /**
+ * A case which runs f .
+ */
+ private static void s(String name, Consumer f) {
+ CASES.add(new Case(name, f));
+ }
+
+ private static IntVar[] x(Model m, int n, int lb, int ub) {
+ return m.intVarArray("x", n, lb, ub);
+ }
+
+ private static IntVar v(Model m, String name, int lb, int ub) {
+ return m.intVar(name, lb, ub);
+ }
+
+ private static BoolVar[] b(Model m, int n) {
+ return m.boolVarArray("b", n);
+ }
+
+ private static int[] ints(int... values) {
+ return values;
+ }
+
+ /**
+ * Post a unary cumulative over the tasks, so that they matter.
+ */
+ private static void cum(Model m, Task... tasks) {
+ IntVar[] h = new IntVar[tasks.length];
+ Arrays.fill(h, m.intVar(1));
+ m.cumulative(tasks, h, m.intVar(1)).post();
+ }
+
+ private static Tuples tuples(boolean feasible, int[]... ts) {
+ Tuples t = new Tuples(feasible);
+ t.add(ts);
+ return t;
+ }
+
+ /**
+ * The numbering of the states of an automaton built from a regular expression is not deterministic (it
+ * depends on identity hash codes): the direct and the recorded models of a case must share the automaton.
+ */
+ private static final FiniteAutomaton AUTOMATON = new FiniteAutomaton("0*1*2*");
+
+ private static FiniteAutomaton automaton() {
+ return AUTOMATON;
+ }
+
+ private static Task[] tasks(Model m, int n) {
+ Task[] ts = new Task[n];
+ for (int i = 0; i < n; i++) {
+ ts[i] = m.taskVar(v(m, "s" + i, 0, 3), 1 + (i % 2));
+ }
+ return ts;
+ }
+
+ static {
+ // ---------------------------------------------------------------- variables
+ s("boolVar()", m -> m.boolVar());
+ s("boolVar(String)", m -> m.boolVar("b"));
+ s("boolVar(String,boolean)", m -> m.arithm(m.boolVar("t", true), "=", m.boolVar("b")).post());
+ s("boolVar(boolean)", m -> m.arithm(m.boolVar(false), "=", m.boolVar("b")).post());
+ s("boolVarArray(String,int)", m -> m.boolVarArray("b", 3));
+ s("boolVarArray(int)", m -> m.boolVarArray(3));
+ s("boolVarMatrix(String,int,int)", m -> m.boolVarMatrix("b", 2, 2));
+ s("boolVarMatrix(int,int)", m -> m.boolVarMatrix(2, 2));
+ s("generateName()", m -> {
+ m.generateName();
+ m.boolVar();
+ });
+ s("generateName(String)", m -> {
+ m.generateName("X_");
+ m.intVar(0, 2);
+ });
+ s("intVar(IntVar)", m -> m.intVar(m.intVar("x", new int[]{1, 3, 5})));
+ s("intVar(String,IntVar)", m -> m.intVar("y", v(m, "x", 0, 3)));
+ s("intVar(String,int)", m -> m.arithm(m.intVar("c", 7), ">", v(m, "x", 5, 9)).post());
+ s("intVar(String,int,int)", m -> m.intVar("x", 0, 3));
+ s("intVar(String,int,int,boolean)", m -> m.intVar("x", 0, 3, true));
+ s("intVar(String,int[])", m -> m.intVar("x", new int[]{1, 4, 6}));
+ s("intVar(int)", m -> m.arithm(m.intVar(4), ">", v(m, "x", 2, 6)).post());
+ s("intVar(int,int)", m -> m.intVar(0, 3));
+ s("intVar(int,int,boolean)", m -> m.intVar(0, 3, false));
+ s("intVar(int[])", m -> m.intVar(new int[]{2, 5}));
+ s("intVarArray(String,int,int,int)", m -> m.intVarArray("x", 2, 0, 2));
+ s("intVarArray(String,int,int,int,boolean)", m -> m.intVarArray("x", 2, 0, 2, true));
+ s("intVarArray(String,int,int[])", m -> m.intVarArray("x", 2, new int[]{1, 3}));
+ s("intVarArray(int,int,int)", m -> m.intVarArray(2, 0, 2));
+ s("intVarArray(int,int,int,boolean)", m -> m.intVarArray(2, 0, 2, false));
+ s("intVarArray(int,int[])", m -> m.intVarArray(2, new int[]{1, 3}));
+ s("intVarMatrix(String,int,int,int,int)", m -> m.intVarMatrix("x", 2, 2, 0, 1));
+ s("intVarMatrix(String,int,int,int,int,boolean)", m -> m.intVarMatrix("x", 2, 2, 0, 1, true));
+ s("intVarMatrix(String,int,int,int[])", m -> m.intVarMatrix("x", 2, 2, new int[]{0, 2}));
+ s("intVarMatrix(int,int,int,int)", m -> m.intVarMatrix(2, 2, 0, 1));
+ s("intVarMatrix(int,int,int,int,boolean)", m -> m.intVarMatrix(2, 2, 0, 1, false));
+ s("intVarMatrix(int,int,int[])", m -> m.intVarMatrix(2, 2, new int[]{0, 2}));
+ // tasks
+ s("taskVar(IntVar,IntVar)", m -> cum(m, m.taskVar(v(m, "s", 0, 3), v(m, "d", 1, 2)), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(IntVar,IntVar,BoolVar)", m -> cum(m, m.taskVar(v(m, "s", 0, 3), v(m, "d", 1, 2), m.boolVar("p")), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(IntVar,IntVar,IntVar)", m -> cum(m, m.taskVar(v(m, "s", 0, 3), v(m, "d", 1, 2), v(m, "e", 1, 5)), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(IntVar,IntVar,IntVar,BoolVar)", m -> cum(m, m.taskVar(v(m, "s", 0, 3), v(m, "d", 1, 2), v(m, "e", 1, 5), m.boolVar("p")), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(IntVar,IntVar,IntVar,boolean)", m -> cum(m, m.taskVar(v(m, "s", 0, 3), v(m, "d", 1, 2), v(m, "e", 1, 5), true), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(IntVar,IntVar,boolean)", m -> cum(m, m.taskVar(v(m, "s", 0, 3), v(m, "d", 1, 2), true), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(IntVar,int)", m -> cum(m, m.taskVar(v(m, "s", 0, 3), 2), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(IntVar,int,BoolVar)", m -> cum(m, m.taskVar(v(m, "s", 0, 3), 2, m.boolVar("p")), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(IntVar,int,IntVar)", m -> cum(m, m.taskVar(v(m, "s", 0, 3), 2, v(m, "e", 2, 5)), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(IntVar,int,IntVar,BoolVar)", m -> cum(m, m.taskVar(v(m, "s", 0, 3), 2, v(m, "e", 2, 5), m.boolVar("p")), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(IntVar,int,IntVar,boolean)", m -> cum(m, m.taskVar(v(m, "s", 0, 3), 2, v(m, "e", 2, 5), true), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(IntVar,int,boolean)", m -> cum(m, m.taskVar(v(m, "s", 0, 3), 2, true), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(int,int,int,int,int)", m -> cum(m, m.taskVar(0, 3, 2, 2, 5), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(int,int,int,int,int,BoolVar)", m -> cum(m, m.taskVar(0, 3, 2, 2, 5, m.boolVar("p")), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVar(int,int,int,int,int,boolean)", m -> cum(m, m.taskVar(0, 3, 2, 2, 5, true), m.taskVar(v(m, "t", 0, 3), 1)));
+ s("taskVarArray(IntVar[],IntVar[],IntVar[])", m -> cum(m, m.taskVarArray(x(m, 2, 0, 2), m.intVarArray("d", 2, 1, 2), m.intVarArray("e", 2, 1, 4))));
+ s("taskVarMatrix(IntVar[][],IntVar[][],IntVar[][])", m -> {
+ Task[][] t = m.taskVarMatrix(m.intVarMatrix("s", 1, 2, 0, 2), m.intVarMatrix("d", 1, 2, 1, 2), m.intVarMatrix("e", 1, 2, 1, 4));
+ cum(m, t[0]);
+ });
+
+ // ---------------------------------------------------------------- views
+ s("abs(IntVar)", m -> m.arithm(m.abs(v(m, "x", -2, 2)), "!=", 1).post());
+ s("boolNotView(BoolVar)", m -> {
+ BoolVar[] bs = b(m, 2);
+ m.arithm(m.boolNotView(bs[0]), "=", bs[1]).post();
+ });
+ s("intAbsView(IntVar)", m -> m.arithm(m.intAbsView(v(m, "x", -2, 2)), "!=", 1).post());
+ s("intAffineView(int,IntVar,int)", m -> m.arithm(m.intAffineView(2, v(m, "x", 0, 3), 1), "!=", 3).post());
+ s("intEqView(IntVar,int)", m -> m.arithm(m.intEqView(v(m, "x", 0, 3), 1), "=", 0).post());
+ s("intGeView(IntVar,int)", m -> m.arithm(m.intGeView(v(m, "x", 0, 3), 2), "=", 1).post());
+ s("intLeView(IntVar,int)", m -> m.arithm(m.intLeView(v(m, "x", 0, 3), 1), "=", 1).post());
+ s("intMinusView(IntVar)", m -> m.arithm(m.intMinusView(v(m, "x", 0, 3)), ">", -2).post());
+ s("intNeView(IntVar,int)", m -> m.arithm(m.intNeView(v(m, "x", 0, 3), 1), "=", 1).post());
+ s("intOffsetView(IntVar,int)", m -> m.arithm(m.intOffsetView(v(m, "x", 0, 3), 2), "!=", 3).post());
+ s("intScaleView(IntVar,int)", m -> m.arithm(m.intScaleView(v(m, "x", 0, 3), 3), "!=", 3).post());
+ s("intView(int,IntVar,int)", m -> m.arithm(m.intView(2, v(m, "x", 0, 3), -1), "!=", 1).post());
+ s("isEq(IntVar,int)", m -> m.arithm(m.isEq(v(m, "x", 0, 3), 1), "=", 0).post());
+ s("isGeq(IntVar,int)", m -> m.arithm(m.isGeq(v(m, "x", 0, 3), 2), "=", 1).post());
+ s("isLeq(IntVar,int)", m -> m.arithm(m.isLeq(v(m, "x", 0, 3), 1), "=", 1).post());
+ s("isNeq(IntVar,int)", m -> m.arithm(m.isNeq(v(m, "x", 0, 3), 1), "=", 1).post());
+ s("mul(IntVar,int)", m -> m.arithm(m.mul(v(m, "x", 0, 3), 2), "!=", 2).post());
+ s("neg(IntVar)", m -> m.arithm(m.neg(v(m, "x", 0, 3)), ">", -2).post());
+ s("offset(IntVar,int)", m -> m.arithm(m.offset(v(m, "x", 0, 3), 2), "!=", 3).post());
+
+ // ---------------------------------------------------------------- integer constraints
+ c("absolute(IntVar,IntVar)", m -> m.absolute(v(m, "y", 0, 2), v(m, "x", -2, 2)));
+ c("allDiffPrec(IntVar[],boolean[][])", m -> m.allDiffPrec(x(m, 3, 0, 2), new boolean[][]{{false, true, false}, {false, false, false}, {false, false, false}}));
+ c("allDiffPrec(IntVar[],boolean[][],String)", m -> m.allDiffPrec(x(m, 3, 0, 2), new boolean[][]{{false, true, false}, {false, false, false}, {false, false, false}}, "BESSIERE"));
+ c("allDiffPrec(IntVar[],int[][],int[][])", m -> m.allDiffPrec(x(m, 3, 0, 2), new int[][]{{}, {0}, {}}, new int[][]{{1}, {}, {}}));
+ c("allDiffPrec(IntVar[],int[][],int[][],String)", m -> m.allDiffPrec(x(m, 3, 0, 2), new int[][]{{}, {0}, {}}, new int[][]{{1}, {}, {}}, "GREEDY"));
+ c("allDifferent(IntVar[])", m -> m.allDifferent(x(m, 3, 0, 3)));
+ c("allDifferent(IntVar[],String)", m -> m.allDifferent(x(m, 3, 0, 3), "AC"));
+ c("allDifferentExcept0(IntVar[])", m -> m.allDifferentExcept0(x(m, 3, 0, 2)));
+ c("allDifferentExceptValues(IntVar[],int[])", m -> m.allDifferentExceptValues(x(m, 3, 0, 2), ints(0, 1)));
+ c("allDifferentUnderCondition(IntVar[],Condition,boolean)", m -> m.allDifferentUnderCondition(x(m, 3, 0, 2), Condition.EXCEPT_0, true));
+ c("allDifferentUnderCondition(IntVar[],Condition,boolean,String)", m -> m.allDifferentUnderCondition(x(m, 3, 0, 2), v -> !v.contains(0), false, "AC"));
+ c("allEqual(IntVar[])", m -> m.allEqual(x(m, 3, 0, 2)));
+ c("among(IntVar,IntVar[],int[])", m -> m.among(v(m, "n", 0, 3), x(m, 3, 0, 2), ints(1, 2)));
+ c("and(BoolVar[])", m -> m.and(b(m, 3)));
+ c("and(Constraint[])", m -> {
+ IntVar[] x = x(m, 2, 0, 2);
+ return m.and(m.arithm(x[0], "<", x[1]), m.arithm(x[1], "!=", 1));
+ });
+ c("argmax(IntVar,int,IntVar[])", m -> m.argmax(v(m, "z", 0, 2), 0, x(m, 3, 0, 2)));
+ c("argmin(IntVar,int,IntVar[])", m -> m.argmin(v(m, "z", 1, 3), 1, x(m, 3, 0, 2)));
+ c("arithm(IntVar,String,IntVar)", m -> m.arithm(v(m, "x", 0, 3), "<", v(m, "y", 0, 3)));
+ c("arithm(IntVar,String,IntVar,String,IntVar)", m -> m.arithm(v(m, "x", 0, 3), "+", v(m, "y", 0, 3), "=", v(m, "z", 0, 3)));
+ c("arithm(IntVar,String,IntVar,String,int)", m -> m.arithm(v(m, "x", 0, 3), "-", v(m, "y", 0, 3), "<=", 1));
+ c("arithm(IntVar,String,int)", m -> m.arithm(v(m, "x", 0, 3), "!=", 2));
+ c("atLeastNValues(IntVar[],IntVar,boolean)", m -> m.atLeastNValues(x(m, 3, 0, 2), v(m, "n", 2, 3), true));
+ c("atMostNValues(IntVar[],IntVar,boolean)", m -> m.atMostNValues(x(m, 3, 0, 2), v(m, "n", 1, 2), false));
+ c("binPacking(IntVar[],int[],IntVar[],int)", m -> m.binPacking(x(m, 3, 0, 1), ints(1, 2, 2), m.intVarArray("l", 2, 0, 4), 0));
+ c("bitsIntChanneling(BoolVar[],IntVar)", m -> m.bitsIntChanneling(b(m, 3), v(m, "x", 0, 7)));
+ c("boolsIntChanneling(BoolVar[],IntVar,int)", m -> m.boolsIntChanneling(b(m, 3), v(m, "x", 0, 2), 0));
+ c("circuit(IntVar[])", m -> m.circuit(x(m, 4, 0, 3)));
+ c("circuit(IntVar[],int)", m -> m.circuit(x(m, 4, 1, 4), 1));
+ c("circuit(IntVar[],int,CircuitConf)", m -> m.circuit(x(m, 4, 0, 3), 0, CircuitConf.RD));
+ c("clausesIntChanneling(IntVar,BoolVar[],BoolVar[])", m -> m.clausesIntChanneling(v(m, "x", 0, 2), m.boolVarArray("e", 3), m.boolVarArray("l", 3)));
+ c("costRegular(IntVar[],IntVar,ICostAutomaton)", m -> {
+ int[][] costs = new int[3][3];
+ for (int[] c : costs) Arrays.fill(c, 1);
+ costs[1][2] = 3;
+ return m.costRegular(x(m, 3, 0, 2), v(m, "c", 0, 5), CostAutomaton.makeSingleResource(automaton(), costs, 0, 5));
+ });
+ c("count(IntVar,IntVar[],IntVar)", m -> m.count(v(m, "val", 0, 2), x(m, 3, 0, 2), v(m, "n", 1, 2)));
+ c("count(int,IntVar[],IntVar)", m -> m.count(1, x(m, 3, 0, 2), v(m, "n", 1, 2)));
+ c("custom(String,Variable[],Function)", m -> m.custom("geq", x(m, 2, 0, 2), PropGreaterOrEqualX_Y::new));
+ c("custom(String,Variable[],Object,BiFunction)", m -> m.custom("geq", x(m, 1, 0, 2), x(m, 1, 0, 2), (u, v) -> new PropGreaterOrEqualX_Y(new IntVar[]{u[0], v[0]})));
+ c("decreasing(IntVar[],int)", m -> m.decreasing(x(m, 3, 0, 3), 1));
+ c("diffN(IntVar[],IntVar[],IntVar[],IntVar[],boolean)", m -> m.diffN(m.intVarArray("x", 2, 0, 2), m.intVarArray("y", 2, 0, 2), m.intVarArray("w", 2, 1, 2), m.intVarArray("h", 2, 1, 2), true));
+ c("diffN(IntVar[][],int[][])", m -> m.diffN(m.intVarMatrix("o", 2, 2, 0, 2), new int[][]{{1, 2}, {2, 1}}));
+ c("distance(IntVar,IntVar,String,IntVar)", m -> m.distance(v(m, "x", 0, 3), v(m, "y", 0, 3), "<", v(m, "z", 1, 2)));
+ c("distance(IntVar,IntVar,String,int)", m -> m.distance(v(m, "x", 0, 3), v(m, "y", 0, 3), "=", 1));
+ c("div(IntVar,IntVar,IntVar)", m -> m.div(v(m, "x", 0, 6), v(m, "y", 1, 3), v(m, "z", 0, 6)));
+ c("element(IntVar,IntVar[],IntVar,int)", m -> m.element(v(m, "v", 0, 2), x(m, 3, 0, 2), v(m, "i", 1, 3), 1));
+ c("element(IntVar,int[],IntVar)", m -> m.element(v(m, "v", 0, 5), ints(3, 1, 4, 1, 5), v(m, "i", 0, 4)));
+ c("element(IntVar,int[],IntVar,int)", m -> m.element(v(m, "v", 0, 5), ints(3, 1, 4, 1, 5), v(m, "i", 0, 6), 2));
+ c("globalCardinality(IntVar[],int[],IntVar[],boolean)", m -> m.globalCardinality(x(m, 3, 0, 2), ints(0, 1), m.intVarArray("o", 2, 0, 3), false));
+ c("globalCardinality(IntVar[],int[],IntVar[],boolean,String)", m -> m.globalCardinality(x(m, 3, 0, 2), ints(0, 1), m.intVarArray("o", 2, 0, 3), true, "AC"));
+ c("increasing(IntVar[],int)", m -> m.increasing(x(m, 3, 0, 3), 0));
+ c("intValuePrecedeChain(IntVar[],int,int)", m -> m.intValuePrecedeChain(x(m, 4, 0, 2), 1, 2));
+ c("intValuePrecedeChain(IntVar[],int[])", m -> m.intValuePrecedeChain(x(m, 4, 0, 2), ints(0, 1, 2)));
+ c("inverseChanneling(IntVar[],IntVar[])", m -> m.inverseChanneling(x(m, 3, 0, 2), m.intVarArray("y", 3, 0, 2)));
+ c("inverseChanneling(IntVar[],IntVar[],int,int)", m -> m.inverseChanneling(x(m, 3, 1, 3), m.intVarArray("y", 3, 1, 3), 1, 1));
+ c("inverseChanneling(IntVar[],IntVar[],int,int,boolean)", m -> m.inverseChanneling(x(m, 3, 1, 3), m.intVarArray("y", 3, 0, 2), 0, 1, true));
+ c("keySort(IntVar[][],IntVar[],IntVar[][],int)", m -> m.keySort(m.intVarMatrix("a", 3, 2, 0, 1), m.intVarArray("p", 3, 1, 3), m.intVarMatrix("s", 3, 2, 0, 1), 1));
+ c("knapsack(IntVar[],IntVar,IntVar,int[],int[])", m -> m.knapsack(x(m, 3, 0, 2), v(m, "w", 0, 6), v(m, "e", 0, 10), ints(1, 2, 3), ints(2, 3, 4)));
+ c("lexChainLess(IntVar[][])", m -> m.lexChainLess(m.intVarMatrix("a", 3, 2, 0, 1)));
+ c("lexChainLessEq(IntVar[][])", m -> m.lexChainLessEq(m.intVarMatrix("a", 3, 2, 0, 1)));
+ c("lexLess(IntVar[],IntVar[])", m -> m.lexLess(x(m, 2, 0, 2), m.intVarArray("y", 2, 0, 2)));
+ c("lexLessEq(IntVar[],IntVar[])", m -> m.lexLessEq(x(m, 2, 0, 2), m.intVarArray("y", 2, 0, 2)));
+ c("max(BoolVar,BoolVar[])", m -> m.max(m.boolVar("m"), b(m, 3)));
+ c("max(IntVar,IntVar,IntVar)", m -> m.max(v(m, "m", 0, 3), v(m, "x", 0, 3), v(m, "y", 0, 3)));
+ c("max(IntVar,IntVar[])", m -> m.max(v(m, "m", 0, 3), x(m, 3, 0, 3)));
+ c("mddc(IntVar[],MultivaluedDecisionDiagram)", m -> {
+ IntVar[] x = x(m, 3, 0, 2);
+ return m.mddc(x, new MultivaluedDecisionDiagram(x, tuples(true, ints(0, 1, 2), ints(1, 1, 1), ints(2, 0, 1))));
+ });
+ c("member(IntVar,IntIterableRangeSet)", m -> m.member(v(m, "x", 0, 5), new IntIterableRangeSet(ints(1, 3, 4))));
+ c("member(IntVar,int,int)", m -> m.member(v(m, "x", 0, 5), 2, 4));
+ c("member(IntVar,int[])", m -> m.member(v(m, "x", 0, 5), ints(1, 5)));
+ c("min(BoolVar,BoolVar[])", m -> m.min(m.boolVar("m"), b(m, 3)));
+ c("min(IntVar,IntVar,IntVar)", m -> m.min(v(m, "m", 0, 3), v(m, "x", 0, 3), v(m, "y", 0, 3)));
+ c("min(IntVar,IntVar[])", m -> m.min(v(m, "m", 0, 3), x(m, 3, 0, 3)));
+ c("mod(IntVar,IntVar,IntVar)", m -> m.mod(v(m, "x", 0, 6), v(m, "y", 1, 3), v(m, "z", 0, 2)));
+ c("mod(IntVar,int,IntVar)", m -> m.mod(v(m, "x", 0, 6), 3, v(m, "z", 0, 2)));
+ c("mod(IntVar,int,int)", m -> m.mod(v(m, "x", 0, 6), 3, 1));
+ c("multiCostRegular(IntVar[],IntVar[],ICostAutomaton)", m -> {
+ int[][][] costs = new int[3][3][2];
+ for (int[][] l : costs) for (int[] c : l) Arrays.fill(c, 1);
+ return m.multiCostRegular(x(m, 3, 0, 2), m.intVarArray("c", 2, 0, 3), CostAutomaton.makeMultiResources(automaton(), costs, ints(0, 0), ints(3, 3)));
+ });
+ c("multiCostRegular(IntVar[],IntVar[],ICostAutomaton,double)", m -> {
+ int[][][] costs = new int[3][3][2];
+ for (int[][] l : costs) for (int[] c : l) Arrays.fill(c, 1);
+ return m.multiCostRegular(x(m, 3, 0, 2), m.intVarArray("c", 2, 0, 3), CostAutomaton.makeMultiResources(automaton(), costs, ints(0, 0), ints(3, 3)), 1e-4);
+ });
+ c("nValues(IntVar[],IntVar)", m -> m.nValues(x(m, 3, 0, 2), v(m, "n", 1, 2)));
+ c("not(Constraint)", m -> m.not(m.arithm(v(m, "x", 0, 3), "=", 1)));
+ c("notAllEqual(IntVar[])", m -> m.notAllEqual(x(m, 3, 0, 1)));
+ c("notMember(IntVar,IntIterableRangeSet)", m -> m.notMember(v(m, "x", 0, 5), new IntIterableRangeSet(ints(1, 3, 4))));
+ c("notMember(IntVar,int,int)", m -> m.notMember(v(m, "x", 0, 5), 2, 4));
+ c("notMember(IntVar,int[])", m -> m.notMember(v(m, "x", 0, 5), ints(1, 5)));
+ c("or(BoolVar[])", m -> m.or(b(m, 3)));
+ c("or(Constraint[])", m -> {
+ IntVar[] x = x(m, 2, 0, 2);
+ return m.or(m.arithm(x[0], "<", x[1]), m.arithm(x[1], "=", 0));
+ });
+ c("path(IntVar[],IntVar,IntVar)", m -> m.path(x(m, 4, 0, 4), v(m, "s", 0, 3), v(m, "e", 0, 3)));
+ c("path(IntVar[],IntVar,IntVar,int)", m -> m.path(x(m, 4, 1, 5), v(m, "s", 1, 4), v(m, "e", 1, 4), 1));
+ c("pow(IntVar,IntVar,IntVar)", m -> m.pow(v(m, "b", 0, 2), v(m, "e", 0, 2), v(m, "r", 0, 4)));
+ c("pow(IntVar,int,IntVar)", m -> m.pow(v(m, "b", -2, 2), 2, v(m, "r", 0, 4)));
+ c("regular(IntVar[],IAutomaton)", m -> m.regular(x(m, 3, 0, 2), automaton()));
+ c("scalar(IntVar[],int[],String,IntVar)", m -> m.scalar(x(m, 3, 0, 2), ints(1, 2, 3), "<=", v(m, "s", 0, 6)));
+ c("scalar(IntVar[],int[],String,IntVar,int)", m -> m.scalar(x(m, 3, 0, 2), ints(1, 2, 3), "=", v(m, "s", 0, 6), 2));
+ c("scalar(IntVar[],int[],String,int)", m -> m.scalar(x(m, 3, 0, 2), ints(1, 2, 3), "=", 4));
+ c("scalar(IntVar[],int[],String,int,int)", m -> m.scalar(x(m, 3, 0, 2), ints(1, -2, 3), ">=", 2, 2));
+ c("sort(IntVar[],IntVar[])", m -> m.sort(x(m, 3, 0, 2), m.intVarArray("y", 3, 0, 2)));
+ c("square(IntVar,IntVar)", m -> m.square(v(m, "y", 0, 9), v(m, "x", -3, 3)));
+ c("subCircuit(IntVar[],int,IntVar)", m -> m.subCircuit(x(m, 4, 0, 3), 0, v(m, "l", 0, 4)));
+ c("subPath(IntVar[],IntVar,IntVar,int,IntVar)", m -> m.subPath(x(m, 4, 0, 4), v(m, "s", 0, 3), v(m, "e", 0, 3), 0, v(m, "l", 1, 4)));
+ c("sum(BoolVar[],String,IntVar)", m -> m.sum(b(m, 3), "=", v(m, "s", 0, 2)));
+ c("sum(BoolVar[],String,IntVar,int)", m -> m.sum(b(m, 3), "<=", v(m, "s", 0, 2), 2));
+ c("sum(BoolVar[],String,int)", m -> m.sum(b(m, 3), "=", 2));
+ c("sum(IntVar[],String,IntVar)", m -> m.sum(x(m, 3, 0, 2), "=", v(m, "s", 0, 6)));
+ c("sum(IntVar[],String,IntVar,int)", m -> m.sum(x(m, 3, 0, 2), "<", v(m, "s", 0, 6), 2));
+ c("sum(IntVar[],String,IntVar[])", m -> m.sum(x(m, 2, 0, 2), "=", m.intVarArray("y", 2, 0, 2)));
+ c("sum(IntVar[],String,int)", m -> m.sum(x(m, 3, 0, 2), "=", 3));
+ c("sum(IntVar[],String,int,int)", m -> m.sum(x(m, 3, 0, 2), "!=", 3, 2));
+ c("table(IntVar,IntVar,Tuples)", m -> m.table(v(m, "x", 0, 2), v(m, "y", 0, 2), tuples(true, ints(0, 1), ints(1, 2), ints(2, 2))));
+ c("table(IntVar,IntVar,Tuples,String)", m -> m.table(v(m, "x", 0, 2), v(m, "y", 0, 2), tuples(false, ints(0, 1), ints(1, 2)), "AC3"));
+ c("table(IntVar[],HybridTuples)", m -> {
+ HybridTuples h = new HybridTuples();
+ h.add(HybridTuples.eq(1), HybridTuples.any(), HybridTuples.ge(1));
+ h.add(HybridTuples.ne(1), HybridTuples.col(0), HybridTuples.lt(2));
+ return m.table(x(m, 3, 0, 2), h);
+ });
+ c("table(IntVar[],Tuples)", m -> m.table(x(m, 3, 0, 2), tuples(true, ints(0, 1, 2), ints(1, 1, 1), ints(2, 0, 1))));
+ c("table(IntVar[],Tuples,String)", m -> m.table(x(m, 3, 0, 2), tuples(true, ints(0, 1, 2), ints(1, 1, 1), ints(2, 0, 1)), "STR2+"));
+ c("times(IntVar,IntVar,IntVar)", m -> m.times(v(m, "x", -2, 2), v(m, "y", 0, 2), v(m, "z", -4, 4)));
+ c("times(IntVar,IntVar,int)", m -> m.times(v(m, "x", 0, 3), v(m, "y", 0, 3), 2));
+ c("times(IntVar,int,IntVar)", m -> m.times(v(m, "x", 0, 3), 2, v(m, "z", 0, 6)));
+ c("tree(IntVar[],IntVar)", m -> m.tree(x(m, 4, 0, 3), v(m, "n", 1, 2)));
+ c("tree(IntVar[],IntVar,int)", m -> m.tree(x(m, 4, 1, 4), v(m, "n", 1, 2), 1));
+ s("conditional(Variable[],Function,Supplier) [unsupported by design]", m -> {
+ IntVar[] x = x(m, 2, 0, 2);
+ m.conditional(x, vs -> vs[0].isInstantiated(), () -> new Constraint[]{m.arithm(x[0], "!=", x[1])});
+ });
+
+ // ---------------------------------------------------------------- decompositions
+ s("allDifferentExceptDec(IntVar[],int[])", m -> m.allDifferentExceptDec(x(m, 3, 0, 2), ints(0)));
+ s("allDifferentExceptDecWithGCC(IntVar[],int[])", m -> m.allDifferentExceptDecWithGCC(x(m, 3, 0, 2), ints(0)));
+ s("amongDec(IntVar,IntVar[],IntVar[])", m -> m.amongDec(v(m, "n", 0, 3), x(m, 3, 0, 2), new IntVar[]{m.intVar(1), m.intVar(2)}));
+ s("argmaxDec(IntVar,int,IntVar[])", m -> m.argmaxDec(v(m, "z", 0, 2), 0, x(m, 3, 0, 2)));
+ s("argminDec(IntVar,int,IntVar[])", m -> m.argminDec(v(m, "z", 0, 2), 0, x(m, 3, 0, 2)));
+ s("binPackingDec(IntVar[],int[],IntVar[],int)", m -> m.binPackingDec(x(m, 3, 0, 1), ints(1, 2, 2), m.intVarArray("l", 2, 0, 4), 0));
+ s("circuitDec(IntVar[],int)", m -> m.circuitDec(x(m, 4, 0, 3), 0));
+ s("costFlow(int[],int[],int[],int[],IntVar[],IntVar,int)", m -> m.costFlow(ints(0, 1, 0), ints(1, 2, 2), ints(1, 0, -1), ints(1, 1, 3), m.intVarArray("f", 3, 0, 1), v(m, "c", 0, 5), 0));
+ s("cumulativeDec(IntVar[],IntVar[],IntVar[],IntVar)", m -> m.cumulativeDec(x(m, 2, 0, 2), m.intVarArray("d", 2, 1, 2), m.intVarArray("h", 2, 1, 1), v(m, "c", 1, 1)));
+ s("cumulativeDec(Task[],IntVar[],IntVar)", m -> m.cumulativeDec(tasks(m, 2), m.intVarArray("h", 2, 1, 1), v(m, "c", 1, 1)));
+ s("cumulativeTimeDec(IntVar[],int[],int[],int)", m -> m.cumulativeTimeDec(x(m, 2, 0, 2), ints(1, 2), ints(1, 1), 1));
+ s("disjunctiveDec(Task[],boolean)", m -> m.disjunctiveDec(tasks(m, 2), true));
+ s("element(IntVar,IntVar[][],IntVar,int,IntVar,int)", m -> m.element(v(m, "v", 0, 2), m.intVarMatrix("t", 2, 2, 0, 2), v(m, "r", 0, 1), 0, v(m, "c", 1, 2), 1));
+ s("element(IntVar,int[][],IntVar,int,IntVar,int)", m -> m.element(v(m, "v", 0, 5), new int[][]{{1, 2}, {3, 4}}, v(m, "r", 0, 1), 0, v(m, "c", 0, 1), 0));
+ s("globalCardinalityDec(IntVar[],IntVar[],IntVar[],boolean)", m -> m.globalCardinalityDec(x(m, 3, 0, 2), new IntVar[]{m.intVar(0), m.intVar(1)}, m.intVarArray("o", 2, 0, 3), false));
+ s("ifThenElseDec(BoolVar[],IntVar[],IntVar)", m -> m.ifThenElseDec(b(m, 2), x(m, 2, 0, 2), v(m, "y", 0, 2)));
+ s("ifThenElseDec(BoolVar[],int[],IntVar)", m -> m.ifThenElseDec(b(m, 2), ints(1, 2), v(m, "y", 0, 2)));
+ s("intValuePrecedeChainDec(IntVar[],int,int)", m -> m.intValuePrecedeChainDec(x(m, 4, 0, 2), 1, 2));
+ s("intValuePrecedeChainDec(IntVar[],int[])", m -> m.intValuePrecedeChainDec(x(m, 4, 0, 2), ints(0, 1, 2)));
+ s("product(BoolVar[][],BoolVar[][],BoolVar[][])", m -> m.product(m.boolVarMatrix("a", 2, 2), m.boolVarMatrix("b", 2, 2), m.boolVarMatrix("c", 2, 2)));
+ s("product(IntVar[][],IntVar[][],IntVar[][])", m -> m.product(m.intVarMatrix("a", 2, 2, 0, 1), m.intVarMatrix("b", 2, 2, 0, 1), m.intVarMatrix("c", 2, 2, 0, 2)));
+ s("regularDec(IntVar[],IAutomaton)", m -> m.regularDec(x(m, 3, 0, 2), automaton()));
+
+ // ---------------------------------------------------------------- reifications
+ s("ifOnlyIf(Constraint,Constraint)", m -> {
+ IntVar[] x = x(m, 2, 0, 2);
+ m.ifOnlyIf(m.arithm(x[0], "=", 1), m.arithm(x[1], ">", 0));
+ });
+ s("ifThen(BoolVar,Constraint)", m -> m.ifThen(m.boolVar("b"), m.arithm(v(m, "x", 0, 2), "=", 1)));
+ s("ifThen(Constraint,Constraint)", m -> {
+ IntVar[] x = x(m, 2, 0, 2);
+ m.ifThen(m.arithm(x[0], "=", 1), m.arithm(x[1], ">", 0));
+ });
+ s("ifThenElse(BoolVar,Constraint,Constraint)", m -> {
+ IntVar x = v(m, "x", 0, 2);
+ m.ifThenElse(m.boolVar("b"), m.arithm(x, "=", 1), m.arithm(x, "=", 2));
+ });
+ s("ifThenElse(Constraint,Constraint,Constraint)", m -> {
+ IntVar[] x = x(m, 2, 0, 2);
+ m.ifThenElse(m.arithm(x[0], "=", 1), m.arithm(x[1], ">", 0), m.arithm(x[1], "=", 0));
+ });
+ s("impXrelC(IntVar,String,int,BoolVar)", m -> m.impXrelC(v(m, "x", 0, 3), "<", 2, m.boolVar("b")));
+ s("impXrelYC(IntVar,String,IntVar,int,BoolVar)", m -> m.impXrelYC(v(m, "x", 0, 3), "<=", v(m, "y", 0, 3), 1, m.boolVar("b")));
+ s("reifXrelC(IntVar,String,int,BoolVar)", m -> m.reifXrelC(v(m, "x", 0, 3), ">=", 1, m.boolVar("b")));
+ s("reifXrelYC(IntVar,String,IntVar,int,BoolVar)", m -> m.reifXrelYC(v(m, "x", 0, 3), "=", v(m, "y", 0, 3), 1, m.boolVar("b")));
+ s("reification(BoolVar,Constraint)", m -> m.reification(m.boolVar("b"), m.arithm(v(m, "x", 0, 3), "=", 1)));
+ s("reifyXeqC(IntVar,int,BoolVar)", m -> m.reifyXeqC(v(m, "x", 0, 3), 1, m.boolVar("b")));
+ s("reifyXeqY(IntVar,IntVar,BoolVar)", m -> m.reifyXeqY(v(m, "x", 0, 3), v(m, "y", 0, 3), m.boolVar("b")));
+ s("reifyXeqYC(IntVar,IntVar,int,BoolVar)", m -> m.reifyXeqYC(v(m, "x", 0, 3), v(m, "y", 0, 3), 1, m.boolVar("b")));
+ s("reifyXgeY(IntVar,IntVar,BoolVar)", m -> m.reifyXgeY(v(m, "x", 0, 3), v(m, "y", 0, 3), m.boolVar("b")));
+ s("reifyXgtC(IntVar,int,BoolVar)", m -> m.reifyXgtC(v(m, "x", 0, 3), 1, m.boolVar("b")));
+ s("reifyXgtY(IntVar,IntVar,BoolVar)", m -> m.reifyXgtY(v(m, "x", 0, 3), v(m, "y", 0, 3), m.boolVar("b")));
+ s("reifyXgtYC(IntVar,IntVar,int,BoolVar)", m -> m.reifyXgtYC(v(m, "x", 0, 3), v(m, "y", 0, 3), 1, m.boolVar("b")));
+ s("reifyXinS(IntVar,IntIterableRangeSet,BoolVar)", m -> m.reifyXinS(v(m, "x", 0, 5), new IntIterableRangeSet(ints(1, 4)), m.boolVar("b")));
+ s("reifyXleY(IntVar,IntVar,BoolVar)", m -> m.reifyXleY(v(m, "x", 0, 3), v(m, "y", 0, 3), m.boolVar("b")));
+ s("reifyXltC(IntVar,int,BoolVar)", m -> m.reifyXltC(v(m, "x", 0, 3), 2, m.boolVar("b")));
+ s("reifyXltY(IntVar,IntVar,BoolVar)", m -> m.reifyXltY(v(m, "x", 0, 3), v(m, "y", 0, 3), m.boolVar("b")));
+ s("reifyXltYC(IntVar,IntVar,int,BoolVar)", m -> m.reifyXltYC(v(m, "x", 0, 3), v(m, "y", 0, 3), 1, m.boolVar("b")));
+ s("reifyXneC(IntVar,int,BoolVar)", m -> m.reifyXneC(v(m, "x", 0, 3), 1, m.boolVar("b")));
+ s("reifyXneY(IntVar,IntVar,BoolVar)", m -> m.reifyXneY(v(m, "x", 0, 3), v(m, "y", 0, 3), m.boolVar("b")));
+ s("reifyXneYC(IntVar,IntVar,int,BoolVar)", m -> m.reifyXneYC(v(m, "x", 0, 3), v(m, "y", 0, 3), 1, m.boolVar("b")));
+ s("reifyXnotinS(IntVar,IntIterableRangeSet,BoolVar)", m -> m.reifyXnotinS(v(m, "x", 0, 5), new IntIterableRangeSet(ints(1, 4)), m.boolVar("b")));
+
+ // ---------------------------------------------------------------- result variables
+ s("argmax(String,IntVar[])", m -> m.arithm(m.argmax("a", x(m, 3, 0, 2)), "!=", 1).post());
+ s("argmin(String,IntVar[])", m -> m.arithm(m.argmin("a", x(m, 3, 0, 2)), "!=", 1).post());
+ s("count(String,IntVar,IntVar[])", m -> m.arithm(m.count("c", v(m, "v", 0, 2), x(m, 3, 0, 2)), ">", 1).post());
+ s("count(String,int,IntVar[])", m -> m.arithm(m.count("c", 1, x(m, 3, 0, 2)), ">", 1).post());
+ s("element(String,IntVar[],IntVar,int)", m -> m.arithm(m.element("e", x(m, 3, 0, 2), v(m, "i", 1, 3), 1), "!=", 1).post());
+ s("element(String,int[],IntVar)", m -> m.arithm(m.element("e", ints(3, 1, 4), v(m, "i", 0, 2)), "!=", 1).post());
+ s("element(String,int[],IntVar,int)", m -> m.arithm(m.element("e", ints(3, 1, 4), v(m, "i", 1, 3), 1), "!=", 1).post());
+ s("max(String,IntVar[])", m -> m.arithm(m.max("m", x(m, 3, 0, 2)), "!=", 1).post());
+ s("min(String,IntVar[])", m -> m.arithm(m.min("m", x(m, 3, 0, 2)), "!=", 1).post());
+ s("mod(String,IntVar,IntVar)", m -> m.arithm(m.mod("r", v(m, "x", 0, 6), v(m, "y", 1, 3)), "!=", 1).post());
+ s("mod(String,IntVar,int)", m -> m.arithm(m.mod("r", v(m, "x", 0, 6), 3), "!=", 1).post());
+ s("sum(String,IntVar[])", m -> m.arithm(m.sum("s", x(m, 3, 0, 2)), "=", 3).post());
+
+ // ---------------------------------------------------------------- clauses
+ s("addClause(int[]), lit(int), neg(int), satVar(Variable,Literalizer)", m -> {
+ BoolVar[] b = b(m, 3);
+ m.addClause(m.lit(b[0].satVar()), m.neg(b[1].satVar()));
+ m.addClause(m.lit(b[2].satVar()), m.lit(b[1].satVar()));
+ });
+ s("sat()", m -> {
+ b(m, 2);
+ m.sat();
+ });
+ s("addClauseFalse(BoolVar)", m -> m.addClauseFalse(b(m, 2)[0]));
+ s("addClauseTrue(BoolVar)", m -> m.addClauseTrue(b(m, 2)[0]));
+ s("addClauses(BoolVar[],BoolVar[])", m -> {
+ BoolVar[] b = b(m, 3);
+ m.addClauses(new BoolVar[]{b[0], b[1]}, new BoolVar[]{b[2]});
+ });
+ s("addClauses(LogOp)", m -> {
+ BoolVar[] b = b(m, 3);
+ m.addClauses(LogOp.or(b[0], LogOp.and(b[1], LogOp.nor(b[2]))));
+ });
+ s("addClausesAtMostNMinusOne(BoolVar[])", m -> m.addClausesAtMostNMinusOne(b(m, 3)));
+ s("addClausesAtMostOne(BoolVar[])", m -> m.addClausesAtMostOne(b(m, 3)));
+ s("addClausesBoolAndArrayEqVar(BoolVar[],BoolVar)", m -> m.addClausesBoolAndArrayEqVar(b(m, 2), m.boolVar("r")));
+ s("addClausesBoolAndArrayEqualFalse(BoolVar[])", m -> m.addClausesBoolAndArrayEqualFalse(b(m, 3)));
+ s("addClausesBoolAndEqVar(BoolVar,BoolVar,BoolVar)", m -> {
+ BoolVar[] b = b(m, 3);
+ m.addClausesBoolAndEqVar(b[0], b[1], b[2]);
+ });
+ s("addClausesBoolEq(BoolVar,BoolVar)", m -> {
+ BoolVar[] b = b(m, 2);
+ m.addClausesBoolEq(b[0], b[1]);
+ });
+ s("addClausesBoolIsEqVar(BoolVar,BoolVar,BoolVar)", m -> {
+ BoolVar[] b = b(m, 3);
+ m.addClausesBoolIsEqVar(b[0], b[1], b[2]);
+ });
+ s("addClausesBoolIsLeVar(BoolVar,BoolVar,BoolVar)", m -> {
+ BoolVar[] b = b(m, 3);
+ m.addClausesBoolIsLeVar(b[0], b[1], b[2]);
+ });
+ s("addClausesBoolIsLtVar(BoolVar,BoolVar,BoolVar)", m -> {
+ BoolVar[] b = b(m, 3);
+ m.addClausesBoolIsLtVar(b[0], b[1], b[2]);
+ });
+ s("addClausesBoolIsNeqVar(BoolVar,BoolVar,BoolVar)", m -> {
+ BoolVar[] b = b(m, 3);
+ m.addClausesBoolIsNeqVar(b[0], b[1], b[2]);
+ });
+ s("addClausesBoolLe(BoolVar,BoolVar)", m -> {
+ BoolVar[] b = b(m, 2);
+ m.addClausesBoolLe(b[0], b[1]);
+ });
+ s("addClausesBoolLt(BoolVar,BoolVar)", m -> {
+ BoolVar[] b = b(m, 2);
+ m.addClausesBoolLt(b[0], b[1]);
+ });
+ s("addClausesBoolNot(BoolVar,BoolVar)", m -> {
+ BoolVar[] b = b(m, 2);
+ m.addClausesBoolNot(b[0], b[1]);
+ });
+ s("addClausesBoolOrArrayEqVar(BoolVar[],BoolVar)", m -> m.addClausesBoolOrArrayEqVar(b(m, 2), m.boolVar("r")));
+ s("addClausesBoolOrArrayEqualTrue(BoolVar[])", m -> m.addClausesBoolOrArrayEqualTrue(b(m, 3)));
+ s("addClausesBoolOrEqVar(BoolVar,BoolVar,BoolVar)", m -> {
+ BoolVar[] b = b(m, 3);
+ m.addClausesBoolOrEqVar(b[0], b[1], b[2]);
+ });
+ s("addClausesBoolXorEqVar(BoolVar,BoolVar,BoolVar)", m -> {
+ BoolVar[] b = b(m, 3);
+ m.addClausesBoolXorEqVar(b[0], b[1], b[2]);
+ });
+ s("addClausesMaxBoolArrayLessEqVar(BoolVar[],BoolVar)", m -> m.addClausesMaxBoolArrayLessEqVar(b(m, 2), m.boolVar("r")));
+ s("addClausesSumBoolArrayGreaterEqVar(BoolVar[],BoolVar)", m -> m.addClausesSumBoolArrayGreaterEqVar(b(m, 2), m.boolVar("r")));
+ s("addClausesSumBoolArrayLessEqKVar(BoolVar[],BoolVar)", m -> m.addClausesSumBoolArrayLessEqKVar(b(m, 3), m.boolVar("r")));
+ s("addConstructiveDisjunction(Constraint[])", m -> {
+ IntVar[] x = x(m, 2, 0, 3);
+ m.addConstructiveDisjunction(m.arithm(x[0], "<", x[1]), m.arithm(x[0], "=", 3));
+ });
+ s("addElement(IntVar,int[],IntVar,int)", m -> m.addElement(v(m, "v", 0, 5), ints(3, 1, 4), v(m, "i", 0, 2), 0));
+ s("addTable(IntVar[],HybridTuples)", m -> {
+ HybridTuples h = new HybridTuples();
+ h.add(HybridTuples.eq(1), HybridTuples.any());
+ h.add(HybridTuples.ne(1), HybridTuples.col(0));
+ m.addTable(x(m, 2, 0, 2), h);
+ });
+ s("addTable(IntVar[],Tuples)", m -> m.addTable(x(m, 2, 0, 2), tuples(true, ints(0, 1), ints(2, 2))));
+
+ // ---------------------------------------------------------------- scheduling
+ s("cumulative(IntVar[],int[],int[],int)", m -> m.cumulative(x(m, 3, 0, 3), ints(1, 2, 1), ints(1, 1, 2), 2).post());
+ s("cumulative(List,List,IntVar)", m -> m.cumulative(Arrays.asList(tasks(m, 2)), Arrays.asList(m.intVar(1), m.intVar(1)), v(m, "c", 1, 2)).post());
+ s("cumulative(Task[],IntVar[],IntVar)", m -> m.cumulative(tasks(m, 3), m.intVarArray("h", 3, 1, 2), v(m, "c", 2, 3)).post());
+ s("cumulative(Task[],IntVar[],IntVar,boolean,boolean)", m -> m.cumulative(tasks(m, 3), m.intVarArray("h", 3, 1, 2), v(m, "c", 2, 3), true, false).post());
+ s("setTimes(Task[])", m -> {
+ Task[] t = tasks(m, 2);
+ cum(m, t);
+ m.setTimes(t);
+ });
+ s("smallest(Task[]), smallest(Task[],ArbitrationRule), before(Task,Task,ArbitrationRule)", m -> {
+ Task[] t = tasks(m, 2);
+ cum(m, t);
+ m.smallest(t);
+ m.smallest(t, ISchedulingFactory.ArbitrationRule.MIN_ECT);
+ m.before(t[0], t[1], ISchedulingFactory.ArbitrationRule.MIN_EST);
+ });
+ }
+
+ //***********************************************************************************
+ // CHECKS
+ //***********************************************************************************
+
+ /**
+ * The modes in which the cases are checked: some factories build different constraints (or variables) with
+ * the SAT solver or with LCG.
+ */
+ private enum Mode {
+ DEFAULT, SAT, LCG;
+
+ Settings settings() {
+ switch (this) {
+ case SAT:
+ return SettingsBuilder.init().setEnableSAT(true).build();
+ case LCG:
+ return SettingsBuilder.init().setLCG(true).build();
+ default:
+ return SettingsBuilder.init().build();
+ }
+ }
+ }
+
+ @DataProvider
+ public Object[][] cases() {
+ List res = new ArrayList<>();
+ for (Mode mode : Mode.values()) {
+ for (Case c : CASES) {
+ res.add(new Object[]{mode, c});
+ }
+ }
+ return res.toArray(new Object[0][]);
+ }
+
+ @Test(groups = "1s", timeOut = 60000, dataProvider = "cases")
+ public void testCase(Mode mode, Case c) {
+ Settings settings = mode.settings();
+ // what the solver itself does not support (in this mode) is out of the scope of the duplication
+ Model direct = new Model("m", settings);
+ String stats;
+ try {
+ c.body().accept(direct);
+ stats = SpecTestUtils.solve(direct, 50_000);
+ } catch (Throwable e) {
+ throw new SkipException("not supported by the solver in mode " + mode + ": " + e);
+ }
+ direct = new Model("m", settings);
+ c.body().accept(direct);
+ RecordingModel recorded = new RecordingModel("m", settings);
+ c.body().accept(recorded);
+ if (c.name().contains("[unsupported by design]")) {
+ SpecException e = Assert.expectThrows(SpecException.class, recorded::snapshot);
+ Assert.assertTrue(e.getMessage().contains("lambda"), e.getMessage());
+ return;
+ }
+ ModelSpec spec = recorded.snapshot();
+ Model replayed = spec.instantiate().model();
+ String expected = SpecTestUtils.structure(direct);
+ Assert.assertEquals(SpecTestUtils.structure(recorded), expected, "recording changes the model");
+ Assert.assertEquals(SpecTestUtils.structure(replayed), expected, "the replayed model differs");
+ Assert.assertEquals(SpecTestUtils.solve(recorded, 50_000), stats, "the recorded model searches differently");
+ Assert.assertEquals(SpecTestUtils.solve(replayed, 50_000), stats, "the replayed model searches differently");
+ }
+
+ /**
+ * @return the factory methods dealing only with integer and boolean variables
+ */
+ private static Set scope() {
+ Set scope = new TreeSet<>();
+ for (Method m : IModel.class.getMethods()) {
+ if (!m.isDefault() || Modifier.isStatic(m.getModifiers()) || m.isSynthetic() || RecordingModel.EXCLUDED.contains(m.getName())) {
+ continue;
+ }
+ String types = Arrays.stream(m.getGenericParameterTypes()).map(Type::getTypeName)
+ .collect(Collectors.joining(" ")) + " " + m.getGenericReturnType().getTypeName();
+ boolean real = Arrays.stream(m.getParameterTypes()).anyMatch(p -> p == double.class || p == double[].class)
+ && !m.getName().equals("multiCostRegular");
+ String sig = signature(m.getName(), m.getParameterTypes());
+ if (OUT_OF_SCOPE.stream().noneMatch(types::contains) && !real && !NOT_IMPLEMENTED.contains(sig)) {
+ scope.add(sig);
+ }
+ }
+ return scope;
+ }
+
+ private static String signature(String name, Class>[] params) {
+ return name + Arrays.stream(params).map(Class::getSimpleName).collect(Collectors.joining(",", "(", ")"));
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testEveryIntFactoryMethodIsCovered() {
+ Set called = new TreeSet<>();
+ for (Mode mode : Mode.values()) {
+ for (Case c : CASES) {
+ record(mode, c, called);
+ }
+ }
+ List missing = scope().stream().filter(s -> !called.contains(s)).collect(Collectors.toList());
+ Assert.assertTrue(missing.isEmpty(), missing.size() + " factory methods are not covered: " + missing);
+ }
+
+ /**
+ * Run a case on a recording model and collect the factory methods called at top level.
+ */
+ private static void record(Mode mode, Case c, Set called) {
+ {
+ RecordingModel m = new RecordingModel("m", mode.settings());
+ try {
+ c.body().accept(m);
+ } catch (Throwable ignored) {
+ // not supported by the solver in this mode
+ return;
+ }
+ // the calls are recorded, whether journalizable or not
+ m.getRecorder().unsupported().forEach(u -> called.add(u.operation().replace(" ", "")));
+ try {
+ for (Step s : m.snapshot().steps()) {
+ if (s instanceof Step.Call) {
+ called.add(((Step.Call) s).method().toString().replace(" ", ""));
+ }
+ }
+ } catch (SpecException ignored) {
+ // unsupported by design
+ }
+ }
+ }
+}
diff --git a/solver/src/test/java/org/chocosolver/solver/spec/ModelSpecTest.java b/solver/src/test/java/org/chocosolver/solver/spec/ModelSpecTest.java
new file mode 100644
index 0000000000..01634f1a15
--- /dev/null
+++ b/solver/src/test/java/org/chocosolver/solver/spec/ModelSpecTest.java
@@ -0,0 +1,399 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.Solver;
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.constraints.extension.Tuples;
+import org.chocosolver.solver.constraints.nary.automata.FA.FiniteAutomaton;
+import org.chocosolver.solver.constraints.nary.cnf.LogOp;
+import org.chocosolver.solver.exception.SolverException;
+import org.chocosolver.solver.search.strategy.Search;
+import org.chocosolver.solver.variables.BoolVar;
+import org.chocosolver.solver.variables.IntVar;
+import org.chocosolver.solver.variables.Task;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+ * Tests of {@link ModelSpec} built by hand, without recorder.
+ *
+ * @author Charles Prud'homme
+ */
+public class ModelSpecTest {
+
+ /**
+ * Enumerate all solutions with a deterministic search.
+ *
+ * @return solutions, nodes and fails
+ */
+ private static String solveAll(Model model, IntVar... decisions) {
+ Solver s = model.getSolver();
+ s.setSearch(Search.inputOrderLBSearch(decisions));
+ while (s.solve()) ;
+ return s.getSolutionCount() + " sols, " + s.getNodeCount() + " nodes, " + s.getFailCount() + " fails";
+ }
+
+ private record Queens(ModelSpec spec, int[] q) {
+ }
+
+ private static Queens queensSpec(int n) {
+ SpecBuilder b = new SpecBuilder(Header.of("queens"));
+ int[] q = b.callArray(n, "intVarArray", "q", n, 1, n);
+ for (int i = 0; i < n - 1; i++) {
+ for (int j = i + 1; j < n; j++) {
+ b.post(b.call("arithm", b.ref(q[i]), "!=", b.ref(q[j])));
+ b.post(b.call("arithm", b.ref(q[i]), "!=", b.ref(q[j]), "+", j - i));
+ b.post(b.call("arithm", b.ref(q[i]), "!=", b.ref(q[j]), "-", j - i));
+ }
+ }
+ return new Queens(b.build(), q);
+ }
+
+ private static String queensDirect(int n) {
+ Model m = new Model("queens");
+ IntVar[] q = m.intVarArray("q", n, 1, n);
+ for (int i = 0; i < n - 1; i++) {
+ for (int j = i + 1; j < n; j++) {
+ m.arithm(q[i], "!=", q[j]).post();
+ m.arithm(q[i], "!=", q[j], "+", j - i).post();
+ m.arithm(q[i], "!=", q[j], "-", j - i).post();
+ }
+ }
+ return solveAll(m, q);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testQueensSameSearchAsDirectModel() {
+ Queens qs = queensSpec(8);
+ Resolver i1 = qs.spec().instantiate();
+ Resolver i2 = qs.spec().instantiate();
+ Assert.assertNotSame(i1.model(), i2.model());
+ Assert.assertEquals(i1.model().getNbVars(), 8);
+ Assert.assertEquals(i1.model().getNbCstrs(), 3 * 28);
+ String direct = queensDirect(8);
+ Assert.assertEquals(solveAll(i1.model(), i1.intVars(qs.q())), direct);
+ Assert.assertEquals(solveAll(i2.model(), i2.intVars(qs.q())), direct);
+ Assert.assertTrue(direct.startsWith("92 sols"));
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testInstancesAreIndependent() {
+ Queens qs = queensSpec(4);
+ Resolver i1 = qs.spec().instantiate();
+ Resolver i2 = qs.spec().instantiate();
+ for (int id : qs.q()) {
+ IntVar v1 = i1.intVar(id);
+ IntVar v2 = i2.intVar(id);
+ Assert.assertNotSame(v1, v2);
+ Assert.assertSame(v1.getModel(), i1.model());
+ Assert.assertSame(v2.getModel(), i2.model());
+ Assert.assertEquals(i1.idOf(v1), id);
+ Assert.assertEquals(i1.idOf(v2), -1);
+ }
+ }
+
+ @Test(groups = "10s", timeOut = 120000)
+ public void testConcurrentInstantiations() throws Exception {
+ Queens qs = queensSpec(8);
+ String direct = queensDirect(8);
+ ExecutorService pool = Executors.newFixedThreadPool(8);
+ try {
+ List> futures = new ArrayList<>();
+ for (int k = 0; k < 16; k++) {
+ futures.add(pool.submit(() -> {
+ Resolver inst = qs.spec().instantiate();
+ return solveAll(inst.model(), inst.intVars(qs.q()));
+ }));
+ }
+ for (Future f : futures) {
+ Assert.assertEquals(f.get(), direct);
+ }
+ } finally {
+ pool.shutdownNow();
+ }
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testHeader() {
+ Header h = Header.of("h").withSeed(42);
+ Model m = new ModelSpec(h, List.of()).instantiate().model();
+ Assert.assertEquals(m.getName(), "h");
+ Assert.assertEquals(m.getSeed(), 42L);
+ Assert.assertSame(m.getSettings(), h.settings());
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testVarargsAndAllDifferent() {
+ SpecBuilder b = new SpecBuilder(Header.of("ad"));
+ int[] x = b.callArray(3, "intVarArray", "x", 3, 1, 3);
+ b.post(b.call("allDifferent", b.refs(IntVar.class, x)));
+ Resolver inst = b.build().instantiate();
+ Assert.assertEquals(solveAll(inst.model(), inst.intVars(x)).split(",")[0], "6 sols");
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testReificationSteps() {
+ // direct
+ Model m = new Model();
+ IntVar x = m.intVar("x", 0, 5);
+ Constraint c1 = m.arithm(x, ">", 2);
+ BoolVar b1 = c1.reify();
+ BoolVar b2 = m.boolVar("b2");
+ m.arithm(x, "<", 4).reifyWith(b2);
+ BoolVar b3 = m.boolVar("b3");
+ m.arithm(x, "=", 0).implies(b3);
+ BoolVar b4 = m.boolVar("b4");
+ m.arithm(x, "=", 5).impliedBy(b4);
+ m.sum(new BoolVar[]{b1, b2}, "=", 1).post();
+ String direct = solveAll(m, x, b1, b2, b3, b4);
+ // spec
+ SpecBuilder b = new SpecBuilder(Header.of("reif"));
+ int ix = b.call("intVar", "x", 0, 5);
+ int ib1 = b.reify(b.call("arithm", b.ref(ix), ">", 2));
+ int ib2 = b.call("boolVar", "b2");
+ b.reifyWith(b.call("arithm", b.ref(ix), "<", 4), ib2);
+ int ib3 = b.call("boolVar", "b3");
+ b.implies(b.call("arithm", b.ref(ix), "=", 0), ib3);
+ int ib4 = b.call("boolVar", "b4");
+ b.impliedBy(b.call("arithm", b.ref(ix), "=", 5), ib4);
+ b.post(b.call("sum", b.refs(BoolVar.class, ib1, ib2), "=", 1));
+ Resolver inst = b.build().instantiate();
+ Assert.assertEquals(inst.model().getNbCstrs(), m.getNbCstrs());
+ Assert.assertEquals(solveAll(inst.model(), inst.intVars(ix, ib1, ib2, ib3, ib4)), direct);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testUnpost() {
+ SpecBuilder b = new SpecBuilder(Header.of("unpost"));
+ int x = b.call("intVar", "x", 0, 3);
+ int c = b.call("arithm", b.ref(x), ">", 1);
+ b.post(c).unpost(c);
+ Resolver inst = b.build().instantiate();
+ Assert.assertEquals(inst.model().getNbCstrs(), 0);
+ Assert.assertEquals(solveAll(inst.model(), inst.intVar(x)).split(",")[0], "4 sols");
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testTableSharesFrozenTuples() {
+ Tuples tuples = new Tuples(true);
+ tuples.add(1, 2);
+ tuples.add(2, 1);
+ tuples.add(3, 3);
+ SpecBuilder b = new SpecBuilder(Header.of("table"));
+ int[] x = b.callArray(2, "intVarArray", "x", 2, 1, 3);
+ b.post(b.call("table", b.refs(IntVar.class, x), tuples));
+ ModelSpec spec = b.build();
+ Assert.assertTrue(tuples.isFrozen());
+ Assert.assertThrows(SolverException.class, () -> tuples.add(1, 1));
+ Assert.assertThrows(SolverException.class, tuples::sort);
+ for (int k = 0; k < 2; k++) {
+ Resolver inst = spec.instantiate();
+ Assert.assertEquals(solveAll(inst.model(), inst.intVars(x)).split(",")[0], "3 sols");
+ }
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testArraysAreCopied() {
+ int[] coeffs = {1, 2, 3};
+ SpecBuilder b = new SpecBuilder(Header.of("scalar"));
+ int[] x = b.callArray(3, "intVarArray", "x", 3, 0, 3);
+ b.post(b.call("scalar", b.refs(IntVar.class, x), coeffs, "=", 6));
+ ModelSpec spec = b.build();
+ coeffs[0] = 100;
+ Model m = new Model();
+ IntVar[] y = m.intVarArray("x", 3, 0, 3);
+ m.scalar(y, new int[]{1, 2, 3}, "=", 6).post();
+ Resolver inst = spec.instantiate();
+ Assert.assertEquals(solveAll(inst.model(), inst.intVars(x)), solveAll(m, y));
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testAutomatonIsCopied() {
+ FiniteAutomaton fa = new FiniteAutomaton("(1|2)3*1");
+ SpecBuilder b = new SpecBuilder(Header.of("regular"));
+ int[] x = b.callArray(4, "intVarArray", "x", 4, 0, 3);
+ b.post(b.call("regular", b.refs(IntVar.class, x), fa));
+ ModelSpec spec = b.build();
+ Step.Call call = (Step.Call) spec.steps().get(1);
+ Object captured = ((Arg.Const) call.args()[1]).value();
+ Assert.assertNotSame(captured, fa);
+ Assert.assertNotSame(((Arg.Const) call.args()[1]).value(), captured);
+ Model m = new Model();
+ IntVar[] y = m.intVarArray("x", 4, 0, 3);
+ m.regular(y, fa).post();
+ String direct = solveAll(m, y);
+ Assert.assertTrue(direct.startsWith("2 sols"));
+ Resolver inst = spec.instantiate();
+ Assert.assertEquals(solveAll(inst.model(), inst.intVars(x)), direct);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testAutomatonCloneDoesNotShareBuffer() throws Exception {
+ FiniteAutomaton fa = new FiniteAutomaton("(1|2)3*1");
+ FiniteAutomaton copy = fa.clone();
+ Field nexts = FiniteAutomaton.class.getDeclaredField("nexts");
+ nexts.setAccessible(true);
+ Assert.assertNotSame(nexts.get(copy), nexts.get(fa));
+ Assert.assertEquals(copy.isDeterministic(), fa.isDeterministic());
+ Assert.assertEquals(copy.getTransitions().size(), fa.getTransitions().size());
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testMatrixArgumentAndOutput() {
+ Model m = new Model();
+ IntVar[][] y = m.intVarMatrix("m", 2, 3, 0, 2);
+ m.lexChainLess(y).post();
+ String direct = solveAll(m, Arrays.stream(y).flatMap(Arrays::stream).toArray(IntVar[]::new));
+ SpecBuilder b = new SpecBuilder(Header.of("lex"));
+ int[] ids = b.callArray(6, "intVarMatrix", "m", 2, 3, 0, 2);
+ int[][] rows = {Arrays.copyOfRange(ids, 0, 3), Arrays.copyOfRange(ids, 3, 6)};
+ b.post(b.call("lexChainLess", b.refs(IntVar.class, rows)));
+ Resolver inst = b.build().instantiate();
+ Assert.assertEquals(solveAll(inst.model(), inst.intVars(ids)), direct);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testTasksAndCumulative() {
+ int[] durations = {2, 3, 1};
+ int[] heights = {1, 2, 2};
+ Model m = new Model();
+ IntVar[] s = new IntVar[3];
+ Task[] tasks = new Task[3];
+ IntVar[] h = new IntVar[3];
+ for (int i = 0; i < 3; i++) {
+ s[i] = m.intVar("s" + i, 0, 4);
+ IntVar e = m.intVar("e" + i, 0, 6);
+ tasks[i] = new Task(s[i], m.intVar(durations[i]), e);
+ h[i] = m.intVar(heights[i]);
+ }
+ m.cumulative(tasks, h, m.intVar(3)).post();
+ String direct = solveAll(m, s);
+
+ SpecBuilder b = new SpecBuilder(Header.of("cumulative"));
+ int[] is = new int[3];
+ int[] it = new int[3];
+ int[] ih = new int[3];
+ for (int i = 0; i < 3; i++) {
+ is[i] = b.call("intVar", "s" + i, 0, 4);
+ int ie = b.call("intVar", "e" + i, 0, 6);
+ it[i] = b.newTask(is[i], b.call("intVar", durations[i]), ie);
+ ih[i] = b.call("intVar", heights[i]);
+ }
+ b.post(b.call("cumulative", b.refs(Task.class, it), b.refs(IntVar.class, ih), b.ref(b.call("intVar", 3))));
+ Resolver inst = b.build().instantiate();
+ Assert.assertEquals(inst.model().getNbVars(), m.getNbVars());
+ Assert.assertEquals(solveAll(inst.model(), inst.intVars(is)), direct);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testClauses() {
+ Model m = new Model();
+ BoolVar[] x = m.boolVarArray("b", 3);
+ m.addClauses(LogOp.or(LogOp.and(x[0], x[1]), LogOp.nor(x[2])));
+ String direct = solveAll(m, x);
+ SpecBuilder b = new SpecBuilder(Header.of("clauses"));
+ int[] ix = b.callArray(3, "boolVarArray", "b", 3);
+ b.effect("addClauses", Arg.or(Arg.and(b.ref(ix[0]), b.ref(ix[1])), new Arg.Logic(false, true, new Arg[]{b.ref(ix[2])})));
+ Resolver inst = b.build().instantiate();
+ Assert.assertEquals(solveAll(inst.model(), inst.intVars(ix)), direct);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testCustom() {
+ SpecBuilder b = new SpecBuilder(Header.of("custom"));
+ int x = b.call("intVar", "x", 0, 3);
+ b.post(b.custom("gt1", r -> r.model().arithm(r.intVar(x), ">", 1)));
+ Resolver inst = b.build().instantiate();
+ Assert.assertEquals(solveAll(inst.model(), inst.intVar(x)).split(",")[0], "2 sols");
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testMostSpecificOverload() {
+ SpecBuilder b = new SpecBuilder(Header.of("overload"));
+ int[] x = b.callArray(3, "boolVarArray", "b", 3);
+ // sum(IntVar[], ...) and sum(BoolVar[], ...) both apply: the latter is chosen
+ b.post(b.call("sum", b.refs(BoolVar.class, x), "=", 2));
+ ModelSpec spec = b.build();
+ Step.Call call = (Step.Call) spec.steps().get(1);
+ Assert.assertEquals(call.method().parameterTypes()[0], BoolVar[].class);
+ Resolver inst = spec.instantiate();
+ Assert.assertEquals(solveAll(inst.model(), inst.intVars(x)).split(",")[0], "3 sols");
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testCachedConstantsAreAliased() {
+ SpecBuilder b = new SpecBuilder(Header.of("alias"));
+ int c1 = b.call("intVar", 5);
+ int c2 = b.call("intVar", 5);
+ Resolver inst = b.build().instantiate();
+ Assert.assertSame(inst.get(c1), inst.get(c2));
+ Assert.assertEquals(inst.idOf(inst.get(c2)), c1);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testDivergenceIsDetected() {
+ FactoryMethod intVar = FactoryMethod.of("intVar", String.class, int.class, int.class);
+ Arg[] args = {new Arg.Const("x"), new Arg.Const(0), new Arg.Const(1)};
+ ModelSpec spec = new ModelSpec(Header.of("div"), List.of(
+ new Step.Call(new int[]{0}, intVar, args),
+ new Step.Call(new int[]{0}, intVar, args)));
+ SpecException e = Assert.expectThrows(SpecException.class, spec::instantiate);
+ Assert.assertTrue(e.getMessage().contains("Divergence"), e.getMessage());
+ Assert.assertTrue(e.getMessage().startsWith("Step #1"), e.getMessage());
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testIllFormedSpecs() {
+ // undefined identifier
+ Assert.assertThrows(SpecException.class, () -> new ModelSpec(Header.of("e"), List.of(new Step.Post(new int[]{3}))));
+ // wrong number of arguments
+ FactoryMethod intVar = FactoryMethod.of("intVar", String.class, int.class, int.class);
+ Assert.assertThrows(SpecException.class, () -> new Step.Call(new int[]{0}, intVar, new Arg[]{new Arg.Const("x")}));
+ // not a factory method
+ Assert.assertThrows(SpecException.class, () -> FactoryMethod.of("getSolver"));
+ // unsupported (mutable) constant; an object without field is immutable, hence supported
+ Assert.assertThrows(SpecException.class, () -> new Arg.Const(new StringBuilder()));
+ new Arg.Const(new Object());
+ // a variable where a constraint is expected
+ SpecBuilder b = new SpecBuilder(Header.of("e"));
+ int x = b.call("intVar", "x", 0, 1);
+ ModelSpec spec = b.post(x).build();
+ Assert.assertThrows(SpecException.class, spec::instantiate);
+ // a constraint where a variable is expected
+ FactoryMethod arithm = FactoryMethod.of("arithm", org.chocosolver.solver.variables.IntVar.class, String.class, int.class);
+ SpecBuilder b2 = new SpecBuilder(Header.of("e"));
+ int y = b2.call("intVar", "y", 0, 1);
+ int c = b2.call("arithm", b2.ref(y), ">", 0);
+ b2.call(arithm, b2.ref(c), ">", 0);
+ Assert.assertThrows(SpecException.class, () -> b2.build().instantiate());
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testNoSharedMutableObject() {
+ Queens qs = queensSpec(5);
+ Resolver i1 = qs.spec().instantiate();
+ Resolver i2 = qs.spec().instantiate();
+ Set seen = java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>());
+ seen.addAll(Arrays.asList(i1.model().getVars()));
+ seen.addAll(Arrays.asList(i1.model().getCstrs()));
+ for (Object o : i2.model().getVars()) Assert.assertFalse(seen.contains(o));
+ for (Object o : i2.model().getCstrs()) Assert.assertFalse(seen.contains(o));
+ Assert.assertEquals(new HashSet<>(Arrays.asList(i1.model().getVars())).size(), 5);
+ }
+}
diff --git a/solver/src/test/java/org/chocosolver/solver/spec/RecordedModelTest.java b/solver/src/test/java/org/chocosolver/solver/spec/RecordedModelTest.java
new file mode 100644
index 0000000000..3d46874c6c
--- /dev/null
+++ b/solver/src/test/java/org/chocosolver/solver/spec/RecordedModelTest.java
@@ -0,0 +1,128 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.ParallelPortfolio;
+import org.chocosolver.solver.Solution;
+import org.chocosolver.solver.exception.SolverException;
+import org.chocosolver.solver.search.strategy.Search;
+import org.chocosolver.solver.search.strategy.strategy.AbstractStrategy;
+import org.chocosolver.solver.variables.IntVar;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.util.List;
+
+/**
+ * The simple usage: {@link Model#record(String)}, {@link Model#duplicate()} and
+ * {@link ParallelPortfolio#of(Model, int)}, without knowing anything about specs.
+ *
+ * @author Charles Prud'homme
+ */
+public class RecordedModelTest {
+
+ private static IntVar[] queens(Model model, int n) {
+ IntVar[] q = model.intVarArray("q", n, 1, n);
+ for (int i = 0; i < n - 1; i++) {
+ for (int j = i + 1; j < n; j++) {
+ model.arithm(q[i], "!=", q[j]).post();
+ model.arithm(q[i], "!=", q[j], "+", j - i).post();
+ model.arithm(q[i], "!=", q[j], "-", j - i).post();
+ }
+ }
+ return q;
+ }
+
+ private static void assertQueens(Solution s, IntVar[] q) {
+ for (int i = 0; i < q.length - 1; i++) {
+ for (int j = i + 1; j < q.length; j++) {
+ int a = s.getIntVal(q[i]);
+ int b = s.getIntVal(q[j]);
+ Assert.assertTrue(a != b && a != b + j - i && a != b - j + i);
+ }
+ }
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testDuplicate() {
+ Model model = Model.record("queens");
+ queens(model, 8);
+
+ Model copy = model.duplicate();
+ Assert.assertNotSame(copy, model);
+ Assert.assertEquals(copy.getNbVars(), model.getNbVars());
+ Assert.assertEquals(copy.getNbCstrs(), model.getNbCstrs());
+ Assert.assertEquals(copy.getSolver().findAllSolutions().size(), 92);
+ // the original is untouched, and can be duplicated again
+ Assert.assertEquals(model.duplicate().getSolver().findAllSolutions().size(), 92);
+ Assert.assertEquals(model.getSolver().findAllSolutions().size(), 92);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testPortfolio() {
+ Model model = Model.record("queens");
+ IntVar[] q = queens(model, 10);
+
+ ParallelPortfolio portfolio = ParallelPortfolio.of(model, 4);
+ Assert.assertEquals(portfolio.getModels().size(), 4);
+ Assert.assertSame(portfolio.getModels().get(0), model, "the original model is the first worker");
+ Assert.assertTrue(portfolio.solve());
+ // the solution is expressed with the variables of the original model
+ assertQueens(portfolio.getBestSolution(), q);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testOptimizationPortfolio() {
+ Model model = Model.record("knapsack");
+ IntVar[] occ = model.intVarArray("o", 3, 0, 5);
+ IntVar value = model.intVar("v", 0, 100);
+ model.knapsack(occ, model.intVar("w", 0, 10), value, new int[]{2, 3, 4}, new int[]{3, 5, 7}).post();
+ model.setObjective(Model.MAXIMIZE, value);
+
+ ParallelPortfolio portfolio = ParallelPortfolio.of(model, List.of(Variant.IDENTITY, Variant.seed(3)));
+ Assert.assertEquals(portfolio.getModels().size(), 3, "the original model and one copy per variant");
+ int best = -1;
+ while (portfolio.solve()) {
+ Solution s = portfolio.getBestSolution();
+ best = s.getIntVal(value);
+ Assert.assertEquals(best, 3 * s.getIntVal(occ[0]) + 5 * s.getIntVal(occ[1]) + 7 * s.getIntVal(occ[2]));
+ }
+ Assert.assertEquals(best, 17);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testOriginalSearchIsKept() {
+ Model model = Model.record("queens");
+ IntVar[] q = queens(model, 8);
+ AbstractStrategy search = Search.inputOrderUBSearch(q);
+ model.getSolver().setSearch(search);
+ ParallelPortfolio portfolio = ParallelPortfolio.of(model, 2);
+ Assert.assertTrue(portfolio.solve());
+ Assert.assertSame(model.getSolver().getSearch(), search, "the search declared on the original model is kept");
+ assertQueens(portfolio.getBestSolution(), q);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testSolvedModelCannotBeDuplicated() {
+ Model model = Model.record("queens");
+ queens(model, 4);
+ Assert.assertNotNull(model.duplicate());
+ Assert.assertTrue(model.getSolver().solve());
+ // once its resolution has started, the model cannot be duplicated anymore
+ Assert.assertThrows(SolverException.class, model::duplicate);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testPlainModelCannotBeDuplicated() {
+ Model model = Model.create("plain");
+ queens(model, 4);
+ SolverException e = Assert.expectThrows(SolverException.class, model::duplicate);
+ Assert.assertTrue(e.getMessage().contains("Model.record"), e.getMessage());
+ Assert.assertThrows(SolverException.class, () -> ParallelPortfolio.of(model, 2));
+ }
+}
diff --git a/solver/src/test/java/org/chocosolver/solver/spec/RecorderTest.java b/solver/src/test/java/org/chocosolver/solver/spec/RecorderTest.java
new file mode 100644
index 0000000000..385835d0e0
--- /dev/null
+++ b/solver/src/test/java/org/chocosolver/solver/spec/RecorderTest.java
@@ -0,0 +1,358 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.constraints.binary.PropGreaterOrEqualX_Y;
+import org.chocosolver.solver.constraints.extension.Tuples;
+import org.chocosolver.solver.constraints.nary.automata.FA.CostAutomaton;
+import org.chocosolver.solver.constraints.nary.automata.FA.FiniteAutomaton;
+import org.chocosolver.solver.constraints.nary.cnf.LogOp;
+import org.chocosolver.solver.exception.SolverException;
+import org.chocosolver.solver.variables.BoolVar;
+import org.chocosolver.solver.variables.IntVar;
+import org.chocosolver.solver.variables.OptionalTask;
+import org.chocosolver.solver.variables.Task;
+import org.testng.Assert;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+
+/**
+ * Tests of {@link RecordingModel} and {@link Recorder}: a model built directly and the same model recorded,
+ * snapshot and instantiated must be identical.
+ *
+ * @author Charles Prud'homme
+ */
+public class RecorderTest {
+
+ /**
+ * Build the model directly and through a snapshot, check they are identical and return the spec.
+ */
+ private static ModelSpec roundTrip(Consumer builder) {
+ Model direct = new Model("m");
+ builder.accept(direct);
+ RecordingModel recorded = new RecordingModel("m");
+ builder.accept(recorded);
+ ModelSpec spec = recorded.snapshot();
+ Model replayed = spec.instantiate().model();
+ String expected = SpecTestUtils.structure(direct);
+ Assert.assertEquals(SpecTestUtils.structure(recorded), expected, "recording must not change the model");
+ Assert.assertEquals(SpecTestUtils.structure(replayed), expected, "replay differs");
+ Assert.assertEquals(replayed.getSeed(), direct.getSeed());
+ String stats = SpecTestUtils.solve(direct, 100_000);
+ Assert.assertEquals(SpecTestUtils.solve(recorded, 100_000), stats);
+ Assert.assertEquals(SpecTestUtils.solve(replayed, 100_000), stats);
+ return spec;
+ }
+
+ //***********************************************************************************
+ // SCENARIOS
+ //***********************************************************************************
+
+ private static void queens(Model m) {
+ int n = 6;
+ IntVar[] q = m.intVarArray("q", n, 1, n);
+ IntVar[] d1 = new IntVar[n];
+ IntVar[] d2 = new IntVar[n];
+ for (int i = 0; i < n; i++) {
+ d1[i] = m.offset(q[i], i);
+ d2[i] = m.offset(q[i], -i);
+ }
+ m.allDifferent(q, "AC").post();
+ m.allDifferent(d1, "BC").post();
+ m.allDifferent(d2).post();
+ }
+
+ private static void sideEffects(Model m) {
+ IntVar[] x = m.intVarArray("x", 5, 0, 4);
+ IntVar v = m.intVar("v", 0, 10);
+ IntVar i = m.intVar("i", 0, 6);
+ m.element(v, new int[]{3, 1, 4, 1, 5}, i, 2).post();
+ m.among(m.intVar("n", 0, 5), x, new int[]{1, 3}).post();
+ m.globalCardinality(x, new int[]{0, 1}, m.intVarArray("o", 2, 0, 2), false).post();
+ m.nValues(x, m.intVar("nv", 2, 3)).post();
+ m.sum(x, "<=", m.intVar("s", 0, 12)).post();
+ m.increasing(Arrays.copyOf(x, 3), 0).post();
+ m.count(2, x, m.intVar("c", 1, 2)).post();
+ m.mod(x[3], 3, x[4]).post();
+ m.scalar(x, new int[]{1, 2, 3, 4, 5}, ">=", 10).post();
+ }
+
+ private static void reification(Model m) {
+ IntVar x = m.intVar("x", 0, 5);
+ IntVar y = m.intVar("y", 0, 5);
+ BoolVar b1 = m.arithm(x, ">", y).reify();
+ BoolVar b2 = m.boolVar("b2");
+ m.arithm(x, "=", 2).reifyWith(b2);
+ BoolVar b3 = m.boolVar("b3");
+ m.arithm(y, "=", 0).implies(b3);
+ BoolVar b4 = m.boolVar("b4");
+ m.arithm(y, "=", 5).impliedBy(b4);
+ m.or(m.arithm(x, "=", y), m.arithm(x, "=", 0)).post();
+ m.and(b1.not(), b3).post();
+ m.ifThen(m.arithm(x, "=", 1), m.arithm(y, "=", 1));
+ m.not(m.arithm(x, "=", 4)).post();
+ Constraint c = m.arithm(x, "<", 5);
+ BoolVar again1 = c.reify();
+ BoolVar again2 = c.reify();
+ Assert.assertSame(again1, again2);
+ m.sum(new BoolVar[]{b1, b2, b4, again1}, ">=", 1).post();
+ }
+
+ private static void expressions(Model m) {
+ IntVar x = m.intVar("x", 0, 5);
+ IntVar y = m.intVar("y", 0, 5);
+ IntVar z = m.intVar("z", 0, 12);
+ x.add(y).mul(2).eq(z).post();
+ x.gt(y).or(z.lt(3)).post();
+ x.sub(y).abs().ge(2).extension().post();
+ IntVar xy = x.add(y).intVar();
+ m.arithm(xy, "<=", 8).post();
+ x.mul(y).ne(4).decompose().post();
+ }
+
+ private static void views(Model m) {
+ IntVar x = m.intVar("x", -3, 3);
+ BoolVar b = m.boolVar("b");
+ m.arithm(m.intMinusView(x), "<=", 2).post();
+ m.arithm(m.intScaleView(x, 2), "!=", 2).post();
+ m.arithm(m.intAbsView(x), ">", 0).post();
+ BoolVar eq = m.isEq(x, 1);
+ m.arithm(eq, "!=", b).post();
+ m.arithm(b.not(), "=", m.isLeq(x, 0)).post();
+ m.arithm(m.intVar(5), ">", x).post();
+ m.arithm(m.intVar(5), "!=", m.intVar("w", 4, 6)).post();
+ }
+
+ private static void scheduling(Model m) {
+ IntVar[] s = m.intVarArray("s", 3, 0, 5);
+ Task t0 = new Task(s[0], 2);
+ Task t1 = m.taskVar(s[1], 3);
+ Task t2 = new Task(s[2], m.intVar("d2", 1, 2), m.intVar("e2", 0, 8));
+ OptionalTask t3 = new OptionalTask(m.intVar("s3", 0, 5), 2);
+ m.cumulative(new Task[]{t0, t1, t2, t3}, m.intVarArray("h", 4, 1, 1), m.intVar(2)).post();
+ m.cumulative(s, new int[]{1, 1, 2}, new int[]{1, 1, 1}, 2).post();
+ }
+
+ private static void sat(Model m) {
+ BoolVar[] b = m.boolVarArray("b", 4);
+ m.addClauses(LogOp.or(LogOp.and(b[0], b[1]), LogOp.nor(b[2])));
+ m.addClauseTrue(b[3]);
+ m.addClausesBoolOrArrayEqVar(new BoolVar[]{b[0], b[2]}, b[1]);
+ m.addClauses(new BoolVar[]{b[0]}, new BoolVar[]{b[2]});
+ }
+
+ private static void data(Model m) {
+ IntVar[] x = m.intVarArray("x", 3, 0, 3);
+ Tuples t = new Tuples(true);
+ t.add(0, 1, 2);
+ t.add(1, 2, 3);
+ t.add(3, 3, 3);
+ m.table(x, t, "CT+").post();
+ m.table(new IntVar[]{x[0], x[1]}, new Tuples(new int[][]{{0, 1}, {1, 2}, {3, 3}}, false, java.util.OptionalInt.empty())).post();
+ IntVar[] y = m.intVarArray("y", 4, 0, 3);
+ m.regular(y, new FiniteAutomaton("(1|2)3*1")).post();
+ FiniteAutomaton fa = new FiniteAutomaton("(0|1)*");
+ int[][] costs = new int[4][4];
+ for (int[] c : costs) Arrays.fill(c, 1);
+ costs[1][1] = 0;
+ m.costRegular(m.intVarArray("z", 4, 0, 1), m.intVar("cost", 0, 3),
+ CostAutomaton.makeSingleResource(fa, costs, 0, 3)).post();
+ m.lexChainLess(m.intVarMatrix("mat", 2, 2, 0, 1)).post();
+ }
+
+ private static void objectiveHooksSeed(Model m) {
+ m.addHook("CUMULATIVE", "GLB");
+ IntVar[] x = m.intVarArray("x", 4, 0, 5);
+ m.setSeed(42);
+ m.allDifferent(x).post();
+ IntVar obj = m.intVar("obj", 0, 20);
+ m.sum(x, "=", obj).post();
+ m.setObjective(Model.MINIMIZE, obj);
+ Constraint tmp = m.arithm(x[0], ">", 3);
+ tmp.post();
+ m.unpost(tmp);
+ }
+
+ private static void custom(Model m) {
+ IntVar[] x = m.intVarArray("x", 2, 0, 3);
+ BoolVar[] b = m.boolVarArray("b", 2);
+ m.custom("geq", x, PropGreaterOrEqualX_Y::new).post();
+ // the propagator is built from an array of the recorded type
+ m.custom("geqBool", b, PropGreaterOrEqualX_Y::new).post();
+ m.custom("geqLambda", new IntVar[]{x[1], b[0]}, vs -> new PropGreaterOrEqualX_Y(vs)).post();
+ }
+
+ private static void nameAndHooks(Model m) {
+ m.setName("renamed");
+ m.addHook("CUMULATIVE", "GLB");
+ m.removeAllHooks();
+ m.addHook("other", 1);
+ m.allDifferent(m.intVarArray("x", 3, 0, 2)).post();
+ }
+
+ private static void groups(Model m) {
+ IntVar[] x = m.intVarArray("x", 4, 0, 3);
+ IntVar[] y = m.intVarArray("y", 3, 0, 3);
+ m.addAsGroup("x", x);
+ m.addAsGroup("y", y);
+ m.allDifferent(x).post();
+ m.sum(y, "=", x[0]).post();
+ }
+
+ @DataProvider
+ public Object[][] scenarios() {
+ return new Object[][]{
+ {"queens", (Consumer) RecorderTest::queens},
+ {"sideEffects", (Consumer) RecorderTest::sideEffects},
+ {"reification", (Consumer) RecorderTest::reification},
+ {"expressions", (Consumer) RecorderTest::expressions},
+ {"views", (Consumer) RecorderTest::views},
+ {"scheduling", (Consumer) RecorderTest::scheduling},
+ {"sat", (Consumer) RecorderTest::sat},
+ {"data", (Consumer) RecorderTest::data},
+ {"objectiveHooksSeed", (Consumer) RecorderTest::objectiveHooksSeed},
+ {"groups", (Consumer) RecorderTest::groups},
+ {"custom", (Consumer) RecorderTest::custom},
+ {"nameAndHooks", (Consumer) RecorderTest::nameAndHooks},
+ };
+ }
+
+ @Test(groups = "1s", timeOut = 60000, dataProvider = "scenarios")
+ public void testRoundTrip(String name, Consumer builder) {
+ roundTrip(builder);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testHooksSeedAndObjectiveAreReplayed() {
+ ModelSpec spec = roundTrip(RecorderTest::objectiveHooksSeed);
+ Model m = spec.instantiate().model();
+ Assert.assertEquals(m.getHook("CUMULATIVE"), "GLB");
+ Assert.assertEquals(m.getSeed(), 42L);
+ Assert.assertNotNull(m.getObjective());
+ Assert.assertEquals(m.getObjective().getName(), "obj");
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testNameAndHooksAreReplayed() {
+ ModelSpec spec = roundTrip(RecorderTest::nameAndHooks);
+ Assert.assertEquals(spec.header().name(), "renamed");
+ Model m = spec.instantiate().model();
+ Assert.assertEquals(m.getName(), "renamed");
+ Assert.assertNull(m.getHook("CUMULATIVE"));
+ Assert.assertEquals(m.getHook("other"), 1);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testCapturingCustomConstraintIsReported() {
+ RecordingModel m = new RecordingModel("m");
+ IntVar[] x = m.intVarArray("x", 2, 0, 3);
+ m.custom("captures", x, vs -> new PropGreaterOrEqualX_Y(x)).post();
+ SpecException e = Assert.expectThrows(SpecException.class, m::snapshot);
+ Assert.assertTrue(e.getMessage().contains("capturing lambda"), e.getMessage());
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testOnlyTopLevelCallsAreRecorded() {
+ RecordingModel m = new RecordingModel("m");
+ IntVar[] x = m.intVarArray("x", 50, 0, 5); // 1 call
+ m.sum(x, "=", 30, 5).post(); // 1 call + 1 post, despite the decomposition
+ m.globalCardinality(x, new int[]{0, 1}, m.intVarArray("o", 2, 0, 50), true).post(); // 2 calls + 1 post
+ List steps = m.snapshot().steps();
+ Assert.assertEquals(steps.size(), 6, steps.toString());
+ Assert.assertTrue(m.getNbVars() > 52, "decomposition should create intermediate variables");
+ Assert.assertTrue(steps.get(0) instanceof Step.Call);
+ Assert.assertEquals(((Step.Call) steps.get(0)).out().length, 50);
+ Assert.assertTrue(steps.get(2) instanceof Step.Post);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testCustomConstraintIsReported() {
+ RecordingModel m = new RecordingModel("m");
+ IntVar[] x = m.intVarArray("x", 2, 0, 3);
+ new Constraint("MyCustom", new PropGreaterOrEqualX_Y(x)).post();
+ Assert.assertEquals(m.getRecorder().unsupported().size(), 1);
+ SpecException e = Assert.expectThrows(SpecException.class, m::snapshot);
+ Assert.assertTrue(e.getMessage().contains("MyCustom"), e.getMessage());
+ Assert.assertTrue(e.getMessage().contains("Model.custom"), e.getMessage());
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testUnsupportedArgumentsAreReported() {
+ RecordingModel m = new RecordingModel("m");
+ IntVar[] x = m.intVarArray("x", 3, 0, 3);
+ // a capturing lambda cannot be journalized (it may capture objects of the recorded model)
+ int threshold = x.length - 2;
+ m.allDifferentUnderCondition(x, v -> v.getUB() > threshold, true).post();
+ // a variable of another model
+ Model other = new Model();
+ try {
+ m.arithm(x[0], "=", other.intVar("o", 0, 1)).post();
+ } catch (SolverException | AssertionError ignored) {
+ // the model itself rejects it (assertion of Model.post)
+ }
+ List u = m.getRecorder().unsupported();
+ Assert.assertTrue(u.size() >= 2, u.toString());
+ Assert.assertTrue(u.get(0).operation().startsWith("allDifferentUnderCondition"), u.toString());
+ Assert.assertThrows(SpecException.class, m::snapshot);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testSnapshotAfterSolveFails() {
+ RecordingModel m = new RecordingModel("m");
+ m.arithm(m.intVar("x", 0, 3), ">", 1).post();
+ m.getSolver().solve();
+ Assert.assertThrows(SpecException.class, m::snapshot);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testTuplesAreFrozenByRecording() {
+ RecordingModel m = new RecordingModel("m");
+ Tuples t = new Tuples(true);
+ t.add(0, 1);
+ m.table(m.intVarArray("x", 2, 0, 1), t).post();
+ Assert.assertTrue(t.isFrozen());
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testSnapshotIsReusableConcurrently() throws Exception {
+ RecordingModel master = new RecordingModel("m");
+ sideEffects(master);
+ reification(master);
+ ModelSpec spec = master.snapshot();
+ String expected = SpecTestUtils.solve(spec.instantiate().model(), 100_000);
+ ExecutorService pool = Executors.newFixedThreadPool(4);
+ try {
+ List> futures = new ArrayList<>();
+ for (int k = 0; k < 8; k++) {
+ futures.add(pool.submit(() -> SpecTestUtils.solve(spec.instantiate().model(), 100_000)));
+ }
+ for (Future f : futures) {
+ Assert.assertEquals(f.get(), expected);
+ }
+ } finally {
+ pool.shutdownNow();
+ }
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testPlainModelHasNoRecorder() {
+ Model m = new Model();
+ Assert.assertNull(m.getRecorder());
+ BoolVar b = m.arithm(m.intVar("x", 0, 3), ">", 1).reify();
+ Assert.assertNotNull(b);
+ }
+}
diff --git a/solver/src/test/java/org/chocosolver/solver/spec/SearchDeclTest.java b/solver/src/test/java/org/chocosolver/solver/spec/SearchDeclTest.java
new file mode 100644
index 0000000000..fcfacd5b1c
--- /dev/null
+++ b/solver/src/test/java/org/chocosolver/solver/spec/SearchDeclTest.java
@@ -0,0 +1,188 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.Solution;
+import org.chocosolver.solver.Solver;
+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.variables.IntVar;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+
+/**
+ * Tests of the search, objective and solutions of a {@link ModelSpec}.
+ *
+ * @author Charles Prud'homme
+ */
+public class SearchDeclTest {
+
+ private static final SearchParams.VarSelConf DOMWDEG =
+ new SearchParams.VarSelConf(SearchParams.VariableSelection.DOMWDEG, Integer.MAX_VALUE);
+ private static final SearchParams.ValSelConf MIN =
+ new SearchParams.ValSelConf(SearchParams.ValueSelection.MIN, false, 1, false);
+ private static final SearchParams.ResConf LUBY =
+ new SearchParams.ResConf(SearchParams.Restart.LUBY, 50, 0, true);
+
+ /**
+ * A small optimization problem: returns its decision variables.
+ */
+ private static IntVar[] build(Model m) {
+ int n = 7;
+ IntVar[] x = m.intVarArray("x", n, 0, n - 1);
+ m.allDifferent(x).post();
+ for (int i = 0; i < n - 1; i++) {
+ m.arithm(x[i], "!=", x[i + 1], "+", 1).post();
+ }
+ IntVar obj = m.intVar("obj", 0, 100);
+ m.scalar(x, new int[]{1, 2, 3, 4, 5, 6, 7}, "=", obj).post();
+ m.setObjective(Model.MINIMIZE, obj);
+ return x;
+ }
+
+ /**
+ * @return solutions, nodes, fails and best objective
+ */
+ private static String solve(Model m) {
+ Solver s = m.getSolver();
+ s.limitNode(20_000);
+ while (s.solve()) ;
+ return s.getSolutionCount() + " sols, " + s.getNodeCount() + " nodes, " + s.getFailCount() + " fails, best="
+ + s.getBestSolutionValue();
+ }
+
+ /**
+ * Configure the direct model with direct , and the instances with decl : they must search alike.
+ */
+ private static void assertSameSearch(Consumer direct, SearchDecl decl) throws Exception {
+ Model d = new Model("m");
+ IntVar[] x = build(d);
+ direct.accept(x);
+ String expected = solve(d);
+
+ RecordingModel r = new RecordingModel("m");
+ build(r);
+ ModelSpec spec = r.snapshot().withSearch(decl);
+ ExecutorService pool = Executors.newFixedThreadPool(4);
+ try {
+ List> futures = new ArrayList<>();
+ for (int k = 0; k < 4; k++) {
+ futures.add(pool.submit(() -> solve(spec.instantiate().model())));
+ }
+ for (Future f : futures) {
+ Assert.assertEquals(f.get(), expected);
+ }
+ } finally {
+ pool.shutdownNow();
+ }
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testModelBasedSearch() throws Exception {
+ assertSameSearch(x -> Search.defaultSearch(x[0].getModel()), SearchDecl.of(Search::defaultSearch));
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testBlackBoxWithNogoods() throws Exception {
+ // nogoods on restarts create the SAT constraint on each instance
+ assertSameSearch(x -> BlackBoxConfigurator.forCOP().make(x[0].getModel()),
+ SearchDecl.of(m -> BlackBoxConfigurator.forCOP().make(m)));
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testCatalogSearch() throws Exception {
+ RecordingModel r = new RecordingModel("m");
+ IntVar[] x = build(r);
+ int[] ids = r.getRecorder().idsOf(x);
+ assertSameSearch(v -> {
+ Solver s = v[0].getModel().getSolver();
+ s.setSearch(DOMWDEG.make().apply(v, MIN.make().apply(v[0].getModel())));
+ s.addRestarter(LUBY.make().apply(s));
+ s.setSearch(Search.lastConflict(s.getSearch(), 2));
+ },
+ SearchDecl.intVarSearch(DOMWDEG, MIN, ids)
+ .andThen(SearchDecl.restarts(LUBY))
+ .andThen(SearchDecl.lastConflict(2)));
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testSolutionsAreSharedByIdentifiers() throws Exception {
+ RecordingModel master = new RecordingModel("m");
+ IntVar[] mx = build(master);
+ int[] ids = master.getRecorder().idsOf(mx);
+ ModelSpec spec = master.snapshot().withSearch(SearchDecl.intVarSearch(DOMWDEG, MIN, ids));
+
+ // a worker finds the optimum
+ Resolver worker = spec.instantiate();
+ SpecSolution best = null;
+ while (worker.model().getSolver().solve()) {
+ best = SpecSolution.record(worker);
+ }
+ Assert.assertNotNull(best);
+ int objId = master.getRecorder().idOf(master.getObjective());
+ int optimum = best.valueOf(objId);
+ Assert.assertEquals(optimum, worker.model().getSolver().getBestSolutionValue().intValue());
+
+ // read by the master
+ Solution inMaster = best.toSolution(master.getRecorder());
+ for (int i = 0; i < mx.length; i++) {
+ Assert.assertEquals(inMaster.getIntVal(mx[i]), best.valueOf(ids[i]));
+ }
+ Assert.assertEquals(inMaster.getIntVal((IntVar) master.getObjective()), optimum);
+
+ // and it is a solution of another instance
+ Resolver other = spec.instantiate();
+ for (int id : ids) {
+ other.model().arithm(other.intVar(id), "=", best.valueOf(id)).post();
+ }
+ Assert.assertTrue(other.model().getSolver().solve());
+ Assert.assertEquals(SpecSolution.record(other).valueOf(objId), optimum);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testCustomConstraintCapturingAnotherModelIsDetected() {
+ Model master = new Model();
+ IntVar foreign = master.intVar("x", 0, 3);
+ SpecBuilder b = new SpecBuilder(Header.of("guard"));
+ b.call("intVar", "y", 0, 3);
+ b.post(b.custom("bad", r -> r.model().arithm(foreign, ">", 1)));
+ ModelSpec spec = b.build();
+ SpecException e = Assert.expectThrows(SpecException.class, spec::instantiate);
+ Assert.assertTrue(e.getCause() instanceof AssertionError, String.valueOf(e.getCause()));
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testSearchCapturingAnotherModelIsDetected() {
+ Model master = new Model();
+ IntVar foreign = master.intVar("x", 0, 3);
+ SpecBuilder b = new SpecBuilder(Header.of("guard"));
+ b.call("intVar", "y", 0, 3);
+ ModelSpec spec = b.build().withSearch(r -> r.model().getSolver().setSearch(Search.inputOrderLBSearch(foreign)));
+ Assert.expectThrows(AssertionError.class, spec::instantiate);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testMasterIsAResolver() {
+ RecordingModel master = new RecordingModel("m");
+ IntVar[] x = build(master);
+ Recorder r = master.getRecorder();
+ Assert.assertSame(r.model(), master);
+ for (IntVar v : x) {
+ Assert.assertSame(r.get(r.idOf(v)), v);
+ }
+ Assert.assertThrows(SpecException.class, () -> r.idsOf(new Model().intVar("z", 0, 1)));
+ }
+}
diff --git a/solver/src/test/java/org/chocosolver/solver/spec/SpecPortfolioTest.java b/solver/src/test/java/org/chocosolver/solver/spec/SpecPortfolioTest.java
new file mode 100644
index 0000000000..e0a109f825
--- /dev/null
+++ b/solver/src/test/java/org/chocosolver/solver/spec/SpecPortfolioTest.java
@@ -0,0 +1,136 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.ParallelPortfolio;
+import org.chocosolver.solver.Solution;
+import org.chocosolver.solver.SettingsBuilder;
+import org.chocosolver.solver.exception.SolverException;
+import org.chocosolver.solver.search.strategy.SearchParams;
+import org.chocosolver.solver.variables.IntVar;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Tests of {@link ParallelPortfolio#of(ModelSpec, List)}.
+ *
+ * @author Charles Prud'homme
+ */
+public class SpecPortfolioTest {
+
+ private static final int[] WEIGHTS = {3, 4, 5, 8, 9, 11, 13};
+ private static final int[] VALUES = {4, 5, 7, 10, 11, 15, 17};
+ private static final int CAPACITY = 30;
+
+ /**
+ * A small knapsack, to maximize.
+ *
+ * @return the occurrences
+ */
+ private static IntVar[] knapsack(Model m) {
+ IntVar[] occ = m.intVarArray("o", WEIGHTS.length, 0, 3);
+ IntVar weight = m.intVar("w", 0, CAPACITY);
+ IntVar value = m.intVar("v", 0, 200);
+ m.knapsack(occ, weight, value, WEIGHTS, VALUES).post();
+ m.setObjective(Model.MAXIMIZE, value);
+ return occ;
+ }
+
+ /**
+ * @return the optimum, computed sequentially
+ */
+ private static int optimum() {
+ Model m = new Model();
+ knapsack(m);
+ Solution best = m.getSolver().findOptimalSolution((IntVar) m.getObjective(), Model.MAXIMIZE);
+ return best.getIntVal((IntVar) m.getObjective());
+ }
+
+ private static void assertOptimal(ParallelPortfolio portfolio, RecordingModel master, IntVar[] occ) {
+ int best = -1;
+ while (portfolio.solve()) {
+ best = portfolio.getBestModel().getSolver().getBestSolutionValue().intValue();
+ // each solution can be read from the master
+ SpecSolution sol = portfolio.getBestSpecSolution();
+ Solution inMaster = sol.toSolution(master.getRecorder());
+ int weight = 0;
+ int value = 0;
+ for (int i = 0; i < occ.length; i++) {
+ weight += WEIGHTS[i] * inMaster.getIntVal(occ[i]);
+ value += VALUES[i] * inMaster.getIntVal(occ[i]);
+ }
+ Assert.assertTrue(weight <= CAPACITY);
+ Assert.assertEquals(value, best);
+ Assert.assertEquals(inMaster.getIntVal((IntVar) master.getObjective()), best);
+ }
+ Assert.assertEquals(best, optimum());
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testCarbonCopies() {
+ RecordingModel master = new RecordingModel("knapsack");
+ IntVar[] occ = knapsack(master);
+ ParallelPortfolio portfolio = ParallelPortfolio.of(master.snapshot(), 4);
+ List models = portfolio.getModels();
+ Assert.assertEquals(models.size(), 4);
+ Set names = new HashSet<>();
+ for (Model m : models) {
+ Assert.assertNotSame(m, master);
+ Assert.assertNotNull(portfolio.resolverOf(m));
+ names.add(m.getName());
+ }
+ Assert.assertEquals(names.size(), 4, "the models are named after their rank");
+ assertOptimal(portfolio, master, occ);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testVariants() {
+ RecordingModel master = new RecordingModel("knapsack");
+ IntVar[] occ = knapsack(master);
+ int[] ids = master.getRecorder().idsOf(occ);
+ SearchDecl search = SearchDecl.intVarSearch(
+ new SearchParams.VarSelConf(SearchParams.VariableSelection.DOM, Integer.MAX_VALUE),
+ new SearchParams.ValSelConf(SearchParams.ValueSelection.MAX, false, 1, false), ids);
+ ParallelPortfolio portfolio = ParallelPortfolio.of(master.snapshot(), List.of(
+ Variant.IDENTITY,
+ Variant.seed(17),
+ Variant.search(search),
+ Variant.settings(SettingsBuilder.init().setEnableSAT(true).build())));
+ Assert.assertEquals(portfolio.getModels().size(), 4);
+ Assert.assertEquals(portfolio.getModels().get(1).getSeed(), 17L);
+ assertOptimal(portfolio, master, occ);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testManualPortfolioHasNoResolver() {
+ ParallelPortfolio portfolio = new ParallelPortfolio();
+ Model m = new Model();
+ knapsack(m);
+ portfolio.addModel(m);
+ Assert.assertNull(portfolio.resolverOf(m));
+ Assert.assertTrue(portfolio.solve());
+ Assert.assertThrows(SolverException.class, portfolio::getBestSpecSolution);
+ Assert.assertNotNull(portfolio.getBestSolution());
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testInvalidVariantIsReported() {
+ RecordingModel master = new RecordingModel("knapsack");
+ knapsack(master);
+ ModelSpec spec = master.snapshot();
+ // a variant which breaks the replay: the error of the instantiation is reported
+ Variant broken = Variant.mapCalls(c -> c.method().name().equals("knapsack")
+ ? new Step.Call(c.out(), FactoryMethod.of("allDifferent", IntVar[].class, String.class),
+ new Arg[]{c.args()[0], new Arg.Const("UNKNOWN")}) : c);
+ Assert.assertThrows(SolverException.class, () -> ParallelPortfolio.of(spec, List.of(Variant.IDENTITY, broken)));
+ }
+}
diff --git a/solver/src/test/java/org/chocosolver/solver/spec/SpecTestUtils.java b/solver/src/test/java/org/chocosolver/solver/spec/SpecTestUtils.java
new file mode 100644
index 0000000000..9bba375a23
--- /dev/null
+++ b/solver/src/test/java/org/chocosolver/solver/spec/SpecTestUtils.java
@@ -0,0 +1,73 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.Solver;
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.search.strategy.Search;
+import org.chocosolver.solver.variables.IntVar;
+import org.chocosolver.solver.variables.Variable;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+/**
+ * What the tests of this package compare between models built directly, recorded and replayed.
+ *
+ * @author Charles Prud'homme
+ */
+final class SpecTestUtils {
+
+ private SpecTestUtils() {
+ }
+
+ /**
+ * @return a description of the structure of the model: variables (with their domains), groups and constraints
+ * (with their propagators), in order
+ */
+ static String structure(Model m) {
+ StringBuilder sb = new StringBuilder();
+ for (Variable v : m.getVars()) {
+ sb.append(v.getClass().getSimpleName()).append(' ').append(v.getName());
+ if (v instanceof IntVar iv) {
+ sb.append(' ').append(iv.getLB()).append("..").append(iv.getUB()).append(" #").append(iv.getDomainSize());
+ }
+ sb.append('\n');
+ }
+ m.getGroups().forEach(g -> sb.append("group ").append(g.getName()).append(Arrays.stream(g.getVariables())
+ .map(Variable::getName).collect(Collectors.joining(",", "[", "]"))).append('\n'));
+ for (Constraint c : m.getCstrs()) {
+ sb.append(c.getName()).append(Arrays.stream(c.getPropagators())
+ .map(p -> p.getClass().getSimpleName() + "/" + p.getNbVars())
+ .collect(Collectors.joining(",", "[", "]"))).append('\n');
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Solve with a deterministic search over all integer variables, limited in nodes.
+ *
+ * @return solutions, nodes, fails and best objective
+ */
+ static String solve(Model m, long nodes) {
+ Solver s = m.getSolver();
+ IntVar[] vars = m.retrieveIntVars(true);
+ if (vars.length > 0) {
+ s.setSearch(Search.inputOrderLBSearch(vars));
+ }
+ s.limitNode(nodes);
+ Number best = null;
+ while (s.solve()) {
+ if (m.getObjective() != null) {
+ best = s.getBestSolutionValue();
+ }
+ }
+ return s.getSolutionCount() + " sols, " + s.getNodeCount() + " nodes, " + s.getFailCount()
+ + " fails" + (best == null ? "" : ", best=" + best);
+ }
+}
diff --git a/solver/src/test/java/org/chocosolver/solver/spec/VariantTest.java b/solver/src/test/java/org/chocosolver/solver/spec/VariantTest.java
new file mode 100644
index 0000000000..8405f497f0
--- /dev/null
+++ b/solver/src/test/java/org/chocosolver/solver/spec/VariantTest.java
@@ -0,0 +1,210 @@
+/*
+ * This file is part of choco-solver, http://choco-solver.org/
+ * Copyright (c) 1999, IMT Atlantique.
+ * SPDX-License-Identifier: BSD-3-Clause.
+ * See LICENSE file in the project root for full license information.
+ */
+package org.chocosolver.solver.spec;
+
+import org.chocosolver.solver.Model;
+import org.chocosolver.solver.Solver;
+import org.chocosolver.solver.constraints.Constraint;
+import org.chocosolver.solver.constraints.extension.Tuples;
+import org.chocosolver.solver.search.strategy.Search;
+import org.chocosolver.solver.search.strategy.SearchParams;
+import org.chocosolver.solver.variables.IntVar;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.stream.Collectors;
+
+/**
+ * Tests of {@link Variant}.
+ *
+ * @author Charles Prud'homme
+ */
+public class VariantTest {
+
+ private static final int N = 7;
+
+ /**
+ * Queens with three allDifferent, and a table on the first two queens.
+ */
+ private static IntVar[] queens(Model m) {
+ IntVar[] q = m.intVarArray("q", N, 1, N);
+ IntVar[] d1 = new IntVar[N];
+ IntVar[] d2 = new IntVar[N];
+ for (int i = 0; i < N; i++) {
+ d1[i] = m.offset(q[i], i);
+ d2[i] = m.offset(q[i], -i);
+ }
+ m.allDifferent(q).post();
+ m.allDifferent(d1, "AC").post();
+ m.allDifferent(d2).post();
+ Tuples t = new Tuples(false);
+ t.add(1, 3);
+ t.add(3, 1);
+ m.table(new IntVar[]{q[0], q[1], q[2]}, tuples3()).post();
+ m.table(q[0], q[1], t).post();
+ return q;
+ }
+
+ private static Tuples tuples3() {
+ Tuples t = new Tuples(true);
+ for (int a = 1; a <= N; a++) {
+ for (int b = 1; b <= N; b++) {
+ for (int c = 1; c <= N; c++) {
+ if (a != 2 || b != 4) {
+ t.add(a, b, c);
+ }
+ }
+ }
+ }
+ return t;
+ }
+
+ private static ModelSpec spec() {
+ return ModelSpec.record("queens", org.chocosolver.solver.SettingsBuilder.init().build(), VariantTest::queens);
+ }
+
+ private static List propagators(Model m, String constraintName) {
+ return Arrays.stream(m.getCstrs()).filter(c -> c.getName().equals(constraintName))
+ .flatMap(c -> Arrays.stream(c.getPropagators())).map(p -> p.getClass().getSimpleName())
+ .collect(Collectors.toList());
+ }
+
+ private static long solutions(Model m) {
+ Solver s = m.getSolver();
+ s.setSearch(Search.inputOrderLBSearch(m.retrieveIntVars(true)));
+ return s.findAllSolutions().size();
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testIdentity() {
+ ModelSpec spec = spec();
+ Assert.assertSame(spec.with(Variant.IDENTITY), spec);
+ Assert.assertEquals(solutions(spec.instantiate(Variant.IDENTITY).model()), solutions(spec.instantiate().model()));
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testAllDifferentConsistency() {
+ ModelSpec spec = spec();
+ long expected = solutions(spec.instantiate().model());
+ Model bc = spec.instantiate(Variant.allDifferent("BC")).model();
+ List props = propagators(bc, "ALLDIFFERENT");
+ Assert.assertEquals(props.stream().filter(p -> p.contains("BC")).count(), 3, props.toString());
+ Assert.assertTrue(props.stream().noneMatch(p -> p.contains("AC")), props.toString());
+ Assert.assertEquals(solutions(bc), expected);
+ Model neqs = spec.instantiate(Variant.allDifferent("NEQS")).model();
+ Assert.assertTrue(propagators(neqs, "ALLDIFFERENT").isEmpty() || !propagators(neqs, "ALLDIFFERENT").contains("PropAllDiffBC"));
+ Assert.assertEquals(solutions(neqs), expected);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testTableAlgorithms() {
+ ModelSpec spec = spec();
+ long expected = solutions(spec.instantiate().model());
+ Model str2 = spec.instantiate(Variant.tableAlgorithm("STR2+")).model();
+ Assert.assertTrue(propagators(str2, "TABLE").contains("PropTableStr2"), propagators(str2, "TABLE").toString());
+ Assert.assertEquals(solutions(str2), expected);
+ Model ac3 = spec.instantiate(Variant.binaryTableAlgorithm("AC3rm")).model();
+ Assert.assertTrue(propagators(ac3, "TABLE").stream().anyMatch(p -> p.contains("AC3rm")), propagators(ac3, "TABLE").toString());
+ Assert.assertEquals(solutions(ac3), expected);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testSeedOverridesRecordedSeeds() {
+ Model recorded = Model.record("seeded");
+ recorded.setSeed(42);
+ recorded.allDifferent(recorded.intVarArray("x", 3, 0, 2)).post();
+ ModelSpec spec = recorded.getRecorder().snapshot();
+ Assert.assertEquals(spec.instantiate().model().getSeed(), 42L);
+ Assert.assertEquals(spec.instantiate(Variant.seed(7)).model().getSeed(), 7L);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testSeedAndSearch() {
+ ModelSpec spec = spec();
+ long expected = solutions(spec.instantiate().model());
+ Model m = spec.instantiate(Variant.seed(123)).model();
+ Assert.assertEquals(m.getSeed(), 123L);
+ Assert.assertEquals(solutions(m), expected);
+ SearchParams.VarSelConf dom = new SearchParams.VarSelConf(SearchParams.VariableSelection.DOM, Integer.MAX_VALUE);
+ SearchParams.ValSelConf max = new SearchParams.ValSelConf(SearchParams.ValueSelection.MAX, false, 1, false);
+ int[] q = new int[N];
+ for (int i = 0; i < N; i++) {
+ q[i] = i; // intVarArray is the first call: its outputs are 0..N-1
+ }
+ Model searched = spec.instantiate(Variant.search(SearchDecl.intVarSearch(dom, max, q))).model();
+ Assert.assertTrue(searched.getSolver().solve());
+ Assert.assertEquals(searched.getSolver().getSearch().getClass().getSimpleName(), "IntStrategy");
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testExtendAndUnpost() {
+ RecordingModel master = new RecordingModel("queens");
+ IntVar[] q = queens(master);
+ int[] ids = master.getRecorder().idsOf(q);
+ ModelSpec spec = master.snapshot();
+ long expected = solutions(spec.instantiate().model());
+ int nbCstrs = spec.instantiate().model().getNbCstrs();
+
+ // redundant constraint: the sum of the queens
+ Variant redundant = Variant.extend(b -> b.post(b.call("sum", b.refs(IntVar.class, ids), "=", N * (N + 1) / 2)));
+ Model r = spec.instantiate(redundant).model();
+ Assert.assertEquals(r.getNbCstrs(), nbCstrs + 1);
+ Assert.assertEquals(solutions(r), expected);
+
+ // symmetry breaking: q[0] < q[N-1]
+ Variant symmetry = Variant.extend(b -> b.post(b.call("arithm", b.ref(ids[0]), "<", b.ref(ids[N - 1]))));
+ Assert.assertTrue(solutions(spec.instantiate(symmetry).model()) < expected);
+
+ // removal of the tables
+ Model relaxed = spec.instantiate(Variant.unpostIf(c -> c.method().name().equals("table"))).model();
+ Assert.assertEquals(relaxed.getNbCstrs(), nbCstrs - 2);
+ Assert.assertTrue(solutions(relaxed) > expected);
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testComposedVariantsInParallel() throws Exception {
+ ModelSpec spec = spec();
+ long expected = solutions(spec.instantiate().model());
+ List variants = List.of(
+ Variant.IDENTITY,
+ Variant.allDifferent("BC"),
+ Variant.allDifferent("AC").andThen(Variant.tableAlgorithm("STR2+")).andThen(Variant.seed(7)),
+ Variant.binaryTableAlgorithm("FC").andThen(Variant.search(SearchDecl.of(Search::defaultSearch))));
+ List specs = variants.stream().map(spec::with).collect(Collectors.toList());
+ ExecutorService pool = Executors.newFixedThreadPool(4);
+ try {
+ List> futures = new ArrayList<>();
+ for (ModelSpec s : specs) {
+ futures.add(pool.submit(() -> solutions(s.instantiate().model())));
+ }
+ for (Future f : futures) {
+ Assert.assertEquals(f.get().longValue(), expected);
+ }
+ } finally {
+ pool.shutdownNow();
+ }
+ }
+
+ @Test(groups = "1s", timeOut = 60000)
+ public void testVariantKeepsIdentifiers() {
+ ModelSpec spec = spec();
+ ModelSpec v = spec.with(Variant.allDifferent("BC"));
+ Assert.assertEquals(v.steps().size(), spec.steps().size());
+ Resolver i = v.instantiate();
+ for (int id = 0; id < N; id++) {
+ Assert.assertEquals(i.intVar(id).getName(), "q[" + id + "]");
+ }
+ Constraint[] cs = i.model().getCstrs();
+ Assert.assertEquals(cs.length, spec.instantiate().model().getNbCstrs());
+ }
+}