diff --git a/src/main/java/cryptator/config/CryptagenConfig.java b/src/main/java/cryptator/config/CryptagenConfig.java index 8b29b5f..7a31fd0 100644 --- a/src/main/java/cryptator/config/CryptagenConfig.java +++ b/src/main/java/cryptator/config/CryptagenConfig.java @@ -13,7 +13,7 @@ public class CryptagenConfig extends CryptaCmdConfig { - @Option(name="-d",handler=ExplicitBooleanOptionHandler.class,usage="dry run (generate but do not solve candidate cryptarithms)") + @Option(name="-d",handler=ExplicitBooleanOptionHandler.class, usage="dry run (generate but do not solve candidate cryptarithms)") private boolean dryRun; @Option(name="-ctry",usage="country code for doubly true cryptarithms)") @@ -31,6 +31,11 @@ public class CryptagenConfig extends CryptaCmdConfig { @Option(name="-maxop", usage="maximum number of left operands") private int maxLeftOperands= -1; + @Option(name="-lightM", handler=ExplicitBooleanOptionHandler.class, usage="use less auxiliary variables") + private boolean lightModel = true; + + @Option(name="-lightP", handler=ExplicitBooleanOptionHandler.class, usage="use weak consistency") + private boolean lightPropagation; public final boolean isDryRun() { return dryRun; @@ -56,9 +61,26 @@ public final int getMaxLeftOperands() { return maxLeftOperands; } + + public final boolean isLightModel() { + return lightModel; + } + + public final void setLightModel(boolean lightModel) { + this.lightModel = lightModel; + } + + public final boolean isLightPropagation() { + return lightPropagation; + } + + public final void setLightPropagation(boolean lightPropagation) { + this.lightPropagation = lightPropagation; + } + @Override public String toString() { - return super.toString() + "\nc LANG " + langCode + "\nc THREADS " + nthreads; + return super.toString() + "\nc LANG " + langCode + "\nc THREADS " + nthreads+"\nc LIGHT_MOD " + lightModel+"\nc LIGHT_PROPAG " + lightPropagation; } diff --git a/src/main/java/cryptator/gen/CryptaEqnMember.java b/src/main/java/cryptator/gen/CryptaEqnMember.java deleted file mode 100644 index 44e4674..0000000 --- a/src/main/java/cryptator/gen/CryptaEqnMember.java +++ /dev/null @@ -1,83 +0,0 @@ -/** - * This file is part of cryptator, https://github.com/arnaud-m/cryptator - * - * Copyright (c) 2022, Université Côte d'Azur. All rights reserved. - * - * Licensed under the BSD 3-clause license. - * See LICENSE file in the project root for full license information. - */ -package cryptator.gen; - -import org.chocosolver.solver.Model; -import org.chocosolver.solver.variables.BoolVar; -import org.chocosolver.solver.variables.IntVar; - -public class CryptaEqnMember { - - public final String[] strWords; - - public final BoolVar[] words; - public final IntVar[] lengths; - public final IntVar maxLength; - public final IntVar wordCount; - - public CryptaEqnMember(Model m, String[] words, String prefix) { - super(); - this.strWords = words; - this.words = WordsListModel.buildWordVars(m, words, prefix); - - lengths = new IntVar[words.length]; - int maxLen = 0; - for (int i = 0; i < words.length; i++) { - if(maxLen < words[i].length()) maxLen = words[i].length(); - lengths[i] = this.words[i].mul(words[i].length()).intVar(); - } - - maxLength = m.intVar(prefix + "maxLen", 0, maxLen); - m.max(maxLength, lengths).post(); - - wordCount = m.intVar(prefix + "wordCount", 0, words.length); - m.sum(this.words, "=", wordCount).post(); - - } - - public final BoolVar[] getWords() { - return words; - } - - public final IntVar[] getLengths() { - return lengths; - } - - public final IntVar getMaxLength() { - return maxLength; - } - - public final IntVar getWordCount() { - return wordCount; - } - -// public ICryptaNode recordMember(CryptaEqnMember member) { -// ICryptaNode node = null; -// for (int i = 0; i < words.length; i++) { -// if(words[i].isInstantiatedTo(1)) { -// final CryptaLeaf leaf = new CryptaLeaf(strWords[i]); -// node = node == null ? leaf : new CryptaNode(CryptaOperator.ADD, node, leaf); -// } -// } -// return node; -// } - - @Override - public String toString() { - StringBuilder b = new StringBuilder(); - for (int i = 0; i < words.length; i++) { - if(words[i].isInstantiatedTo(1)) b.append(strWords[i]).append(" + "); - } - if(b.length() > 0) b.delete(b.length()-3, b.length()); - return b.toString(); - } - - - -} \ No newline at end of file diff --git a/src/main/java/cryptator/gen/CryptaGenBaseModel.java b/src/main/java/cryptator/gen/CryptaGenBaseModel.java new file mode 100644 index 0000000..6dd611a --- /dev/null +++ b/src/main/java/cryptator/gen/CryptaGenBaseModel.java @@ -0,0 +1,164 @@ +/** + * This file is part of cryptator, https://github.com/arnaud-m/cryptator + * + * Copyright (c) 2022, Université Côte d'Azur. All rights reserved. + * + * Licensed under the BSD 3-clause license. + * See LICENSE file in the project root for full license information. + */ +package cryptator.gen; + +import java.util.Arrays; +import java.util.Collection; +import java.util.function.BinaryOperator; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.chocosolver.solver.Model; +import org.chocosolver.solver.variables.BoolVar; +import org.chocosolver.solver.variables.IntVar; + +import cryptator.CryptaOperator; +import cryptator.specs.ICryptaGenModel; +import cryptator.specs.ICryptaNode; +import cryptator.tree.CryptaConstant; +import cryptator.tree.CryptaLeaf; +import cryptator.tree.CryptaNode; +import cryptator.tree.TreeUtils; + +/** + * Base class for generation model.. + */ +class CryptaGenBaseModel implements ICryptaGenModel { + + + /** The model. */ + protected final Model model; + + /** The word array . */ + protected final String[] words; + + /** The word variables that indicates if the word is present. */ + protected final BoolVar[] vwords; + + /** The word count. */ + protected final IntVar wordCount; + + /** The maximum length of a present word. */ + protected final IntVar maxLength; + + public CryptaGenBaseModel(Model model, String[] words, String prefix, boolean boundedDomain) { + super(); + this.model = model; + this.words = words; + this.vwords = buildWordVars(model, words, prefix); + this.wordCount = model.intVar(prefix+ "wordCount", 0, words.length); + this.maxLength = model.intVar(prefix+ "maxLength", 0, getMaxLength(words), boundedDomain); + } + + @Override + public final Model getModel() { + return model; + } + + @Override + public final String[] getWords() { + return words; + } + + @Override + public final BoolVar[] getWordVars() { + return vwords; + } + + @Override + public final IntVar getWordCount() { + return wordCount; + } + + @Override + public final IntVar getMaxLength() { + return maxLength; + } + + /** + * Builds named boolean variables associated to the words. + * + * @param model the model + * @param words the words list + * @param prefix the prefix for the variable names + * @return the array of boolean variables + */ + private static BoolVar[] buildWordVars(Model model, String[] words, String prefix) { + final BoolVar[] vars = new BoolVar[words.length]; + for (int i = 0; i < words.length; i++) { + vars[i] = model.boolVar(prefix + words[i]); + } + return vars; + } + + protected void postWordCountConstraint() { + model.sum(vwords, "=", wordCount).post(); + } + + protected void postMaxLengthConstraints() { + maxLength.eq(0).decompose().post(); + } + + @Override + public void buildModel() { + postWordCountConstraint(); + postMaxLengthConstraints(); + } + + public static int[] getLengths(String[] words) { + return Arrays.stream(words).mapToInt(String::length).toArray(); + } + + public static int getMaxLength(String[] words) { + return Arrays.stream(words).mapToInt(String::length).max().orElse(0); + } + + public static int[] getLengthCounts(String[] words) { + final int n = getMaxLength(words); + int[] v = new int[n + 1]; + for (String w : words) { + v[w.length()]++; + } + return v; + } + + public static BoolVar[] toArray(Collection vars) { + return vars.toArray(new BoolVar[vars.size()]); + } + + public static Stream wordStream(ICryptaGenModel model) { + final BoolVar[] v = model.getWordVars(); + final String[] w = model.getWords(); + return IntStream.range(0, model.getN()) + .filter(i -> v[i].isInstantiatedTo(1)) + .mapToObj(i -> w[i]); + } + + public static String recordString(ICryptaGenModel model, String separator) { + return wordStream(model).collect( Collectors.joining(separator ) ); + } + + public static ICryptaNode recordAddition(ICryptaGenModel model) { + BinaryOperator add = (a, b) -> { + return a == null ? b : new CryptaNode(CryptaOperator.ADD, a, b); + }; + return wordStream(model) + .map(CryptaLeaf::new) + .map(ICryptaNode.class::cast) + .reduce(null, add); + } + + + @Override + public String toString() { + return recordString(this, " + "); + } + +} \ No newline at end of file diff --git a/src/main/java/cryptator/gen/CryptaGenModel.java b/src/main/java/cryptator/gen/CryptaGenModel.java index 3bb3730..1f7c99b 100644 --- a/src/main/java/cryptator/gen/CryptaGenModel.java +++ b/src/main/java/cryptator/gen/CryptaGenModel.java @@ -14,103 +14,113 @@ import org.chocosolver.util.tools.ArrayUtils; import cryptator.CryptaOperator; +import cryptator.specs.ICryptaGenModel; import cryptator.specs.ICryptaNode; -import cryptator.tree.CryptaLeaf; import cryptator.tree.CryptaNode; public class CryptaGenModel extends WordsListModel { - private final CryptaEqnMember left; + //private final ICryptaGenModel left; + //private final CryptaMemberScalar left; + private final CryptaMemberLen left; - private final CryptaEqnMember right; + private final ICryptaGenModel right; - public CryptaGenModel(String[] words) { + public CryptaGenModel(String[] words, boolean lenOrCardModel) { super(new Model("Generate"), words); - left = new CryptaEqnMember(model, words, "L_"); - right = new CryptaEqnMember(model, words, "R_"); - buildCoreModel(); + left = lenOrCardModel ? new CryptaMemberLen(model, words, "L_") : new CryptaMemberCard(model, words, "L_"); + right = new CryptaMemberElt(model, words, "R_"); + this.buildModel(); } - private void postLeftOrRightMemberConstraints() { - for (int i = 0; i < words.length; i++) { - left.words[i].add(right.words[i]).eq(wordVariables[i]).post(); - } + @Override + public void buildModel() { + super.buildModel(); + left.buildModel(); + right.buildModel(); + postLeftOrRightConstraints(); + postSymBreakLengthLenConstraint(); } - private void postMemberMaxLenConstraint() { - right.maxLength.ge(left.maxLength).post(); - + @Override + protected void postMaxLengthConstraints() { + model.max(maxLength, left.getMaxLength(), right.getMaxLength()).post(); } - private void buildCoreModel() { - postLeftOrRightMemberConstraints(); - postMemberMaxLenConstraint(); + + private void postLeftOrRightConstraints() { + final BoolVar[] l = left.getWordVars(); + final BoolVar[] r = right.getWordVars(); + for (int i = 0; i < vwords.length; i++) { + l[i].add(r[i]).eq(vwords[i]).post(); + } } - - public void postMemberCardConstraints(int min, int max) { - if(min > 1) left.wordCount.ge(min).post(); - else left.wordCount.ge(2).post(); - if(max >= min) left.wordCount.le(max).post(); - - // TODO Option to relax the constraint (allow subtractions in the bignum model) - // Would need to break reflexion symmetry - right.wordCount.eq(1).post(); + + + private void postSymBreakLengthLenConstraint() { + left.getMaxLength().le(right.getMaxLength()); + } - - public void postLeftMinCardConstraints(int base) { - IntVar diff = right.maxLength.sub(left.maxLength).intVar(); - final int n = words.length; + + public void postLeftCountConstraints(int min, int max) { + min = Math.max(min, 2); + left.getWordCount().ge(min).post(); + if(max >= min) left.getWordCount().le(max).post(); + + } + + public static void postMinLeftCountConstraints(int maxWordCount, IntVar leftCount, IntVar leftMaxLen,IntVar rightMaxLen, int base) { + IntVar diff = rightMaxLen.sub(leftMaxLen).intVar(); int prod = base; int i = 2; - while(prod <= n) { - diff.ge(i).imp(left.wordCount.ge(prod)).post(); + while(prod <= maxWordCount) { + diff.ge(i).imp(leftCount.ge(prod + 1)).post(); prod *= base; i++; } diff.lt(i).post(); } - - public void postRightMemberConstraint() { - BoolVar[] vars = right.getWords(); + + public void postMinLeftCountConstraints(int base) { + left.postLentghSumConstraints(right.getMaxLength(), base); + } + + public void postFixedRightMemberConstraint() { + final BoolVar[] vars = right.getWordVars(); vars[vars.length - 1].eq(1).post(); } - + public void postDoublyTrueConstraint(int lb) { - final int n = words.length; + final int n = getN(); final IntVar sum = model.intVar("SUM", lb, n - 1); - + final IntVar[] lvars = new IntVar[n+1]; - System.arraycopy(left.getWords(), 0, lvars, 0, n); + System.arraycopy(left.getWordVars(), 0, lvars, 0, n); lvars[n] = sum; - + final IntVar[] rvars = new IntVar[n+1]; - System.arraycopy(right.getWords(), 0, rvars, 0, n); + System.arraycopy(right.getWordVars(), 0, rvars, 0, n); rvars[n] = sum; - + final int[] coeffs = ArrayUtils.array(0, n); coeffs[n] = -1; - + model.scalar(lvars, coeffs, "=", 0).post(); model.scalar(rvars, coeffs, "=", 0).post(); } - private ICryptaNode recordMember(CryptaEqnMember member) { - BoolVar[] vars = member.getWords(); - ICryptaNode node = null; - for (int i = 0; i < vars.length; i++) { - if(vars[i].isInstantiatedTo(1)) { - final CryptaLeaf leaf = new CryptaLeaf(words[i]); - node = node == null ? leaf : new CryptaNode(CryptaOperator.ADD, node, leaf); - } - } - return node; + public final ICryptaNode recordCryptarithm() { + final ICryptaNode l = recordAddition(left); + final ICryptaNode r = recordAddition(right); + return r == null || l == null ? null : new CryptaNode(CryptaOperator.EQ, l, r); } - - public final ICryptaNode recordCryptarithm() { - return new CryptaNode(CryptaOperator.EQ, recordMember(left), recordMember(right)); + + + @Override + public String toString() { + return left.toString() + " = " + right.toString(); } - } diff --git a/src/main/java/cryptator/gen/CryptaListGenerator.java b/src/main/java/cryptator/gen/CryptaListGenerator.java index 0e45f26..455bd58 100644 --- a/src/main/java/cryptator/gen/CryptaListGenerator.java +++ b/src/main/java/cryptator/gen/CryptaListGenerator.java @@ -57,11 +57,11 @@ public final int getErrorCount() { } private CryptaGenModel buildModel() { - final CryptaGenModel gen = new CryptaGenModel(words.getWords()); - gen.postMemberCardConstraints(config.getMinLeftOperands(), config.getMaxLeftOperands()); + final CryptaGenModel gen = new CryptaGenModel(words.getWords(), config.isLightModel()); + gen.postLeftCountConstraints(config.getMinLeftOperands(), config.getMaxLeftOperands()); gen.postMaxSymbolCountConstraint(config.getArithmeticBase()); - gen.postLeftMinCardConstraints(config.getArithmeticBase()); - if(words.hasRightMember()) gen.postRightMemberConstraint(); + if(!config.isLightPropagation()) gen.postMinLeftCountConstraints(config.getArithmeticBase()); + if(words.hasRightMember()) gen.postFixedRightMemberConstraint(); if(words.isDoublyTrue()) gen.postDoublyTrueConstraint(words.getLB()); return gen; } diff --git a/src/main/java/cryptator/gen/CryptaMemberCard.java b/src/main/java/cryptator/gen/CryptaMemberCard.java new file mode 100644 index 0000000..1d79e08 --- /dev/null +++ b/src/main/java/cryptator/gen/CryptaMemberCard.java @@ -0,0 +1,78 @@ +/** + * This file is part of cryptator, https://github.com/arnaud-m/cryptator + * + * Copyright (c) 2022, Université Côte d'Azur. All rights reserved. + * + * Licensed under the BSD 3-clause license. + * See LICENSE file in the project root for full license information. + */ +package cryptator.gen; + +import java.util.Arrays; +import java.util.stream.IntStream; + +import org.chocosolver.solver.Model; +import org.chocosolver.solver.constraints.extension.Tuples; +import org.chocosolver.solver.variables.IntVar; + +public class CryptaMemberCard extends CryptaMemberLen { + + protected final IntVar[] cardLengths; + + public CryptaMemberCard(Model m, String[] words, String prefix) { + super(m, words, prefix); + cardLengths = m.intVarArray(prefix + "cardLen", getMaxLength(words) + 1, 0, words.length); + } + + public final IntVar[] getCardLength() { + return cardLengths; + } + + + @Override + protected void postWordCountConstraint() { + super.postWordCountConstraint(); + // Remove card of empty/unused words + IntVar[] vars = Arrays.copyOfRange(cardLengths, 1, cardLengths.length); + model.sum(vars, "=", wordCount).post(); + } + + private void postGlobalCardLengthConstraint() { + int[] values = IntStream.range(0, cardLengths.length).toArray(); + model.globalCardinality(lengths, values, cardLengths, true).post(); + } + + public void postLentghSumConstraints(IntVar sumLength, int base) { + + int[] maxCardLengths = getLengthCounts(words); + final int maxCard = IntStream.of(maxCardLengths).max().orElse(0); + + IntVar[] vars = Arrays.copyOf(cardLengths, maxCard); + vars[0] = model.intVar(0); + + IntVar x = model.intVar("Xk", 0, maxCard,false); + model.element(x, cardLengths, maxLength, 0).post(); + + IntVar y = model.intVar("Yk", 0, maxCard,false); + model.element(y, cardLengths, maxLength.sub(1).intVar(), 0).post(); + + IntVar z = model.intVar("Zk", 0, maxCard,false); + z.eq(wordCount.sub(x).sub(y)).decompose().post(); + + WordSumTuplesBuilder builder = new WordSumTuplesBuilder(base); + Tuples tuples = builder.buildTuples(maxCardLengths); + + model.table(new IntVar[] {maxLength, x, y, z, sumLength}, tuples).post(); + } + + + @Override + public void buildModel() { + super.buildModel(); + postGlobalCardLengthConstraint(); + } + + + + +} \ No newline at end of file diff --git a/src/main/java/cryptator/gen/CryptaMemberElt.java b/src/main/java/cryptator/gen/CryptaMemberElt.java new file mode 100644 index 0000000..f0a926c --- /dev/null +++ b/src/main/java/cryptator/gen/CryptaMemberElt.java @@ -0,0 +1,42 @@ +/** + * This file is part of cryptator, https://github.com/arnaud-m/cryptator + * + * Copyright (c) 2022, Université Côte d'Azur. All rights reserved. + * + * Licensed under the BSD 3-clause license. + * See LICENSE file in the project root for full license information. + */ +package cryptator.gen; + +import java.util.Arrays; + +import org.chocosolver.solver.Model; +import org.chocosolver.solver.variables.IntVar; + +public class CryptaMemberElt extends CryptaGenBaseModel { + + protected final IntVar index; + + public CryptaMemberElt(Model m, String[] words, String prefix) { + super(m, words, prefix, true); + index = m.intVar(prefix+"idx", 0, words.length - 1); + } + + @Override + protected void postWordCountConstraint() { + super.postWordCountConstraint(); + model.boolsIntChanneling(getWordVars(), index, 0).post(); + wordCount.eq(1).decompose().post(); + + } + + @Override + protected void postMaxLengthConstraints() { + int[] lengths = Arrays.stream(words).mapToInt(String::length).toArray(); + model.element(maxLength, lengths, index).post(); + } + + + + +} \ No newline at end of file diff --git a/src/main/java/cryptator/gen/CryptaMemberLen.java b/src/main/java/cryptator/gen/CryptaMemberLen.java new file mode 100644 index 0000000..af57c1e --- /dev/null +++ b/src/main/java/cryptator/gen/CryptaMemberLen.java @@ -0,0 +1,59 @@ +/** + * This file is part of cryptator, https://github.com/arnaud-m/cryptator + * + * Copyright (c) 2022, Université Côte d'Azur. All rights reserved. + * + * Licensed under the BSD 3-clause license. + * See LICENSE file in the project root for full license information. + */ +package cryptator.gen; + +import org.chocosolver.solver.Model; +import org.chocosolver.solver.variables.IntVar; + +public class CryptaMemberLen extends CryptaGenBaseModel { + + public final IntVar[] lengths; + + public CryptaMemberLen(Model m, String[] words, String prefix) { + super(m, words, prefix, true); + lengths = buildLengths(m, words, prefix); + } + + private static IntVar[] buildLengths(Model m, String[] words, String prefix) { + final int n = words.length; + IntVar[] vars = new IntVar[n]; + for (int i = 0; i < n; i++) { + vars[i] = m.intVar( + prefix + "len" + "[" + i + "]", + new int[] {0, words[i].length()} + ); + } + return vars; + } + + @Override + protected void postMaxLengthConstraints() { + for (int i = 0; i < words.length; i++) { + model.reifyXeqC(lengths[i], words[i].length(), vwords[i]); + } + model.max(maxLength, lengths).post(); + } + + public void postLentghSumConstraints(IntVar sumLength, int base) { + IntVar diff = sumLength.sub(maxLength).intVar(); + final int n = getN(); + int prod = base; + int i = 2; + while(prod <= n) { + //System.err.println(i + " " + (prod + 1)); + diff.ge(i).imp(wordCount.ge(prod + 1)).post(); + prod *= base; + i++; + } + diff.lt(i).post(); + } + + + +} \ No newline at end of file diff --git a/src/main/java/cryptator/gen/WordSumTuplesBuilder.java b/src/main/java/cryptator/gen/WordSumTuplesBuilder.java new file mode 100644 index 0000000..c916af7 --- /dev/null +++ b/src/main/java/cryptator/gen/WordSumTuplesBuilder.java @@ -0,0 +1,236 @@ +/** + * This file is part of cryptator, https://github.com/arnaud-m/cryptator + * + * Copyright (c) 2022, Université Côte d'Azur. All rights reserved. + * + * Licensed under the BSD 3-clause license. + * See LICENSE file in the project root for full license information. + */ +package cryptator.gen; + +import java.math.BigInteger; + +import org.chocosolver.solver.constraints.extension.Tuples; + +/** + * The Class WordSumTuplesBuilder is a factory that builds tuples. + * + * Let S be a sum of numbers written in base b. + * + * The tuples have the following semantics : + *
    + *
  • k:
  • + *
  • k: the number of digits of the largest number
  • + *
  • x: the cardinality of numbers with k digits
  • + *
  • y: the cardinality of numbers with k-1 digits
  • + *
  • z: the cardinality of numbers with strictly less than k - 1
  • + *
  • p: the number of digits of the sum S
  • + *
+ * + */ +public final class WordSumTuplesBuilder { + + /** The base. */ + public BigInteger base = BigInteger.TEN; + + /** + * Instantiates a new word sum tuples builder in base 10. + * + */ + public WordSumTuplesBuilder() { + super(); + } + + /** + * Instantiates a new word sum tuples builder. + * + * @param base the base + */ + public WordSumTuplesBuilder(int base) { + super(); + setBase(base); + } + + public int getBase() { + return base.intValue(); + } + + public void setBase(int base) { + if(base > 1) this.base = BigInteger.valueOf(base); + else throw new IllegalArgumentException("The base must be greater than 2."); + } + /** + * Computes the floor of the logarithm of x in base b. + * + * @param x the value for the logarithm + * @param b the base for the logarithm + * @return the floor of the logarithm in base b. + */ + public static final int logFloor(int x, int b) { + return logFloor(BigInteger.valueOf(x), BigInteger.valueOf(b)); + } + + /** + * Computes the floor of the logarithm of x in base b. + * + * @param x the value for the logarithm + * @param b the base for the logarithm + * @return the floor of the logarithm in base b. + */ + public static final int logFloor(BigInteger x, int b) { + return logFloor(x, BigInteger.valueOf(b)); + } + + /** + * Computes the floor of the logarithm of x in base b. + * + * @param x the value for the logarithm + * @param b the base for the logarithm + * @return the floor of the logarithm in base b. + */ + public static final int logFloor(BigInteger x, BigInteger b) { + BigInteger m = b; + int v = 0; + while( x.compareTo(m) >= 0) { + m = m.multiply(b); + v++; + } + return v; + } + + /** + * Computes the exponentiation of x in base b : x * b^k. + * + * @param x the coefficient + * @param b the base + * @param k the positive exponent + * @return the exponentiation of x. + */ + public static final BigInteger exp(BigInteger x, BigInteger b, int k) { + return b.pow(k).multiply(x); + } + + /** + * Computes the exponentiation of x in base b : x * b^k. + * It fails if the exponent is negative unless x is equal to 0. + * + * @param x the coefficient + * @param k the exponent + * @return the exponentiation of x. + */ + private BigInteger exp(int x, int k) { + return x == 0 ? BigInteger.ZERO : exp(BigInteger.valueOf(x), base, k); + } + + /** + * Computes the minimal number of digits the sum. + * + * @param k the number of digits of the largest number + * @param x the cardinality of numbers with k digits + * @param y the cardinality of numbers with k-1 digits + * @param z the cardinality of numbers with strictly less than k - 1 + * @return the minimal number of digits of the sum + */ + public int getMinLen(int k, int x, int y, int z) { + final BigInteger v = exp(x, k-1).add(exp(y, k-2)).add(BigInteger.valueOf(z)); + // System.out.println(v + " " + Integer.toBinaryString(v.intValue())); + return logFloor(v, base) + 1; + } + + /** + * Computes the minimal number of digits the sum. + * + * @param k the number of digits of the largest number + * @param x the cardinality of numbers with k digits + * @param y the cardinality of numbers with k-1 digits + * @param z the cardinality of numbers with strictly less than k - 1 + * @return the minimal number of digits of the sum + */ + public int getMaxLen(int k, int x, int y, int z) { + final BigInteger xyz = BigInteger.valueOf(x + y + z); + final BigInteger v = exp(x, k).add(exp(y, k-1)).add(exp(z, k-2)).subtract(xyz); + // System.out.println(v + " " + Integer.toBinaryString(v.intValue())); + return logFloor(v, base) + 1; + } + + + /** + * Adds new tuples. + * + * @param tuples the tuples to extend + * @param k the number of digits of the largest number + * @param x the cardinality of numbers with k digits + * @param y the cardinality of numbers with k-1 digits + * @param z the cardinality of numbers with strictly less than k - 1 + */ + private void addTuples(Tuples tuples, int k, int x, int y, int z) { + final int min = getMinLen(k, x, y, z); + final int max = getMaxLen(k, x, y, z); + //System.out.println(k+ " " + x+ " " + y+ " " + z + " " + min+ "-" + max); + for (int p = min; p <= max; p++) { + tuples.add(k, x, y, z, p); + } + } + + /** + * Computes the maximum cardinality of numbers with strictly less than k-1 digits. + * + * @param values the cardinalities of numbers, i.e. the i-th value is the cardinality of numbers with i digits. + * @param k the number of digits of the largest number + * @return the maximum cardinality + */ + public static int getMaxZ(int[] values, int k) { + int maxz = 0; + for (int i = 0; i < k - 1; i++) { + maxz += values[i]; + } + //System.out.println(maxz); + return maxz; + } + + /** + * Add new tuples. + * + * @param tuples the tuples to extends + * @param values the cardinalities of numbers, i.e. the i-th value is the cardinality of numbers with i digits. + * @param k the number of digits of the largest number + */ + private void addTuples(Tuples tuples, int[] values, int k) { + final int maxz = getMaxZ(values, k); + for (int x = 1; x <= values[k]; x++) { + for (int y = 0; y <= values[k-1]; y++) { + for (int z = 0; z <= maxz; z++) { + addTuples(tuples, k, x, y, z); + } + } + } + } + + /** + * Builds the tuples. + * + * @param values the cardinalities of numbers, i.e. the i-th value is the cardinality of numbers with i digits. + * @return the tuples + */ + public Tuples buildTuples(int[] values) { + final Tuples tuples = new Tuples(); + for (int k = 1; k < values.length; k++) { + addTuples(tuples, values, k); + } + return tuples; + } + + /** + * The main method. + * + * @param args the arguments + */ + public static void main(String[] args) { + int[] v = {0, 9, 3, 5, 1, 4, 2}; + + WordSumTuplesBuilder builder = new WordSumTuplesBuilder(2); + Tuples tuples = builder.buildTuples(v); + System.out.println(tuples); + } + +} diff --git a/src/main/java/cryptator/gen/WordsListModel.java b/src/main/java/cryptator/gen/WordsListModel.java index 4d82b04..da43822 100644 --- a/src/main/java/cryptator/gen/WordsListModel.java +++ b/src/main/java/cryptator/gen/WordsListModel.java @@ -9,10 +9,14 @@ package cryptator.gen; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; import org.chocosolver.solver.Model; import org.chocosolver.solver.variables.BoolVar; @@ -22,27 +26,15 @@ * The Class WordsListModel defines a CP model for mapping words to symbols. * A symbol is present if and only if at least one word that contains the symbol is present. */ -public class WordsListModel { - - /** The model. */ - protected final Model model; - - /** The words list. */ - protected final String[] words; - - /** The word variables that indicates if the word is present. */ - protected final BoolVar[] wordVariables; - - /** The word count. */ - private final IntVar wordCount; +public class WordsListModel extends CryptaGenBaseModel { + /** The map that associates a variable to each symbol of the words. */ - private final Map symbolsToVariables; + protected final Map symbolsToVariables; /** The symbol count. */ - private final IntVar symbolCount; + protected final IntVar symbolCount; - /** * Instantiates a new words list model. * @@ -50,48 +42,28 @@ public class WordsListModel { * @param words the words list */ public WordsListModel(Model model, String[] words) { - this.model = model; - this.words = words; - wordVariables = buildWordVars(model, words, ""); - wordCount = buildCountVariable(model, "wordCount", wordVariables); + super(model, words, "", false); symbolsToVariables = buildSymbolVars(model, words); - symbolCount = buildSymbolCountVariable(model, symbolsToVariables); - postChannelingConstraints(); + symbolCount = model.intVar("symbCount", 0 , symbolsToVariables.size()); } - - /** - * Gets the words list. - * - * @return the words - */ - public final String[] getWords() { - return words; + + + public final IntVar getSymbolCount() { + return symbolCount; } - /** - * Gets the CP model. - * - * @return the model - */ - public final Model getModel() { - return model; - } + @Override + public void buildModel() { + super.buildModel(); + postSymbolCountConstraint(); + postChannelingConstraints(); + } - /** - * Builds named boolean variables associated to the words. - * - * @param model the model - * @param words the words list - * @param prefix the prefix for the variable names - * @return the array of boolean variables - */ - protected static BoolVar[] buildWordVars(Model model, String[] words, String prefix) { - final BoolVar[] vars = new BoolVar[words.length]; - for (int i = 0; i < words.length; i++) { - vars[i] = model.boolVar(prefix + words[i]); - } - return vars; + + protected void postSymbolCountConstraint() { + final BoolVar[] symbols = toArray(symbolsToVariables.values()); + model.sum(symbols, "=", symbolCount).post(); } /** @@ -112,32 +84,6 @@ private static Map buildSymbolVars(Model model, String[] wor } - /** - * Builds the symbol count variable and post the required sum constraint. - * - * @param model the model - * @param symbolsToVariables the variables associated to symbols - * @return the variable that gives the number of symbols. - */ - private IntVar buildSymbolCountVariable(Model model, Map symbolsToVariables) { - final BoolVar[] symbols = symbolsToVariables.values().toArray(new BoolVar[symbolsToVariables.size()]); - return buildCountVariable(model, "digitCount", symbols); - } - - /** - * Builds the count variable that indicates how many boolean variables are true. - * - * @param model the model - * @param name the name of the count variable - * @param vars the boolean variables to be counted - * @return the variable giving the number of variables that are true. - */ - private static IntVar buildCountVariable(Model model, String name, BoolVar[] vars) { - final IntVar count = model.intVar(name, 0, vars.length); - model.sum(vars, "=", count).post(); - return count; - } - /** * Builds the mapping between each symbol and the boolean variables associated to words that contains the symbol. * @@ -148,7 +94,7 @@ private Map> buildSymbolsToWords() { for (int i = 0; i < words.length; i++) { for (char c : words[i].toCharArray()) { final Collection list = map.computeIfAbsent(c, s -> new ArrayList<>()); - list.add(wordVariables[i]); + list.add(vwords[i]); } } return map; @@ -161,9 +107,9 @@ private Map> buildSymbolsToWords() { private void postChannelingConstraints() { final Map> symbolsToWords = buildSymbolsToWords(); for (Map.Entry> entry : symbolsToWords.entrySet()) { - final Collection varsL = entry.getValue(); - final BoolVar[] vars = varsL.toArray(new BoolVar[varsL.size()]); - model.max(symbolsToVariables.get(entry.getKey()), vars).post(); + final BoolVar max = symbolsToVariables.get(entry.getKey()); + final BoolVar[] vars = toArray(entry.getValue()); + model.max(max, vars).post(); } } @@ -184,6 +130,6 @@ public void postMaxSymbolCountConstraint(int max) { public void postMaxWordCountConstraint(int max) { wordCount.le(max).post(); } - + } diff --git a/src/main/java/cryptator/specs/ICryptaGenModel.java b/src/main/java/cryptator/specs/ICryptaGenModel.java new file mode 100644 index 0000000..f1baca9 --- /dev/null +++ b/src/main/java/cryptator/specs/ICryptaGenModel.java @@ -0,0 +1,74 @@ +/** + * This file is part of cryptator, https://github.com/arnaud-m/cryptator + * + * Copyright (c) 2022, Université Côte d'Azur. All rights reserved. + * + * Licensed under the BSD 3-clause license. + * See LICENSE file in the project root for full license information. + */ +package cryptator.specs; + +import org.chocosolver.solver.Model; +import org.chocosolver.solver.variables.BoolVar; +import org.chocosolver.solver.variables.IntVar; + +/** + * The Interface ICryptaGenVariables to access variables used for generating cryptarithms. + */ +/** + * @author nono + * + */ +public interface ICryptaGenModel { + + /** + * Gets the model. + * + * @return the model + */ + Model getModel(); + + /** + * Gets the number of words or word variables. + * + * @return the number of words + */ + default int getN() { + return getWords().length; + } + + /** + * Gets the words of the model. + * + * @return the word strings + */ + String[] getWords(); + + /** + * Gets the word variables that indicates if the word is present. + * + * @return the boolean variables + */ + BoolVar[] getWordVars(); + + /** + * Gets variable that represents the maximum length of a present word. + * + * @return the max. length integer variable + */ + IntVar getMaxLength(); + + /** + * Gets the the variable that represents the word count, i.e. the number of present word. + * + * @return the word count integer variable + */ + IntVar getWordCount(); + + + /** + * Post the constraints of the model + */ + void buildModel(); + +} \ No newline at end of file diff --git a/src/test/java/cryptator/GenModelTest.java b/src/test/java/cryptator/GenModelTest.java new file mode 100644 index 0000000..825e1b2 --- /dev/null +++ b/src/test/java/cryptator/GenModelTest.java @@ -0,0 +1,88 @@ +/** + * This file is part of cryptator, https://github.com/arnaud-m/cryptator + * + * Copyright (c) 2022, Université Côte d'Azur. All rights reserved. + * + * Licensed under the BSD 3-clause license. + * See LICENSE file in the project root for full license information. + */ +package cryptator; + +import static org.junit.Assert.assertEquals; + +import org.chocosolver.solver.Model; +import org.junit.Before; +import org.junit.Test; + +import cryptator.gen.CryptaMemberCard; +import cryptator.gen.CryptaMemberElt; +import cryptator.gen.CryptaMemberLen; +import cryptator.gen.WordsListModel; +import cryptator.specs.ICryptaGenModel; + +public class GenModelTest { + + private final String[] words = new String[] {"a", "b", "ba", "bb", "baa", "bab", "bba", "bbb", "baaa"}; + + private ICryptaGenModel[] models; + + @Before + public void buildGenModels() { + models = new ICryptaGenModel[] { + new CryptaMemberLen(new Model(), words, ""), + new CryptaMemberCard(new Model(), words, ""), + new WordsListModel(new Model(), words) + }; + for (ICryptaGenModel m : models) { + m.buildModel(); + } + } + + private void postMaxWordCountConstraint(ICryptaGenModel model, int maxWordCount) { + model.getWordCount().le(maxWordCount).post(); + } + + private void testGenModels(int expectedSolutionCount, int maxWordCount) { + for (ICryptaGenModel m : models) { + postMaxWordCountConstraint(m, maxWordCount); + testGenModel(m, expectedSolutionCount); + } + } + + private void testGenModel(ICryptaGenModel model, int expectedSolutionCount) { + assertEquals(expectedSolutionCount, model.getModel().getSolver().streamSolutions().count()); + } + + @Test + public void testGenModels1() { + testGenModels(10, 1); + } + + @Test + public void testGenModels2() { + testGenModels(46, 2); + } + + @Test + public void testGenModels3() { + testGenModels(130, 3); + } + + @Test + public void testWLModel() { + WordsListModel m = new WordsListModel(new Model(), words); + m.buildModel(); + m.postMaxSymbolCountConstraint(1); + postMaxWordCountConstraint(m, 2); + testGenModel(m, 8); + } + + @Test + public void testOneModel() { + CryptaMemberElt m = new CryptaMemberElt(new Model(), words, ""); + m.buildModel(); + testGenModel(m, 9); + } + + +} diff --git a/src/test/java/cryptator/GenerateTest.java b/src/test/java/cryptator/GenerateTest.java index 78a65a1..3d3d973 100644 --- a/src/test/java/cryptator/GenerateTest.java +++ b/src/test/java/cryptator/GenerateTest.java @@ -27,9 +27,11 @@ public class GenerateTest { public static void configureTestLoggers() { JULogUtil.configureTestLoggers(); } - - public long testGenerate(WordArray wordArray) throws CryptaModelException { + + public long testGenerate(WordArray wordArray, boolean lightModel, boolean lightPropagation) throws CryptaModelException { final CryptagenConfig config = new CryptagenConfig(); + config.setLightModel(lightModel); + config.setLightPropagation(lightPropagation); final CryptaListGenerator gen = new CryptaListGenerator(wordArray, config, Cryptagen.LOGGER); CryptaBiConsumer cons = new CryptaBiConsumer(Cryptagen.LOGGER); cons.withSolutionCheck(config.getArithmeticBase()); @@ -37,39 +39,47 @@ public long testGenerate(WordArray wordArray) throws CryptaModelException { assertEquals(0, cons.getErrorCount()); return cons.getSolutionCount(); } + + + public void testGenerate(int expectedSolCount, WordArray wordArray) throws CryptaModelException { + assertEquals( expectedSolCount,testGenerate(wordArray, false, false)); + assertEquals( expectedSolCount,testGenerate(wordArray, false, true)); + assertEquals( expectedSolCount,testGenerate(wordArray, true, false)); + assertEquals( expectedSolCount,testGenerate(wordArray, true, true)); + } - public long testGenerate(String rightMember, String... words) throws CryptaModelException { - return testGenerate(new WordArray(Arrays.asList(words), rightMember)); + public void testGenerate(int expectedSolCount, String rightMember, String... words) throws CryptaModelException { + testGenerate(expectedSolCount, new WordArray(Arrays.asList(words), rightMember)); } @Test public void testSendMoreMoney() throws CryptaModelException { - assertEquals(1, testGenerate(null, "send", "more", "money")); + testGenerate(1, null, "send", "more", "money"); } @Test public void testPlanets1() throws CryptaModelException { - assertEquals(2, testGenerate(null, "venus", "earth", "uranus", "saturn")); + testGenerate(2, null, "venus", "earth", "uranus", "saturn"); } @Test public void testPlanets2() throws CryptaModelException { - assertEquals(1, testGenerate("planets", "venus", "earth", "uranus", "saturn")); + testGenerate(1, "planets", "venus", "earth", "uranus", "saturn"); } @Test public void testDoublyTrue1() throws CryptaModelException { - assertEquals(0, testGenerate(new WordArray("FR", "fr", 0, 10))); + testGenerate(0, new WordArray("FR", "fr", 0, 10)); } @Test public void testDoublyTrue2() throws CryptaModelException { - assertEquals(1, testGenerate(new WordArray("FR", "fr", 30, 30))); + testGenerate(1, new WordArray("FR", "fr", 30, 30)); } @Test public void testDoublyTrue3() throws CryptaModelException { - assertEquals(3, testGenerate(new WordArray("IT", "it", 20, 30))); + testGenerate(3, new WordArray("IT", "it", 20, 30)); } diff --git a/src/test/java/cryptator/WSTuplesTest.java b/src/test/java/cryptator/WSTuplesTest.java new file mode 100644 index 0000000..7dc6e41 --- /dev/null +++ b/src/test/java/cryptator/WSTuplesTest.java @@ -0,0 +1,98 @@ +/** + * This file is part of cryptator, https://github.com/arnaud-m/cryptator + * + * Copyright (c) 2022, Université Côte d'Azur. All rights reserved. + * + * Licensed under the BSD 3-clause license. + * See LICENSE file in the project root for full license information. + */ +package cryptator; + +import static org.junit.Assert.assertEquals; + +import org.chocosolver.solver.constraints.extension.Tuples; +import org.junit.Test; + +import cryptator.gen.WordSumTuplesBuilder; + +public class WSTuplesTest { + + private final int[] b = {2, 4, 8, 10, 12, 16}; + + private final WordSumTuplesBuilder builder = new WordSumTuplesBuilder(); + + public static int logFloor(double x, double b) { + return (int) Math.floor(Math.log(x) / Math.log(b)); + } + + @Test + public void testFloorLog() { + // Powers of two to avoid numerical errors + // The test fails with base 10, because of the floating point arithmetic. + int[] b = {2, 4, 8, 16}; + int n = 1024; + for (int i = 0; i < b.length; i++) { + for (int x = 1; x < n; x++) { + //System.out.println(x + " " + b[i] + " " + Math.log(x) / Math.log(b[i])); + assertEquals(logFloor(x, b[i]), WordSumTuplesBuilder.logFloor(x, b[i])); + } + } + } + + public void testMaxZ(int expected, int[] values, int k) { + assertEquals(expected, WordSumTuplesBuilder.getMaxZ(values, k)); + } + + @Test + public void testmaxZ() { + int[] v1 = new int[] {0}; + testMaxZ(0, v1, 0); + + int[] v2 = new int[] {0, 3, 3, 2, 1}; + testMaxZ(0, v2, 0); + testMaxZ(0, v2, 1); + testMaxZ(0, v2, 2); + testMaxZ(3, v2, 3); + testMaxZ(6, v2, 4); + } + + public void testTuples(int expected, int[] values, int base) { + builder.setBase(base); + final Tuples tuples = builder.buildTuples(values); + //System.out.println(tuples); + assertEquals(expected, tuples.nbTuples()); + + } + + @Test + public void testTuples1() { + int[] values = {0, 1, 1}; + for (int i = 0; i < b.length; i++) { + testTuples(4, values, b[i]); + } + } + + @Test + public void testTuples2() { + int[] values = {0, 1, 1, 1}; + for (int i = 0; i < b.length; i++) { + testTuples(11, values, b[i]); + } + } + + @Test + public void testTuples3() { + int[] values = {0, 1, 1, 1, 1}; + testTuples(22, values, 2); + } + + + @Test + public void testTuples4() { + int[] values = {0, 4, 2, 2, 1}; + testTuples(107, values, 2); + } + + + +} diff --git a/src/test/java/cryptator/WordsListModelTest.java b/src/test/java/cryptator/WordsListModelTest.java deleted file mode 100644 index 7319acb..0000000 --- a/src/test/java/cryptator/WordsListModelTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/** - * This file is part of cryptator, https://github.com/arnaud-m/cryptator - * - * Copyright (c) 2022, Université Côte d'Azur. All rights reserved. - * - * Licensed under the BSD 3-clause license. - * See LICENSE file in the project root for full license information. - */ -package cryptator; - -import static org.junit.Assert.assertEquals; - -import org.chocosolver.solver.Model; -import org.junit.Test; - -import cryptator.gen.WordsListModel; - -public class WordsListModelTest { - - private final String[] words = new String[] {"a", "b", "ba", "bb", "baa", "bab", "bba", "bbb", "baaa"}; - - @Test - public void testWLModel1() { - WordsListModel m = new WordsListModel(new Model(), words); - m.postMaxSymbolCountConstraint(1); - assertEquals(9, m.getModel().getSolver().streamSolutions().count()); - } - - @Test - public void testWLModel2() { - WordsListModel m = new WordsListModel(new Model(), words); - m.postMaxWordCountConstraint(2); - assertEquals(46, m.getModel().getSolver().streamSolutions().count()); - } - - @Test - public void testWLModel3() { - WordsListModel m = new WordsListModel(new Model(), words); - m.postMaxWordCountConstraint(3); - assertEquals(130, m.getModel().getSolver().streamSolutions().count()); - } - - @Test - public void testWLModel4() { - WordsListModel m = new WordsListModel(new Model(), words); - m.postMaxSymbolCountConstraint(1); - m.postMaxWordCountConstraint(2); - assertEquals(8, m.getModel().getSolver().streamSolutions().count()); - } - - -}