From 3e667d6de07b0ea16121c56dca17c1baf4526a5f Mon Sep 17 00:00:00 2001 From: Codex instance Date: Thu, 30 Jul 2026 17:24:06 +0100 Subject: [PATCH] solver: construct odd-order solutions Avoid repeating the exponential search for odd orders at least nine. Search the even predecessor, translate its row-major placements to the enlarged board, and tile the new border. Keep direct search and construction explicit so benchmarks can report their costs separately. Verify the routed order-9 result independently and require its search counters to match order 8. Tests: Release, Debug, ASan and UBSan CTest (8 passed each) Refs: #6 --- BENCHMARKING.md | 41 +++++++++++++++----- CMakeLists.txt | 1 + TESTING.md | 6 ++- benchmarks/benchmark.cc | 46 ++++++++++++++++++----- main.cc | 56 ++++++++++++++++++++++++--- tests/tests.cc | 83 +++++++++++++++++++++++++++++++---------- 6 files changed, 189 insertions(+), 44 deletions(-) diff --git a/BENCHMARKING.md b/BENCHMARKING.md index 3ee0bf2..3579fa9 100644 --- a/BENCHMARKING.md +++ b/BENCHMARKING.md @@ -16,19 +16,20 @@ The default policy is one unrecorded warm-up followed by five repetitions per case, with a 600-second timeout for each process. Solver stdout is captured; the probe renders into an in-memory stream so grids do not perturb terminal I/O. Override the policy with `--orders`, `--warmup`, `--repetitions`, and -`--timeout`. Order 9 is intentionally supported but may be omitted during -local iteration because the current solver takes minutes: +`--timeout`. Order 9 uses the constructive odd-order path, searching order 8 +and then tiling the enlarged border, so it is suitable for normal local +benchmarking: ```sh python3 benchmarks/run.py --binary build-benchmark/partridge_benchmark \ - --orders 7 8 --warmup 1 --repetitions 5 --timeout 60 > benchmark.json + --orders 8 9 --warmup 1 --repetitions 5 --timeout 60 > benchmark.json ``` The JSON contains every run and median, range, and median absolute deviation -for solve, construction, independent validation, and rendering. The current -direct-search solver reports zero construction time: its setup and allocation -remain part of solve time. The separate construction field is reserved for -future constructive solution paths. The document also records compiler, +for solve, construction, independent validation, and rendering. Direct-search +cases report zero construction time: their setup and allocation remain part of +solve time. Constructed odd-order cases report predecessor search and +construction separately. The document also records compiler, flags, build type, commit, OS/CPU metadata, worker count, search policy, seed, timeouts, errors, invalid outputs, and the stdout policy. Search counts must be stable across repeated runs. Prune and task counters are zero for the @@ -80,8 +81,30 @@ Counts were stable across repetitions. Interleaved counter-free medians were overheads of 0.53% and 0.64% respectively. Construction time was zero; median independent validation and rendering times were each below 0.02 milliseconds. -Order 9 was not rerun for this initial baseline because its documented runtime -is several minutes. The default suite includes it with a per-run timeout. +Order 9 was not rerun for this initial baseline because the former direct +search took several minutes. The default suite includes it with a per-run +timeout. + +## Odd construction comparison + +The order-9 construction was measured from the issue 6 working tree based on +commit `ddf07e7`, using Apple Clang 21.0.0 with `-O3 -DNDEBUG`, macOS arm64, +one worker, one warm-up, and three measured repetitions. Counter-free and +counted runs were interleaved: + +| Order | Mode | Search median (range) | Construction median | Nodes | +| --- | --- | --- | --- | ---: | +| 8 | counter-free | 1.773 s (1.770–1.775 s) | 0 | 0 | +| 8 | counted | 1.849 s (1.848–1.850 s) | 0 | 60,485,176 | +| 9 | counter-free | 1.778 s (1.773–1.779 s) | 0.458 us | 0 | +| 9 | counted | 1.852 s (1.845–1.912 s) | 0.416 us | 60,485,176 | + +All runs completed with valid results and stable counters. The matching +order-8 and order-9 search counts demonstrate that the new path searches only +the predecessor. Compared with the recorded 158.69-second direct order-9 +elapsed time in `results.md`, the 1.778-second counter-free median plus +construction is approximately 89 times faster. The benchmark working tree +was necessarily dirty with the issue 6 implementation. New optimization issues should quote the exact JSON environment, policy, median/spread, stable counters, and counted overhead from diff --git a/CMakeLists.txt b/CMakeLists.txt index 1302ea4..19f79ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ if(BUILD_TESTING) tests/tests.cc) add_test(NAME validator COMMAND partridge_tests validator) add_test(NAME construction COMMAND partridge_tests construction) + add_test(NAME solver-odd-route COMMAND partridge_tests solver-odd-route) add_test(NAME rendering COMMAND partridge_tests rendering) add_test(NAME solver-small COMMAND partridge_tests solver-small) add_test(NAME solver-completion COMMAND partridge_tests solver-completion) diff --git a/TESTING.md b/TESTING.md index 2f5bc97..7af94d4 100644 --- a/TESTING.md +++ b/TESTING.md @@ -12,8 +12,10 @@ ctest --test-dir build --output-on-failure The tests independently check board dimensions, square multiplicities, bounds, overlap, and complete coverage. They cover small unsatisfiable solver inputs, a known order-8 solution, invalid placement diagnostics, and construction of an -order-9 solution from the order-8 fixture. The rendering test also formats the -known order-8 solution and checks the resulting grid dimensions and coverage. +order-9 solution from the order-8 fixture. The routed order-9 solver test +checks that its search counters exactly match order 8. The rendering test also +formats the known order-8 solution and checks the resulting grid dimensions and +coverage. ## Debug and sanitizers diff --git a/benchmarks/benchmark.cc b/benchmarks/benchmark.cc index c19a61e..e44894f 100644 --- a/benchmarks/benchmark.cc +++ b/benchmarks/benchmark.cc @@ -20,6 +20,37 @@ namespace { return std::chrono::duration(end - begin).count(); } + struct TimedSolution { + Results result; + double search_seconds; + double construction_seconds; + }; + + auto solve(std::uint64_t order, bool instrument, SearchCounters &counters) + -> TimedSolution { + auto const predecessor_order = + uses_odd_construction(order) ? order - 1 : order; + auto const search_begin = Clock::now(); + auto predecessor = + instrument + ? search_solution_instrumented(predecessor_order, counters) + : search_solution(predecessor_order); + auto const search_end = Clock::now(); + + auto const construction_begin = Clock::now(); + auto result = uses_odd_construction(order) + ? construct_odd_solution(order, std::move(predecessor)) + : std::move(predecessor); + auto const construction_end = Clock::now(); + return { + std::move(result), + seconds(search_begin, search_end), + uses_odd_construction(order) + ? seconds(construction_begin, construction_end) + : 0.0, + }; + } + auto valid(std::uint64_t order, Results const &result) -> bool { auto const expected = triangle_num(order); if (result.length() != expected) { @@ -71,19 +102,16 @@ int main(int argc, char **argv) { } SearchCounters counters; - auto const solve_begin = Clock::now(); - auto result = instrument ? find_solution_instrumented(order, counters) - : find_solution(order); - auto const solve_end = Clock::now(); + auto timed = solve(order, instrument, counters); auto const validation_begin = Clock::now(); - auto const validation_ok = valid(order, result); + auto const validation_ok = valid(order, timed.result); auto const validation_end = Clock::now(); auto const render_begin = Clock::now(); std::ostringstream rendered; auto *const old_buffer = std::cout.rdbuf(rendered.rdbuf()); - result.output(); + timed.result.output(); std::cout.rdbuf(old_buffer); auto const render_end = Clock::now(); @@ -92,10 +120,10 @@ int main(int argc, char **argv) { << "{\"schema_version\":1" << ",\"order\":" << order << ",\"instrumented\":" << boolean(instrument) - << ",\"solved\":" << boolean(!result.squares().empty()) + << ",\"solved\":" << boolean(!timed.result.squares().empty()) << ",\"valid\":" << boolean(validation_ok) - << ",\"timing_seconds\":{\"solve\":" << seconds(solve_begin, solve_end) - << ",\"construction\":0" + << ",\"timing_seconds\":{\"solve\":" << timed.search_seconds + << ",\"construction\":" << timed.construction_seconds << ",\"validation\":" << seconds(validation_begin, validation_end) << ",\"render\":" << seconds(render_begin, render_end) << "}" << ",\"counters\":{\"search_nodes\":" << counters.search_nodes diff --git a/main.cc b/main.cc index e92fb7d..64b4f9b 100644 --- a/main.cc +++ b/main.cc @@ -245,12 +245,12 @@ namespace { size_t completed_tasks = 0; }; - /** Find a solution to the \a n th Partridge problem. + /** Search directly for a solution to the \a n th Partridge problem. * * Returns the grid of the solution. */ template - auto find_solution_impl(size_t const n, SearchCounters *const counters) noexcept + auto search_solution_impl(size_t const n, SearchCounters *const counters) noexcept -> Results { /* Implementation is iterative, as opposed to recursive. * @@ -343,14 +343,60 @@ namespace { return {length, sqs}; } + auto search_solution(size_t const n) noexcept -> Results { + return search_solution_impl(n, nullptr); + } + + auto search_solution_instrumented(size_t const n, + SearchCounters &counters) noexcept -> Results { + counters = {}; + return search_solution_impl(n, &counters); + } + + /** Construct an odd-order solution from its even-order predecessor. */ + auto construct_odd_solution(size_t const odd_order, Results predecessor) + -> Results { + assert(odd_order >= 9); + assert(odd_order % 2 == 1); + assert(predecessor.length() == triangle_num(odd_order - 1)); + + auto const old_length = predecessor.length(); + auto const new_length = triangle_num(odd_order); + std::vector squares; + squares.reserve(predecessor.squares().size() + odd_order); + for (auto const &square: predecessor.squares()) { + auto const x = square.pos() % old_length; + auto const y = square.pos() / old_length; + squares.emplace_back(x + y * new_length, square.length()); + } + + for (size_t y = 0; y < old_length; y += odd_order) { + squares.emplace_back(old_length + y * new_length, odd_order); + } + for (size_t x = 0; x <= old_length; x += odd_order) { + squares.emplace_back(x + old_length * new_length, odd_order); + } + return {new_length, std::move(squares)}; + } + + [[nodiscard]] auto uses_odd_construction(size_t const n) noexcept -> bool { + return n >= 9 && n % 2 == 1; + } + auto find_solution(size_t const n) noexcept -> Results { - return find_solution_impl(n, nullptr); + if (uses_odd_construction(n)) { + return construct_odd_solution(n, search_solution(n - 1)); + } + return search_solution(n); } auto find_solution_instrumented(size_t const n, SearchCounters &counters) noexcept -> Results { - counters = {}; - return find_solution_impl(n, &counters); + if (uses_odd_construction(n)) { + return construct_odd_solution( + n, search_solution_instrumented(n - 1, counters)); + } + return search_solution_instrumented(n, counters); } } // anon namespace diff --git a/tests/tests.cc b/tests/tests.cc index 3bc6fe4..c9757a0 100644 --- a/tests/tests.cc +++ b/tests/tests.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -16,6 +17,8 @@ namespace { std::uint64_t x; std::uint64_t y; std::uint64_t side; + + auto operator==(Placement const &) const noexcept -> bool = default; }; struct Validation { @@ -148,21 +151,6 @@ namespace { return Results(side, std::move(squares)); } - auto construct_next_odd(std::uint64_t even_order, - std::vector placements) - -> std::vector { - auto const old_side = even_order * (even_order + 1) / 2; - auto const square_side = even_order + 1; - - for (std::uint64_t y = 0; y < old_side; y += square_side) { - placements.push_back({old_side, y, square_side}); - } - for (std::uint64_t x = 0; x <= old_side; x += square_side) { - placements.push_back({x, old_side, square_side}); - } - return placements; - } - auto expect(bool condition, std::string const &message) -> int { if (condition) { return 0; @@ -225,10 +213,64 @@ namespace { } auto test_construction() -> int { - auto const constructed = construct_next_odd(8, known_order_8()); - auto const validation = validate(9, 45, 45, constructed); - return expect(validation.valid(), - "even-to-odd construction was rejected:\n" + validation.text()); + auto predecessor = renderable_result(36, known_order_8()); + auto const predecessor_count = predecessor.squares().size(); + auto const constructed = construct_odd_solution(9, std::move(predecessor)); + auto const converted = to_independent(constructed); + auto const validation = validate(9, constructed); + + int failures = 0; + failures += expect( + validation.valid(), + "even-to-odd construction was rejected:\n" + validation.text()); + failures += expect(constructed.length() == 45, + "constructed board has the wrong side length"); + failures += expect(converted.placements.size() == predecessor_count + 9, + "construction did not add exactly nine squares"); + + auto const expected_border = std::array{{ + {36, 0, 9}, {36, 9, 9}, {36, 18, 9}, {36, 27, 9}, + {0, 36, 9}, {9, 36, 9}, {18, 36, 9}, {27, 36, 9}, + {36, 36, 9}, + }}; + failures += expect( + std::ranges::equal( + std::span(converted.placements).subspan(predecessor_count), + expected_border), + "constructed border coordinates are incorrect"); + failures += expect( + std::ranges::equal( + std::span(converted.placements).first(predecessor_count), + known_order_8()), + "construction translated predecessor coordinates unexpectedly"); + return failures; + } + + auto test_odd_solver_route() -> int { + SearchCounters even_counters; + SearchCounters odd_counters; + auto const even = find_solution_instrumented(8, even_counters); + auto const odd = find_solution_instrumented(9, odd_counters); + + int failures = 0; + failures += expect( + !uses_odd_construction(1) && !uses_odd_construction(7) && + !uses_odd_construction(8) && uses_odd_construction(9) && + !uses_odd_construction(10) && uses_odd_construction(11), + "odd construction route does not preserve direct handling boundaries"); + auto const validation = validate(9, odd); + failures += expect(validation.valid(), + "order-9 routed result is invalid:\n" + validation.text()); + failures += expect(odd.squares().size() == even.squares().size() + 9, + "order-9 route did not construct from order 8"); + failures += expect( + odd_counters.search_nodes == even_counters.search_nodes && + odd_counters.loop_iterations == even_counters.loop_iterations && + odd_counters.attempted_placements == + even_counters.attempted_placements && + odd_counters.backtracks == even_counters.backtracks, + "order-9 route did not perform exactly the order-8 search"); + return failures; } auto test_rendering() -> int { @@ -328,6 +370,9 @@ int main(int argc, char **argv) { if (test == "construction") { return test_construction(); } + if (test == "solver-odd-route") { + return test_odd_solver_route(); + } if (test == "rendering") { return test_rendering(); } -- 2.54.0