introduce bitloops

This commit is contained in:
mike
2026-01-17 04:35:53 +01:00
parent 47b33af09d
commit 3bd7a0f958
6 changed files with 95 additions and 100 deletions

View File

@@ -7,9 +7,15 @@ import lombok.val;
import puzzle.Export.Gridded.Replacar.Cell; import puzzle.Export.Gridded.Replacar.Cell;
import puzzle.Export.LetterVisit.LetterAt; import puzzle.Export.LetterVisit.LetterAt;
import puzzle.Masker.Clues; import puzzle.Masker.Clues;
import puzzle.SwedishGenerator.Dict;
import puzzle.SwedishGenerator.DictEntry;
import puzzle.SwedishGenerator.FillResult; import puzzle.SwedishGenerator.FillResult;
import puzzle.SwedishGenerator.Grid; import puzzle.SwedishGenerator.Grid;
import puzzle.SwedishGenerator.Slotinfo; 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.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; 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) { record Placed(long lemma, int slotKey, int[] cells) {
static final char[] DIRECTION = { Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.HORIZONTAL }; 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) { record DictEntryDTO(LongArrayList words, IntListDTO[][] pos) {
public DictEntryDTO(int L) { public DictEntryDTO(int L) {

View File

@@ -19,7 +19,7 @@ import java.util.concurrent.atomic.AtomicLong;
import static puzzle.CsvIndexService.SC; import static puzzle.CsvIndexService.SC;
import static puzzle.Export.*; import static puzzle.Export.*;
import static puzzle.SwedishGenerator.*; import static puzzle.SwedishGenerator.*;
import static puzzle.SwedishGenerator.Dict.loadDict; import static puzzle.Export.Dicts.loadDict;
public class Main { public class Main {
@@ -393,8 +393,20 @@ public class Main {
var grid = mask.toGrid(); var grid = mask.toGrid();
var filled = fillMask(rng, slotInfo, grid, multiThreaded); var filled = fillMask(rng, slotInfo, grid, multiThreaded);
TOTAL_NODES.addAndGet(filled.stats().nodes); if (!multiThreaded) {
TOTAL_BACKTRACKS.addAndGet(filled.stats().backtracks); 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.nodes());
TOTAL_BACKTRACKS.addAndGet(filled.backtracks());
if (filled.ok()) { if (filled.ok()) {
val simpel = FillResult.calcSimpel(slotInfo); val simpel = FillResult.calcSimpel(slotInfo);
TOTAL_SUCCESS.incrementAndGet(); TOTAL_SUCCESS.incrementAndGet();
@@ -404,12 +416,12 @@ public class Main {
var name = Thread.currentThread().getName(); var name = Thread.currentThread().getName();
var status = filled.ok() ? "SUCCESS" : "FAILED"; var status = filled.ok() ? "SUCCESS" : "FAILED";
var simplicity = String.format(Locale.ROOT, "%.2f", filled.stats().simplicity); 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; var totalTime = (System.currentTimeMillis() - t0) / 1000.0;
System.out.printf(Locale.ROOT, System.out.printf(Locale.ROOT,
"[ATTEMPT] thread=%s | status=%s | nodes=%d | backtracks=%d | nps=%d | simplicity=%s | time=%.1fs%n", "[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)) { if (filled.ok() && (opts.minSimplicity <= 0 || filled.stats().simplicity >= opts.minSimplicity)) {

View File

@@ -9,15 +9,6 @@ import lombok.experimental.Delegate;
import lombok.val; import lombok.val;
import precomp.Neighbors9x8; import precomp.Neighbors9x8;
import precomp.Neighbors9x8.rci; 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 java.util.Locale;
import static java.lang.Long.*; import static java.lang.Long.*;
import static java.lang.Long.numberOfTrailingZeros; 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 long RANGE_0_624 = 624L - 0L + 1L;
public static final int CLUE_INDEX_MAX_SIZE = (288 | 3) + 1; 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)); } 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 @AllArgsConstructor
public static class Pick { public static class Pick {
@@ -88,24 +91,20 @@ public class SwedishGenerator {
@Accessors(fluent = true) @Accessors(fluent = true)
public static final class FillStats { public static final class FillStats {
final public long nodes;
final public long backtracks;
final public double seconds; final public double seconds;
final public int lastMRV; final public int lastMRV;
public double simplicity; public double simplicity;
} }
public static record FillResult(boolean ok, public static record FillResult(boolean ok, long nodes, long backtracks, @Delegate FillStats stats) {
@Delegate FillStats stats) {
static public long calcSimpel(Slotinfo[] slots) { static public long calcSimpel(Slotinfo[] slots) {
int k = 0; int k = 0;
long simpel = 0L; long simpel = 0L;
for (var n = 1; n < slots.length; n++) { for (var n = 1; n < slots.length; n++) {
if (slots[n].assign().w != X) { if (slots[n].assign.w != X) {
k++; k++;
simpel += Lemma.simpel(slots[n].assign().w); simpel += Lemma.simpel(slots[n].assign.w);
} }
} }
simpel = k == 0 ? 0 : simpel / k; simpel = k == 0 ? 0 : simpel / k;
@@ -176,52 +175,7 @@ public class SwedishGenerator {
static int unpackLetters(long w) { return (int) (w & LETTER_MASK); } static int unpackLetters(long w) { return (int) (w & LETTER_MASK); }
} }
public static record Dict( public static record Dict(DictEntry[] index, int length) { }
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);
}
}
}
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
@@ -391,7 +345,7 @@ public class SwedishGenerator {
"%s %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %s", "%s %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %s",
bar, done, TOTAL, nodes, backtracks, lastMRV, elapsed 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(); System.out.flush();
} }
boolean placeWord(final int key, final long lo, final long hi, final long w) { boolean placeWord(final int key, final long lo, final long hi, final long w) {
@@ -548,22 +502,7 @@ public class SwedishGenerator {
// final progress line // final progress line
grid.lo = solver.glo; grid.lo = solver.glo;
grid.hi = solver.ghi; 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));
} }
} }

View File

@@ -67,7 +67,7 @@ public class ExportFormatTest {
assertTrue(placeWord(grid.grid(), grid.grid().g, key, lo, 0L, TEST)); 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[]{ var puzzleResult = new PuzzleResult(new Clued(clues), grid, new Slotinfo[]{
new Slotinfo(key, lo, 0L, 0, new Assign(TEST), null) new Slotinfo(key, lo, 0L, 0, new Assign(TEST), null)
}, fillResult); }, fillResult);
@@ -109,7 +109,7 @@ public class ExportFormatTest {
void testExportFormatEmpty() { void testExportFormatEmpty() {
var grid = SwedishGeneratorTest.createEmpty(); var grid = SwedishGeneratorTest.createEmpty();
val clues = Clues.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 puzzleResult = new PuzzleResult(new Clued(clues), new Gridded(grid), new Slotinfo[0], fillResult);
var exported = puzzleResult.exportFormatFromFilled(1, new Rewards(0, 0, 0)); var exported = puzzleResult.exportFormatFromFilled(1, new Rewards(0, 0, 0));

View File

@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import puzzle.Export.ClueAt; import puzzle.Export.ClueAt;
import puzzle.Export.Clued; import puzzle.Export.Clued;
import puzzle.Export.Dicts;
import puzzle.Export.Gridded; import puzzle.Export.Gridded;
import puzzle.Export.LetterVisit.LetterAt; import puzzle.Export.LetterVisit.LetterAt;
import puzzle.Export.PuzzleResult; import puzzle.Export.PuzzleResult;
@@ -42,7 +43,7 @@ public class MainTest {
this.tries = 1; this.tries = 1;
this.verbose = false; this.verbose = false;
}}; }};
static final Dict dict = Dict.loadDict(opts.wordsPath); static final Dict dict = Dicts.loadDict(opts.wordsPath);
@Test @Test
void testExtractSlots() { void testExtractSlots() {

View File

@@ -4,6 +4,7 @@ import lombok.val;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import puzzle.Export.Clued; import puzzle.Export.Clued;
import puzzle.Export.Dicts;
import puzzle.Export.Gridded; import puzzle.Export.Gridded;
import puzzle.Export.IntListDTO; import puzzle.Export.IntListDTO;
import puzzle.Export.LetterVisit.LetterAt; import puzzle.Export.LetterVisit.LetterAt;
@@ -219,7 +220,7 @@ public class SwedishGeneratorTest {
assertEquals(5, Lemma.length(l1)); assertEquals(5, Lemma.length(l1));
assertEquals(LETTER_A, Lemma.byteAt(l1, 0)); 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()[3].words().length);
assertEquals(1, dict.index()[5].words().length); assertEquals(1, dict.index()[5].words().length);
@@ -275,7 +276,7 @@ public class SwedishGeneratorTest {
@Test @Test
void testCandidateInfoForPattern() { void testCandidateInfoForPattern() {
var dict = new Dict(WORDS2); var dict = Dicts.makeDict(WORDS2);
// Pattern "APP--" for length 5 // Pattern "APP--" for length 5
var info = candidateInfoForPattern(Context.get().bitset(), packPattern("APP"), dict.index()[5].posBitsets(), dict.index()[5].numlong()); 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) // This should detect a slot starting at 0,1 with length 2 (0,1 and 0,2)
var clues = Masker.Clues.createEmpty(); var clues = Masker.Clues.createEmpty();
clues.setClueLo(IDX_0_0.lo, CLUE_RIGHT); 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()); var slots = Masker.extractSlots(clues, dict.index());
assertEquals(1, slots.length); assertEquals(1, slots.length);
var s = slots[0]; var s = slots[0];
@@ -407,7 +408,7 @@ public class SwedishGeneratorTest {
val counts = new byte[SIZE]; val counts = new byte[SIZE];
counts[1] = 2; counts[1] = 2;
counts[2] = 3; counts[2] = 3;
var dict = new Dict(WORDS); var dict = Dicts.makeDict(WORDS);
var entry5 = dict.index()[5]; var entry5 = dict.index()[5];
// cross = (counts[1]-1) + (counts[2]-1) = 1 + 2 = 3 // cross = (counts[1]-1) + (counts[2]-1) = 1 + 2 = 3
// score = 3 * 10 + len(2) = 32 // score = 3 * 10 + len(2) = 32