solver: construct odd-order solutions #21
+32
-9
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
|
||||
+37
-9
@@ -20,6 +20,37 @@ namespace {
|
||||
return std::chrono::duration<double>(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
|
||||
|
||||
@@ -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<bool Instrument>
|
||||
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<false>(n, nullptr);
|
||||
}
|
||||
|
||||
auto search_solution_instrumented(size_t const n,
|
||||
SearchCounters &counters) noexcept -> Results {
|
||||
counters = {};
|
||||
return search_solution_impl<true>(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<Square> 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<false>(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<true>(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
|
||||
|
||||
|
||||
+63
-18
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <span>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
@@ -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<Placement> placements)
|
||||
-> std::vector<Placement> {
|
||||
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(),
|
||||
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<Placement, 9>{{
|
||||
{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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user