diff --git a/src/main/java/puzzle/Main.java b/src/main/java/puzzle/Main.java index 82d8901..c856888 100644 --- a/src/main/java/puzzle/Main.java +++ b/src/main/java/puzzle/Main.java @@ -11,6 +11,7 @@ import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import static puzzle.ExportFormat.*; @@ -18,8 +19,10 @@ import static puzzle.SwedishGenerator.*; import static puzzle.SwedishGenerator.loadWords; public class Main { + // ---------------- Top-level generatePuzzle ---------------- public record PuzzleResult(SwedishGenerator swe, Dict dict, Grid mask, FillResult filled) { } + final static String OUT_DIR = envOrDefault("OUT_DIR", "/data/puzzle"); final static Path PUZZLE_DIR = Paths.get(OUT_DIR, "puzzles"); static final Path INDEX_FILE = PUZZLE_DIR.resolve("index.json"); @@ -30,12 +33,18 @@ public class Main { static final Path OUTPUT_PATH = PUZZLE_DIR.resolve(FILE_NAME); static final String DATE_STRING = now.toLocalDate().toString(); + static final AtomicLong TOTAL_NODES = new AtomicLong(0); + static final AtomicLong TOTAL_BACKTRACKS = new AtomicLong(0); + static final AtomicLong TOTAL_ATTEMPTS = new AtomicLong(0); + static final AtomicLong TOTAL_SUCCESS = new AtomicLong(0); + static final AtomicLong TOTAL_SIMPLICITY = new AtomicLong(0); // Scaled by 100 for precision + @Data public static class Opts { public int seed = (int) (System.nanoTime() ^ System.currentTimeMillis()); public int pop = 18; - public int gens = 500; + public int gens = 2000; public String wordsPath = "nl_score_hints.csv"; public double minSimplicity = 0; // 0 means no limit public int threads = Math.max(1, Runtime.getRuntime().availableProcessors()); @@ -224,6 +233,8 @@ public class Main { i++; } else if (a.equals("--reindex")) { out.reindex = true; + } else if (a.equals("--verbose")) { + out.verbose = true; } else { throw new IllegalArgumentException("Unknown arg: " + a); } @@ -245,9 +256,10 @@ public class Main { info(String.format(Locale.ROOT, "loadTime : %.3f s", (tLoad1 - tLoad0) / 1e9)); section("Search"); - + var tSearch0 = System.currentTimeMillis(); var deadline = System.currentTimeMillis() + 40_000; + PuzzleResult resFinal = null; if (opts.threads > 1) { info("mode : multi-threaded (" + opts.threads + ")"); var executor = Executors.newFixedThreadPool(opts.threads); @@ -268,7 +280,8 @@ public class Main { var result = future.get(); if (result != null) { info("status : SOLVED"); - return result; + resFinal = result; + break; } // Submit another task if we still have time @@ -277,7 +290,7 @@ public class Main { completionService.submit(() -> attempt(new Rng(opts.seed + attempt), dict, opts)); } } - warn("status : UNSOLVED (timeout)"); + if (resFinal == null) warn("status : UNSOLVED (timeout)"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); warn("status : INTERRUPTED"); @@ -286,7 +299,6 @@ public class Main { } finally { executor.shutdownNow(); } - return null; } else { info("mode : single-threaded"); @@ -301,20 +313,57 @@ public class Main { if (result != null) { info("status : SOLVED"); info("foundAtTry : " + attempt); - return result; + resFinal = result; + break; } } - info("status : UNSOLVED (timeout)"); - return null; + if (resFinal == null) info("status : UNSOLVED (timeout)"); } + + var tSearch1 = System.currentTimeMillis(); + var searchTime = (tSearch1 - tSearch0) / 1000.0; + + section("Performance"); + info(String.format(Locale.ROOT, "totalNodes : %,d", TOTAL_NODES.get())); + info(String.format(Locale.ROOT, "totalBacks : %,d", TOTAL_BACKTRACKS.get())); + info(String.format(Locale.ROOT, "searchTime : %.2f s", searchTime)); + info(String.format(Locale.ROOT, "nodes/sec : %,d", (int) (TOTAL_NODES.get() / Math.max(0.001, searchTime)))); + + section("Material"); + info(String.format(Locale.ROOT, "attempts : %,d", TOTAL_ATTEMPTS.get())); + info(String.format(Locale.ROOT, "successRate : %.1f%%", (TOTAL_ATTEMPTS.get() == 0) ? 0 : (TOTAL_SUCCESS.get() * 100.0 / TOTAL_ATTEMPTS.get()))); + if (TOTAL_SUCCESS.get() > 0) { + info(String.format(Locale.ROOT, "avgSimplic : %.2f", (TOTAL_SIMPLICITY.get() / 100.0) / TOTAL_SUCCESS.get())); + } + info(String.format(Locale.ROOT, "dictWords : %,d", dict.wordz().length)); + + return resFinal; } static PuzzleResult attempt(Rng rng, Dict dict, Opts opts) { + TOTAL_ATTEMPTS.incrementAndGet(); var swe = new SwedishGenerator(); var mask = swe.generateMask(rng, dict.lenCounts(), opts.pop, opts.gens, opts.verbose); var filled = swe.fillMask(rng, mask, dict.index(), 200, opts.fillTimeout, opts.verbose); + TOTAL_NODES.addAndGet(filled.stats().nodes); + TOTAL_BACKTRACKS.addAndGet(filled.stats().backtracks); + if (filled.ok()) { + TOTAL_SUCCESS.incrementAndGet(); + TOTAL_SIMPLICITY.addAndGet((long) (filled.simplicity() * 100)); + } + + var name = Thread.currentThread().getName(); + var status = filled.ok() ? "SUCCESS" : "FAILED"; + var simplicity = String.format(Locale.ROOT, "%.2f", filled.simplicity()); + var nps = (int) (filled.stats().nodes / Math.max(0.001, filled.stats().seconds)); + + 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, filled.stats().seconds + ); + if (filled.ok() && (opts.minSimplicity <= 0 || filled.simplicity() >= opts.minSimplicity)) { return new PuzzleResult(swe, dict, mask, filled); } diff --git a/src/main/java/puzzle/SwedishGenerator.java b/src/main/java/puzzle/SwedishGenerator.java index 5a26665..535772a 100644 --- a/src/main/java/puzzle/SwedishGenerator.java +++ b/src/main/java/puzzle/SwedishGenerator.java @@ -718,7 +718,7 @@ public record SwedishGenerator(int[] buff) { public FillResult fillMask(Rng rng, Grid mask, DictEntry[] dictIndex, int logEveryMs, int timeLimitMs, boolean verbose) { - + boolean multiThreaded = Thread.currentThread().getName().contains("pool"); var grid = mask.deepCopyGrid(); var slots = extractSlots(grid); @@ -738,7 +738,7 @@ public record SwedishGenerator(int[] buff) { final var BAR_LEN = 22; Runnable renderProgress = () -> { - if (!verbose) return; + if (!verbose || multiThreaded) return; var now = System.currentTimeMillis(); if ((now - lastLog.get()) < logEveryMs) return; lastLog.set(now); @@ -901,14 +901,16 @@ public record SwedishGenerator(int[] buff) { renderProgress.run(); var ok = new Solver().backtrack(0); // final progress line - System.out.print("\r" + padRight("", 120) + "\r"); - System.out.flush(); + if (!multiThreaded) { + System.out.print("\r" + padRight("", 120) + "\r"); + System.out.flush(); + } stats.seconds = (System.currentTimeMillis() - t0) / 1000.0; var res = new FillResult(ok, grid, assigned, stats); // print a final progress line - if (verbose) { + if (verbose && !multiThreaded) { System.out.println( String.format(Locale.ROOT, "[######################] %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %.1fs",