From 3bd7a0f958c3e7a5eafd854e2e26d44ea07dcc42 Mon Sep 17 00:00:00 2001 From: mike Date: Sat, 17 Jan 2026 04:35:53 +0100 Subject: [PATCH] introduce bitloops --- src/main/java/puzzle/Export.java | 60 ++++++++++-- src/main/java/puzzle/Main.java | 22 ++++- src/main/java/puzzle/SwedishGenerator.java | 97 ++++--------------- src/test/java/puzzle/ExportFormatTest.java | 4 +- src/test/java/puzzle/MainTest.java | 3 +- .../java/puzzle/SwedishGeneratorTest.java | 9 +- 6 files changed, 95 insertions(+), 100 deletions(-) diff --git a/src/main/java/puzzle/Export.java b/src/main/java/puzzle/Export.java index 34eb736..d01bb0b 100644 --- a/src/main/java/puzzle/Export.java +++ b/src/main/java/puzzle/Export.java @@ -7,9 +7,15 @@ import lombok.val; import puzzle.Export.Gridded.Replacar.Cell; import puzzle.Export.LetterVisit.LetterAt; import puzzle.Masker.Clues; +import puzzle.SwedishGenerator.Dict; +import puzzle.SwedishGenerator.DictEntry; import puzzle.SwedishGenerator.FillResult; import puzzle.SwedishGenerator.Grid; import puzzle.SwedishGenerator.Slotinfo; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -191,15 +197,6 @@ public record Export() { } } - interface Bit1029 { - - static long[] bit1029() { return new long[2048]; } - static int wordIndex(int bitIndex) { return bitIndex >> 6; } - static boolean get(long[] bits, int bitIndex) { return (bits[wordIndex(bitIndex)] & 1L << bitIndex) != 0L; } - static void set(long[] bits, int bitIndex) { bits[wordIndex(bitIndex)] |= 1L << bitIndex; } - static void clear(long[] bits, int bitIndex) { bits[wordIndex(bitIndex)] &= ~(1L << bitIndex); } - } - record Placed(long lemma, int slotKey, int[] cells) { static final char[] DIRECTION = { Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.HORIZONTAL }; @@ -286,6 +283,51 @@ public record Export() { } } + interface Dicts { + + static Dict loadDict(String wordsPath) { + try { + var map = new LongArrayList(100_000); + Files.lines(Path.of(wordsPath), StandardCharsets.UTF_8).forEach(line -> CsvIndexService.lineToLemma(line, map::add)); + return makeDict(map.toArray()); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Failed to load dictionary from " + wordsPath, e); + } + } + static Dict makeDict(long[] wordz) { + var index = new DictEntryDTO[SwedishGenerator.MAX_WORD_LENGTH_PLUS_ONE]; + Arrays.setAll(index, i -> new DictEntryDTO(i)); + for (var lemma : wordz) { + var L = Lemma.length(lemma); + + var entry = index[L]; + var idx = entry.words().size(); + entry.words().add(lemma); + for (var i = 0; i < L; i++) entry.pos()[i][Lemma.byteAt(lemma, i) - 1].add(idx); + } + for (int i = SwedishGenerator.MIN_LEN; i < index.length; i++) if (index[i].words().size() <= 0) throw new RuntimeException("No words for length " + i); + return new Dict(Arrays.stream(index).map(i -> { + var words = i.words().toArray(); + int numWords = words.length; + int numLongs = (numWords + 63) >>> 6; + var bitsets = new long[i.pos().length * 26][numLongs]; + for (int p = 0; p < i.pos().length; p++) { + for (int l = 0; l < 26; l++) { + var list = i.pos()[p][l]; + var bs = bitsets[p * 26 + l]; + for (int k = 0; k < list.size(); k++) { + int wordIdx = list.data()[k]; + bs[wordIdx >>> 6] |= (1L << (wordIdx & 63)); + } + } + } + return new DictEntry(words, bitsets, words.length, (words.length + 63) >>> 6); + }).toArray(DictEntry[]::new), + Arrays.stream(index).mapToInt(i -> i.words().size()).sum()); + } + } + record DictEntryDTO(LongArrayList words, IntListDTO[][] pos) { public DictEntryDTO(int L) { diff --git a/src/main/java/puzzle/Main.java b/src/main/java/puzzle/Main.java index 65ea77c..b3e768a 100644 --- a/src/main/java/puzzle/Main.java +++ b/src/main/java/puzzle/Main.java @@ -19,7 +19,7 @@ import java.util.concurrent.atomic.AtomicLong; import static puzzle.CsvIndexService.SC; import static puzzle.Export.*; import static puzzle.SwedishGenerator.*; -import static puzzle.SwedishGenerator.Dict.loadDict; +import static puzzle.Export.Dicts.loadDict; public class Main { @@ -392,9 +392,21 @@ public class Main { val slotInfo = Masker.scoreSlots(new int[slots.length], slots); var grid = mask.toGrid(); var filled = fillMask(rng, slotInfo, grid, multiThreaded); + + if (!multiThreaded) { + System.out.print("\r" + Strings.padRight("", 120) + "\r"); + System.out.flush(); + } + // print a final progress line + if (Main.VERBOSE && !multiThreaded) { + System.out.printf(Locale.ROOT, + "[######################] %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %.1fs%n", + Slotinfo.wordCount(0, slotInfo), slotInfo.length, filled.nodes(), filled.backtracks(), filled.stats().lastMRV(), filled.stats().seconds() + ); + } - TOTAL_NODES.addAndGet(filled.stats().nodes); - TOTAL_BACKTRACKS.addAndGet(filled.stats().backtracks); + TOTAL_NODES.addAndGet(filled.nodes()); + TOTAL_BACKTRACKS.addAndGet(filled.backtracks()); if (filled.ok()) { val simpel = FillResult.calcSimpel(slotInfo); TOTAL_SUCCESS.incrementAndGet(); @@ -404,12 +416,12 @@ public class Main { var name = Thread.currentThread().getName(); var status = filled.ok() ? "SUCCESS" : "FAILED"; var simplicity = String.format(Locale.ROOT, "%.2f", filled.stats().simplicity); - var nps = (int) (filled.stats().nodes / Math.max(0.001, filled.stats().seconds)); + var nps = (int) (filled.nodes() / Math.max(0.001, filled.stats().seconds)); var totalTime = (System.currentTimeMillis() - t0) / 1000.0; System.out.printf(Locale.ROOT, "[ATTEMPT] thread=%s | status=%s | nodes=%d | backtracks=%d | nps=%d | simplicity=%s | time=%.1fs%n", - name, status, filled.stats().nodes, filled.stats().backtracks, nps, simplicity, totalTime + name, status, filled.nodes(), filled.backtracks(), nps, simplicity, totalTime ); if (filled.ok() && (opts.minSimplicity <= 0 || filled.stats().simplicity >= opts.minSimplicity)) { diff --git a/src/main/java/puzzle/SwedishGenerator.java b/src/main/java/puzzle/SwedishGenerator.java index 17a0b43..9933c06 100644 --- a/src/main/java/puzzle/SwedishGenerator.java +++ b/src/main/java/puzzle/SwedishGenerator.java @@ -9,15 +9,6 @@ import lombok.experimental.Delegate; import lombok.val; import precomp.Neighbors9x8; import precomp.Neighbors9x8.rci; -import puzzle.Export.Bit1029; -import puzzle.Export.DictEntryDTO; -import puzzle.Export.Gridded; -import puzzle.Export.Strings; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; import java.util.Locale; import static java.lang.Long.*; import static java.lang.Long.numberOfTrailingZeros; @@ -66,6 +57,18 @@ public class SwedishGenerator { public static final long RANGE_0_624 = 624L - 0L + 1L; public static final int CLUE_INDEX_MAX_SIZE = (288 | 3) + 1; public static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); } + interface Bit1029 { + + static long[] bit1029() { return new long[2048]; } + private static int wordIndex(int bitIndex) { return bitIndex >> 6; } + static boolean get(long[] bits, int bitIndex) { return (bits[wordIndex(bitIndex)] & 1L << bitIndex) != 0L; } + static void set(long[] bits, int bitIndex) { bits[wordIndex(bitIndex)] |= 1L << bitIndex; } + static void clear(long[] bits, int bitIndex) { bits[wordIndex(bitIndex)] &= ~(1L << bitIndex); } + } + static String padRight(String s, int n) { + if (s.length() >= n) return s; + return s + " ".repeat(n - s.length()); + } @AllArgsConstructor public static class Pick { @@ -88,24 +91,20 @@ public class SwedishGenerator { @Accessors(fluent = true) public static final class FillStats { - final public long nodes; - final public long backtracks; final public double seconds; final public int lastMRV; public double simplicity; } - public static record FillResult(boolean ok, - - @Delegate FillStats stats) { + public static record FillResult(boolean ok, long nodes, long backtracks, @Delegate FillStats stats) { static public long calcSimpel(Slotinfo[] slots) { int k = 0; long simpel = 0L; for (var n = 1; n < slots.length; n++) { - if (slots[n].assign().w != X) { + if (slots[n].assign.w != X) { k++; - simpel += Lemma.simpel(slots[n].assign().w); + simpel += Lemma.simpel(slots[n].assign.w); } } simpel = k == 0 ? 0 : simpel / k; @@ -176,52 +175,7 @@ public class SwedishGenerator { static int unpackLetters(long w) { return (int) (w & LETTER_MASK); } } - public static record Dict( - DictEntry[] index, - int length) { - - public Dict(long[] wordz) { - var index = new DictEntryDTO[MAX_WORD_LENGTH_PLUS_ONE]; - Arrays.setAll(index, i -> new DictEntryDTO(i)); - for (var lemma : wordz) { - var L = Lemma.length(lemma); - - var entry = index[L]; - var idx = entry.words().size(); - entry.words().add(lemma); - for (var i = 0; i < L; i++) entry.pos()[i][Lemma.byteAt(lemma, i) - 1].add(idx); - } - for (int i = MIN_LEN; i < index.length; i++) if (index[i].words().size() <= 0) throw new RuntimeException("No words for length " + i); - this(Arrays.stream(index).map(i -> { - var words = i.words().toArray(); - int numWords = words.length; - int numLongs = (numWords + 63) >>> 6; - var bitsets = new long[i.pos().length * 26][numLongs]; - for (int p = 0; p < i.pos().length; p++) { - for (int l = 0; l < 26; l++) { - var list = i.pos()[p][l]; - var bs = bitsets[p * 26 + l]; - for (int k = 0; k < list.size(); k++) { - int wordIdx = list.data()[k]; - bs[wordIdx >>> 6] |= (1L << (wordIdx & 63)); - } - } - } - return new DictEntry(words, bitsets, words.length, (words.length + 63) >>> 6); - }).toArray(DictEntry[]::new), - Arrays.stream(index).mapToInt(i -> i.words().size()).sum()); - } - static Dict loadDict(String wordsPath) { - try { - var map = new LongArrayList(100_000); - Files.lines(Path.of(wordsPath), StandardCharsets.UTF_8).forEach(line -> CsvIndexService.lineToLemma(line, map::add)); - return new Dict(map.toArray()); - } catch (IOException e) { - e.printStackTrace(); - throw new RuntimeException("Failed to load dictionary from " + wordsPath, e); - } - } - } + public static record Dict(DictEntry[] index, int length) { } @AllArgsConstructor @NoArgsConstructor @@ -391,7 +345,7 @@ public class SwedishGenerator { "%s %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %s", bar, done, TOTAL, nodes, backtracks, lastMRV, elapsed ); - System.out.print("\r" + Strings.padRight(msg, 120)); + System.out.print("\r" + padRight(msg, 120)); System.out.flush(); } boolean placeWord(final int key, final long lo, final long hi, final long w) { @@ -548,22 +502,7 @@ public class SwedishGenerator { // final progress line grid.lo = solver.glo; grid.hi = solver.ghi; - var res = new FillResult(ok, - new FillStats(solver.nodes, solver.backtracks, (System.currentTimeMillis() - t0) / 1000.0, solver.lastMRV)); - if (!multiThreaded) { - System.out.print("\r" + Strings.padRight("", 120) + "\r"); - System.out.flush(); - } - // print a final progress line - if (Main.VERBOSE && !multiThreaded) { - System.out.println( - String.format(Locale.ROOT, - "[######################] %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %.1fs", - Slotinfo.wordCount(0, slots), TOTAL, res.nodes(), res.backtracks(), res.lastMRV(), res.seconds() - ) - ); - } - return res; + return new FillResult(ok, solver.nodes, solver.backtracks, new FillStats((System.currentTimeMillis() - t0) / 1000.0, solver.lastMRV)); } } diff --git a/src/test/java/puzzle/ExportFormatTest.java b/src/test/java/puzzle/ExportFormatTest.java index d142811..1254c06 100644 --- a/src/test/java/puzzle/ExportFormatTest.java +++ b/src/test/java/puzzle/ExportFormatTest.java @@ -67,7 +67,7 @@ public class ExportFormatTest { assertTrue(placeWord(grid.grid(), grid.grid().g, key, lo, 0L, TEST)); - var fillResult = new FillResult(true, new FillStats(0, 0, 0, 0)); + var fillResult = new FillResult(true, 0, 0, new FillStats(0, 0)); var puzzleResult = new PuzzleResult(new Clued(clues), grid, new Slotinfo[]{ new Slotinfo(key, lo, 0L, 0, new Assign(TEST), null) }, fillResult); @@ -109,7 +109,7 @@ public class ExportFormatTest { void testExportFormatEmpty() { var grid = SwedishGeneratorTest.createEmpty(); val clues = Clues.createEmpty(); - var fillResult = new FillResult(true, new FillStats(0, 0, 0, 0)); + var fillResult = new FillResult(true, 0, 0, new FillStats(0, 0)); var puzzleResult = new PuzzleResult(new Clued(clues), new Gridded(grid), new Slotinfo[0], fillResult); var exported = puzzleResult.exportFormatFromFilled(1, new Rewards(0, 0, 0)); diff --git a/src/test/java/puzzle/MainTest.java b/src/test/java/puzzle/MainTest.java index 5c37ec2..749dc25 100644 --- a/src/test/java/puzzle/MainTest.java +++ b/src/test/java/puzzle/MainTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import puzzle.Export.ClueAt; import puzzle.Export.Clued; +import puzzle.Export.Dicts; import puzzle.Export.Gridded; import puzzle.Export.LetterVisit.LetterAt; import puzzle.Export.PuzzleResult; @@ -42,7 +43,7 @@ public class MainTest { this.tries = 1; this.verbose = false; }}; - static final Dict dict = Dict.loadDict(opts.wordsPath); + static final Dict dict = Dicts.loadDict(opts.wordsPath); @Test void testExtractSlots() { diff --git a/src/test/java/puzzle/SwedishGeneratorTest.java b/src/test/java/puzzle/SwedishGeneratorTest.java index 0ae5687..8bb66d9 100644 --- a/src/test/java/puzzle/SwedishGeneratorTest.java +++ b/src/test/java/puzzle/SwedishGeneratorTest.java @@ -4,6 +4,7 @@ import lombok.val; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import puzzle.Export.Clued; +import puzzle.Export.Dicts; import puzzle.Export.Gridded; import puzzle.Export.IntListDTO; import puzzle.Export.LetterVisit.LetterAt; @@ -219,7 +220,7 @@ public class SwedishGeneratorTest { assertEquals(5, Lemma.length(l1)); assertEquals(LETTER_A, Lemma.byteAt(l1, 0)); - var dict = new Dict(new long[]{ l1, l2, l2a, l4a, l6a, l7a, l8a }); + var dict = Dicts.makeDict(new long[]{ l1, l2, l2a, l4a, l6a, l7a, l8a }); assertEquals(1, dict.index()[3].words().length); assertEquals(1, dict.index()[5].words().length); @@ -275,7 +276,7 @@ public class SwedishGeneratorTest { @Test void testCandidateInfoForPattern() { - var dict = new Dict(WORDS2); + var dict = Dicts.makeDict(WORDS2); // Pattern "APP--" for length 5 var info = candidateInfoForPattern(Context.get().bitset(), packPattern("APP"), dict.index()[5].posBitsets(), dict.index()[5].numlong()); @@ -289,7 +290,7 @@ public class SwedishGeneratorTest { // This should detect a slot starting at 0,1 with length 2 (0,1 and 0,2) var clues = Masker.Clues.createEmpty(); clues.setClueLo(IDX_0_0.lo, CLUE_RIGHT); - var dict = new Dict(WORDS2); + var dict = Dicts.makeDict(WORDS2); var slots = Masker.extractSlots(clues, dict.index()); assertEquals(1, slots.length); var s = slots[0]; @@ -407,7 +408,7 @@ public class SwedishGeneratorTest { val counts = new byte[SIZE]; counts[1] = 2; counts[2] = 3; - var dict = new Dict(WORDS); + var dict = Dicts.makeDict(WORDS); var entry5 = dict.index()[5]; // cross = (counts[1]-1) + (counts[2]-1) = 1 + 2 = 3 // score = 3 * 10 + len(2) = 32