diff --git a/src/main/java/cryptator/config/CryptaLogConfig.java b/src/main/java/cryptator/config/CryptaLogConfig.java index ed46e4e..371a456 100644 --- a/src/main/java/cryptator/config/CryptaLogConfig.java +++ b/src/main/java/cryptator/config/CryptaLogConfig.java @@ -21,4 +21,8 @@ public final Verbosity getVerbosity() { return verbosity; } + public final void setVerbosity(Verbosity verbosity) { + this.verbosity = verbosity; + } + } diff --git a/src/main/java/cryptator/config/CryptagenConfig.java b/src/main/java/cryptator/config/CryptagenConfig.java index 3759a71..5f2a086 100644 --- a/src/main/java/cryptator/config/CryptagenConfig.java +++ b/src/main/java/cryptator/config/CryptagenConfig.java @@ -19,6 +19,9 @@ public class CryptagenConfig extends CryptaCmdConfig { @Option(name = "-grid", usage = "grid size for crossword cryptarithm)") private int gridSize = 0; + @Option(name = "-mult", handler = ExplicitBooleanOptionHandler.class, usage = "generate multiplication cryptarithms") + private boolean multModel = false; + @Option(name = "-ctry", usage = "country code for doubly true cryptarithms)") private String countryCode = "EN"; @@ -88,6 +91,14 @@ public final void setLightPropagation(final boolean lightPropagation) { this.lightPropagation = lightPropagation; } + public final boolean isMultModel() { + return multModel; + } + + public final void setMultModel(boolean multModel) { + this.multModel = multModel; + } + @Override public String toString() { return super.toString() + "\nc LANG " + langCode + "\nc THREADS " + nthreads + "\nc LIGHT_MOD " + lightModel diff --git a/src/main/java/cryptator/gen/CryptaGenModel.java b/src/main/java/cryptator/gen/CryptaGenModel.java index 2511a5a..be3532a 100644 --- a/src/main/java/cryptator/gen/CryptaGenModel.java +++ b/src/main/java/cryptator/gen/CryptaGenModel.java @@ -20,7 +20,13 @@ class CryptaMemberPair implements ICryptaGenSolver { protected final CryptaMemberLen left; - protected final CryptaMemberElt right; + protected final AbstractCryptaGenModel right; + + protected CryptaMemberPair(CryptaMemberLen left, CryptaMemberLen right) { + super(); + this.left = left; + this.right = right; + } public CryptaMemberPair(final Model model, final String[] words, final String prefix, boolean useMemberLen) { super(); @@ -49,7 +55,7 @@ public final CryptaMemberLen getLeft() { return left; } - public final CryptaMemberElt getRight() { + public final AbstractCryptaGenModel getRight() { return right; } @@ -59,16 +65,29 @@ public void buildModel() { postSymBreakLengthConstraint(); } - private void postSymBreakLengthConstraint() { + protected void postSymBreakLengthConstraint() { left.getMaxLength().le(right.getMaxLength()).post(); } + protected final void postDisjunctionConstraints(final BoolVar[] v) { + final BoolVar[] l = getLeft().getWordVars(); + final BoolVar[] r = getRight().getWordVars(); + for (int i = 0; i < v.length; i++) { + l[i].add(r[i]).eq(v[i]).post(); + } + } + + protected void postMaxLengthConstraint(final IntVar maxLen) { + getModel().max(maxLen, getLeft().getMaxLength(), getRight().getMaxLength()).post(); + + } + public final void postHeavyConstraints(final int base) { left.postLentghSumConstraints(right.getMaxLength(), base); } @Override - public final ICryptaNode recordCryptarithm() { + public ICryptaNode recordCryptarithm() { return GenerateUtil.recordAddition(left, right); } @@ -91,16 +110,12 @@ public void buildModel() { @Override protected void postMaxLengthConstraints() { - model.max(maxLength, addition.getLeft().getMaxLength(), addition.getRight().getMaxLength()).post(); + addition.postMaxLengthConstraint(maxLength); } @Override protected void postWordConstraints() { - final BoolVar[] l = addition.getLeft().getWordVars(); - final BoolVar[] r = addition.getRight().getWordVars(); - for (int i = 0; i < vwords.length; i++) { - l[i].add(r[i]).eq(vwords[i]).post(); - } + addition.postDisjunctionConstraints(vwords); } public void postMinLeftCountConstraints(final int base) { diff --git a/src/main/java/cryptator/gen/CryptaGenMult.java b/src/main/java/cryptator/gen/CryptaGenMult.java new file mode 100644 index 0000000..95b552a --- /dev/null +++ b/src/main/java/cryptator/gen/CryptaGenMult.java @@ -0,0 +1,140 @@ +/* + * This file is part of cryptator, https://github.com/arnaud-m/cryptator + * + * Copyright (c) 2023, 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.expression.discrete.arithmetic.ArExpression; +import org.chocosolver.solver.variables.BoolVar; +import org.chocosolver.solver.variables.IntVar; + +import cryptator.specs.ICryptaGenSolver; +import cryptator.specs.ICryptaNode; + +class CryptaMemberMult extends CryptaMemberPair { + + public CryptaMemberMult(Model model, String[] words, String prefix) { + super(new CryptaMemberLen(model, words, prefix + "L_"), new CryptaMemberLen(model, words, prefix + "R_")); + } + + @Override + public void buildModel() { + super.buildModel(); + left.getWordCount().ge(1).post(); + right.getWordCount().ge(1).post(); + } + + @Override + public ICryptaNode recordCryptarithm() { + return GenerateUtil.recordMultiplication(left, right); + } + + @Override + protected void postSymBreakLengthConstraint() { + super.postSymBreakLengthConstraint(); +// getModel().ifThen(left.getMaxLength().eq(right.getMaxLength()).decompose(), +// getModel().lexChainLess(right.getWordVars(), left.getWordVars())); + left.getMaxLength().eq(right.getMaxLength()).imp(left.getWordCount().ge(right.getWordCount())).post(); + } + + public void postMultHeavyConstraints() { + final IntVar sumL = getModel().sum("L_sumLength", left.lengths); + final IntVar sumR = getModel().sum("R_sumLength", ((CryptaMemberLen) right).lengths); + + final ArExpression minL = sumL.sub(left.getWordCount()).add(1); + final ArExpression minR = sumR.sub(right.getWordCount()).add(1); + minL.le(sumR).post(); + minR.le(sumL).post(); + + // FIXME sumL.le(8).post(); + // FIXME sumR.le(8).post(); + + } + +} + +public class CryptaGenMult extends AbstractCryptaListModel implements ICryptaGenSolver { + + private final CryptaMemberMult multiplication; + + public CryptaGenMult(final String[] words) { + super(new Model("Generate-Multiplication"), words); + multiplication = new CryptaMemberMult(model, words, ""); + } + + @Override + public void buildModel() { + super.buildModel(); + multiplication.buildModel(); + } + + @Override + protected void postMaxLengthConstraints() { + multiplication.postMaxLengthConstraint(maxLength); + maxLength.le(10).post(); + } + + @Override + protected void postWordConstraints() { + multiplication.postDisjunctionConstraints(vwords); + } + + public void postMinLeftCountConstraints(final int base) { + multiplication.postMultHeavyConstraints(); + } + + public void postFixedRightMemberConstraint() { + final BoolVar[] vars = multiplication.getRight().getWordVars(); + vars[vars.length - 1].eq(1).post(); + multiplication.getRight().getWordCount().eq(1).post(); + } + + private int getDoublyCoeff(int i) { + double coeff = Math.log(i) / Math.log((getN() - 1)); + return (int) Math.round(1000 * coeff); + + } + + public void postDoublyTrueConstraint(final int lb) { + final int n = getN(); + final IntVar sumL = model.intVar("L_SUMLOG", getDoublyCoeff(lb), getDoublyCoeff(n - 1)); + final IntVar sumR = model.intVar("R_SUMLOG", getDoublyCoeff(lb), getDoublyCoeff(n - 1)); + + final IntVar[] lvars = new IntVar[n + 1]; + System.arraycopy(multiplication.getLeft().getWordVars(), 0, lvars, 0, n); + lvars[n] = sumL; + + final IntVar[] rvars = new IntVar[n + 1]; + System.arraycopy(multiplication.getRight().getWordVars(), 0, rvars, 0, n); + rvars[n] = sumR; + + final int[] coeffs = new int[n + 1]; + coeffs[0] = IntVar.MIN_INT_BOUND; + for (int i = 1; i < n; i++) { + coeffs[i] = getDoublyCoeff(i); + } + coeffs[n] = -1; + + model.scalar(lvars, coeffs, "=", 0).post(); + model.scalar(rvars, coeffs, "=", 0).post(); + + sumL.sub(sumR).abs().mul(2).le(wordCount).post(); + } + + @Override + public final ICryptaNode recordCryptarithm() { + return multiplication.recordCryptarithm(); + } + + @Override + public String toString() { + return GenerateUtil.recordString(multiplication.getLeft(), " * ") + " = " + + GenerateUtil.recordString(multiplication.getRight(), " * "); + } + +} diff --git a/src/main/java/cryptator/gen/CryptaListGenerator.java b/src/main/java/cryptator/gen/CryptaListGenerator.java index 33f948b..b40aff3 100644 --- a/src/main/java/cryptator/gen/CryptaListGenerator.java +++ b/src/main/java/cryptator/gen/CryptaListGenerator.java @@ -79,6 +79,23 @@ ICryptaGenSolver buildAdditionSolver() { return gen; } + ICryptaGenSolver buildMultiplicationSolver() { + final CryptaGenMult gen = new CryptaGenMult(words.getWords()); + gen.buildModel(); + gen.postWordCountConstraints(Math.max(config.getMinLeftOperands(), 2) + 1, config.getMaxLeftOperands() + 1); + gen.postMaxSymbolCountConstraint(config.getArithmeticBase()); + if (!config.isLightPropagation()) { + gen.postMinLeftCountConstraints(config.getArithmeticBase()); + } + if (words.hasRightMember()) { + gen.postFixedRightMemberConstraint(); + } + if (words.isDoublyTrue()) { + gen.postDoublyTrueConstraint(words.getLB()); + } + return gen; + } + ICryptaGenSolver buildCrosswordSolver() { final CryptaGenCrossword gen = new CryptaGenCrossword(config.getGridSize(), words.getWords(), config.isLightModel()); @@ -92,7 +109,11 @@ ICryptaGenSolver buildCrosswordSolver() { } private ICryptaGenSolver buildModel() { - return config.getGridSize() > 0 ? buildCrosswordSolver() : buildAdditionSolver(); + if (config.getGridSize() > 0) { + return buildCrosswordSolver(); + } else { + return config.isMultModel() ? buildMultiplicationSolver() : buildAdditionSolver(); + } } private Consumer buildConsumer(final IChocoModel gen, diff --git a/src/main/java/cryptator/gen/GenerateUtil.java b/src/main/java/cryptator/gen/GenerateUtil.java index 0e9a778..3dc584f 100644 --- a/src/main/java/cryptator/gen/GenerateUtil.java +++ b/src/main/java/cryptator/gen/GenerateUtil.java @@ -63,4 +63,12 @@ public static ICryptaNode recordAddition(final ICryptaGenModel left, final ICryp return reduceOperation(CryptaOperator.EQ, recordAddition(left), recordAddition(right)); } + public static ICryptaNode recordMultiplication(final ICryptaGenModel model) { + return reduceOperation(CryptaOperator.MUL, leafStream(model)); + } + + public static ICryptaNode recordMultiplication(final ICryptaGenModel left, final ICryptaGenModel right) { + return reduceOperation(CryptaOperator.EQ, recordMultiplication(left), recordMultiplication(right)); + } + } diff --git a/src/test/java/cryptator/GenerateTest.java b/src/test/java/cryptator/GenerateTest.java index 573daaa..ede32e5 100644 --- a/src/test/java/cryptator/GenerateTest.java +++ b/src/test/java/cryptator/GenerateTest.java @@ -59,13 +59,13 @@ private void testGenerate(final int expectedSolCount, final WordArray wordArray, private void testGenerate(final int expectedSolCount, final OptionalInt expectedCandCount, final WordArray wordArray, int gridSize) throws CryptaModelException { - configure(0, false, false); + configure(gridSize, false, false); testGenerate(expectedSolCount, expectedCandCount, wordArray); - configure(0, false, true); + configure(gridSize, false, true); testGenerate(expectedSolCount, expectedCandCount, wordArray); - configure(0, true, false); + configure(gridSize, true, false); testGenerate(expectedSolCount, expectedCandCount, wordArray); - configure(0, false, true); + configure(gridSize, false, true); testGenerate(expectedSolCount, expectedCandCount, wordArray); } @@ -77,6 +77,16 @@ private void testHeavyGenerate(final int expectedSolCount, final OptionalInt exp testGenerate(expectedSolCount, expectedCandCount, wordArray); } + private void testMultGenerate(final int expectedSolCount, final WordArray wordArray) throws CryptaModelException { + JULogUtil.configureSilentLoggers(); + config.setMultModel(true); + configure(0, false, false); + testGenerate(expectedSolCount, OptionalInt.empty(), wordArray); + configure(0, false, true); + testGenerate(expectedSolCount, OptionalInt.empty(), wordArray); + config.setMultModel(false); + } + @Test public void testSendMoreMoney() throws CryptaModelException { final WordArray words = new WordArray(Arrays.asList("send", "more", "money"), null); @@ -149,4 +159,37 @@ public void testCrossword3() throws CryptaModelException { testHeavyGenerate(2, OptionalInt.empty(), words, 3); } + @Test + public void testMult1() throws CryptaModelException { + WordArray words = new WordArray(Arrays.asList("mad", "man", "asylum"), null); + testMultGenerate(0, words); + } + + @Test + public void testMult2() throws CryptaModelException { + config.setArithmeticBase(9); + WordArray words = new WordArray(Arrays.asList("alfred", "e", "neuman"), null); + testMultGenerate(2, words); + config.setArithmeticBase(10); + } + + @Test + public void testMult3() throws CryptaModelException { + WordArray words = new WordArray(Arrays.asList("nora", "l", "aron"), null); + testMultGenerate(2, words); + } + + @Test + public void testMult4() throws CryptaModelException { + WordArray words = new WordArray(Arrays.asList("ba", "cba", "dcba"), null); + testMultGenerate(1, words); + } + + @Test + public void testMult5() throws CryptaModelException { + // FIXME should be 2 + WordArray words = new WordArray(Arrays.asList("north", "south", "east", "west"), null); + testMultGenerate(4, words); + } + } diff --git a/src/test/java/cryptator/MultTest.java b/src/test/java/cryptator/MultTest.java new file mode 100644 index 0000000..ad00b63 --- /dev/null +++ b/src/test/java/cryptator/MultTest.java @@ -0,0 +1,76 @@ +/* + * This file is part of cryptator, https://github.com/arnaud-m/cryptator + * + * Copyright (c) 2023, 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.junit.Test; + +import cryptator.gen.CryptaGenMult; + +public class MultTest { + + private void testMultModel(String[] words, int expectedSolutionCount) { + testMultModel(false, words, expectedSolutionCount); + } + + private void testMultModel(boolean isDoublyTrue, String[] words, int expectedSolutionCount) { + final CryptaGenMult m = new CryptaGenMult(words); + m.buildModel(); + m.postMinLeftCountConstraints(10); + if (isDoublyTrue) { + m.postDoublyTrueConstraint(0); + } + // System.out.println(m.getModel()); + // assertEquals(expectedSolutionCount, + // m.getModel().getSolver().streamSolutions().count()); + // Solution sol = new Solution(m.getModel()); +// m.getSolver().streamSolutions().forEach(s -> { +// System.out.println(m); +// // sol.record(); +// // System.out.println(sol); +// }); + // m.getSolver().printStatistics(); + // assertEquals(expectedSolutionCount, m.getSolver().getSolutionCount()); + assertEquals(expectedSolutionCount, m.getSolver().streamSolutions().count()); + + } + + @Test + public void testMult1() { + final String[] words = new String[] {"a", "bb", "ccc"}; + testMultModel(words, 1); + } + + @Test + public void testMult2() { + final String[] words = new String[] {"a", "bbb", "ccc"}; + testMultModel(words, 4); + } + + @Test + public void testMult3() { + final String[] words = new String[] {"a", "bb", "ccc", "dddd"}; + testMultModel(words, 5); + } + + @Test + public void testMult4() { + final String[] words = new String[] {"a", "bb", "ccc", "dddd", "eeeee"}; + testMultModel(words, 17); + } + + @Test + public void testMultDoublyTrue1() { + final String[] words = new String[] {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", + "nine", "ten", "eleven", "twelve"}; + testMultModel(true, words, 5); + } + +}