Full-height skyline columns partition the remaining board. Add sound gcd and bounded subset-sum checks for the resulting component areas, with boundary-event and periodic benchmark schedules. Keep the rules disabled by default because their small tree reductions do not recover their measured cost. Record the rejected default and scheduling evidence so it can be revisited only with new data. Tests: Debug CTest (14 passed) Tests: ASan+UBSan CTest (14 passed) Refs: #13
278 lines
10 KiB
C++
278 lines
10 KiB
C++
/*
|
|
* Copyright 2026, Matthew-Gretton-Dann
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
#define PARTRIDGE_TESTING
|
|
#include "../main.cc"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <sstream>
|
|
#include <string>
|
|
|
|
namespace {
|
|
using Clock = std::chrono::steady_clock;
|
|
|
|
auto seconds(Clock::time_point begin, Clock::time_point end) -> double {
|
|
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,
|
|
SearchPolicy policy, bool direct_search,
|
|
SymmetryBreaking symmetry,
|
|
Pruning pruning,
|
|
ComponentPruning component_pruning,
|
|
ComponentSchedule component_schedule,
|
|
SearchCounters &counters)
|
|
-> TimedSolution {
|
|
auto const predecessor_order =
|
|
!direct_search && uses_odd_construction(order) ? order - 1 : order;
|
|
auto const search_begin = Clock::now();
|
|
auto predecessor =
|
|
instrument
|
|
? search_solution_instrumented(predecessor_order, counters,
|
|
policy, symmetry, pruning,
|
|
component_pruning,
|
|
component_schedule)
|
|
: search_solution(predecessor_order, policy, symmetry, pruning,
|
|
component_pruning, component_schedule);
|
|
auto const search_end = Clock::now();
|
|
|
|
auto const construction_begin = Clock::now();
|
|
auto result = !direct_search && 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),
|
|
!direct_search && 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) {
|
|
return false;
|
|
}
|
|
|
|
std::vector<std::uint64_t> multiplicities(order + 1);
|
|
std::vector<bool> occupied(expected * expected);
|
|
for (auto const &square: result.squares()) {
|
|
auto const side = square.length();
|
|
auto const x0 = square.pos() % expected;
|
|
auto const y0 = square.pos() / expected;
|
|
if (side == 0 || side > order || x0 + side > expected ||
|
|
y0 + side > expected) {
|
|
return false;
|
|
}
|
|
++multiplicities[side];
|
|
for (auto y = y0; y < y0 + side; ++y) {
|
|
for (auto x = x0; x < x0 + side; ++x) {
|
|
auto &&cell = occupied[x + y * expected];
|
|
if (cell) {
|
|
return false;
|
|
}
|
|
cell = true;
|
|
}
|
|
}
|
|
}
|
|
for (std::uint64_t side = 1; side <= order; ++side) {
|
|
if (multiplicities[side] != side) {
|
|
return result.squares().empty() && order < 8;
|
|
}
|
|
}
|
|
return std::ranges::find(occupied, false) == occupied.end();
|
|
}
|
|
|
|
auto boolean(bool value) -> char const * { return value ? "true" : "false"; }
|
|
|
|
auto pruning_name(Pruning const pruning) -> char const * {
|
|
switch (pruning) {
|
|
case Pruning::all: return "all";
|
|
case Pruning::valley_capacity: return "valley-capacity";
|
|
case Pruning::large_square: return "large-square";
|
|
case Pruning::disabled: return "none";
|
|
}
|
|
assert(false);
|
|
return "none";
|
|
}
|
|
|
|
auto component_pruning_name(ComponentPruning const pruning) -> char const * {
|
|
switch (pruning) {
|
|
case ComponentPruning::disabled: return "none";
|
|
case ComponentPruning::gcd: return "gcd";
|
|
case ComponentPruning::subset_sum: return "subset-sum";
|
|
}
|
|
assert(false);
|
|
return "none";
|
|
}
|
|
|
|
auto component_schedule_name(ComponentSchedule const schedule)
|
|
-> char const * {
|
|
return schedule == ComponentSchedule::boundary ? "boundary"
|
|
: "periodic-8";
|
|
}
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc < 3 || argc > 9) {
|
|
std::cerr << "usage: partridge_benchmark ORDER counters|plain "
|
|
"[ascending|descending|best-fit] [public|direct] "
|
|
"[d4|none] [all|valley-capacity|large-square|none] "
|
|
"[subset-sum|gcd|none] [boundary|periodic-8]\n";
|
|
return 2;
|
|
}
|
|
auto const order = static_cast<std::uint64_t>(std::strtoull(argv[1], nullptr, 10));
|
|
auto const instrument = std::string_view(argv[2]) == "counters";
|
|
if (!instrument && std::string_view(argv[2]) != "plain") {
|
|
std::cerr << "counter mode must be counters or plain\n";
|
|
return 2;
|
|
}
|
|
auto const policy =
|
|
argc < 4 || std::string_view(argv[3]) == "ascending"
|
|
? SearchPolicy::ascending
|
|
: std::string_view(argv[3]) == "descending"
|
|
? SearchPolicy::descending
|
|
: SearchPolicy::best_fit;
|
|
if (argc >= 4 && std::string_view(argv[3]) != "ascending" &&
|
|
std::string_view(argv[3]) != "descending" &&
|
|
std::string_view(argv[3]) != "best-fit") {
|
|
std::cerr << "search policy must be ascending, descending, or best-fit\n";
|
|
return 2;
|
|
}
|
|
auto const direct_search =
|
|
argc >= 5 && std::string_view(argv[4]) == "direct";
|
|
if (argc >= 5 && std::string_view(argv[4]) != "public" &&
|
|
std::string_view(argv[4]) != "direct") {
|
|
std::cerr << "search route must be public or direct\n";
|
|
return 2;
|
|
}
|
|
auto const symmetry =
|
|
argc < 6 || std::string_view(argv[5]) == "d4"
|
|
? SymmetryBreaking::d4_unit_square
|
|
: SymmetryBreaking::disabled;
|
|
if (argc >= 6 && std::string_view(argv[5]) != "d4" &&
|
|
std::string_view(argv[5]) != "none") {
|
|
std::cerr << "symmetry mode must be d4 or none\n";
|
|
return 2;
|
|
}
|
|
auto const pruning =
|
|
argc < 7 || std::string_view(argv[6]) == "all"
|
|
? Pruning::all
|
|
: std::string_view(argv[6]) == "valley-capacity"
|
|
? Pruning::valley_capacity
|
|
: std::string_view(argv[6]) == "large-square"
|
|
? Pruning::large_square
|
|
: Pruning::disabled;
|
|
if (argc >= 7 && std::string_view(argv[6]) != "all" &&
|
|
std::string_view(argv[6]) != "valley-capacity" &&
|
|
std::string_view(argv[6]) != "large-square" &&
|
|
std::string_view(argv[6]) != "none") {
|
|
std::cerr << "pruning mode must be all, valley-capacity, "
|
|
"large-square, or none\n";
|
|
return 2;
|
|
}
|
|
auto const component_pruning =
|
|
argc < 8 || std::string_view(argv[7]) == "none"
|
|
? ComponentPruning::disabled
|
|
: std::string_view(argv[7]) == "gcd"
|
|
? ComponentPruning::gcd
|
|
: ComponentPruning::subset_sum;
|
|
if (argc >= 8 && std::string_view(argv[7]) != "subset-sum" &&
|
|
std::string_view(argv[7]) != "gcd" &&
|
|
std::string_view(argv[7]) != "none") {
|
|
std::cerr << "component pruning mode must be subset-sum, gcd, or none\n";
|
|
return 2;
|
|
}
|
|
auto const component_schedule =
|
|
argc < 9 || std::string_view(argv[8]) == "boundary"
|
|
? ComponentSchedule::boundary
|
|
: ComponentSchedule::periodic_eight;
|
|
if (argc == 9 && std::string_view(argv[8]) != "boundary" &&
|
|
std::string_view(argv[8]) != "periodic-8") {
|
|
std::cerr << "component schedule must be boundary or periodic-8\n";
|
|
return 2;
|
|
}
|
|
|
|
SearchCounters counters;
|
|
auto timed =
|
|
solve(order, instrument, policy, direct_search, symmetry, pruning,
|
|
component_pruning, component_schedule, counters);
|
|
|
|
auto const validation_begin = Clock::now();
|
|
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());
|
|
timed.result.output();
|
|
std::cout.rdbuf(old_buffer);
|
|
auto const render_end = Clock::now();
|
|
|
|
std::cout.precision(17);
|
|
std::cout
|
|
<< "{\"schema_version\":1"
|
|
<< ",\"order\":" << order
|
|
<< ",\"instrumented\":" << boolean(instrument)
|
|
<< ",\"candidate_order\":\""
|
|
<< (policy == SearchPolicy::ascending
|
|
? "ascending"
|
|
: policy == SearchPolicy::descending ? "descending" : "best-fit")
|
|
<< "\""
|
|
<< ",\"search_route\":\""
|
|
<< (direct_search ? "direct" : "public") << "\""
|
|
<< ",\"symmetry_breaking\":\""
|
|
<< (symmetry == SymmetryBreaking::d4_unit_square ? "d4" : "none")
|
|
<< "\""
|
|
<< ",\"pruning\":\""
|
|
<< pruning_name(pruning) << "\""
|
|
<< ",\"component_pruning\":\""
|
|
<< component_pruning_name(component_pruning) << "\""
|
|
<< ",\"component_schedule\":\""
|
|
<< component_schedule_name(component_schedule) << "\""
|
|
<< ",\"solved\":" << boolean(!timed.result.squares().empty())
|
|
<< ",\"valid\":" << boolean(validation_ok)
|
|
<< ",\"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
|
|
<< ",\"loop_iterations\":" << counters.loop_iterations
|
|
<< ",\"attempted_placements\":" << counters.attempted_placements
|
|
<< ",\"backtracks\":" << counters.backtracks
|
|
<< ",\"prune_checks\":" << counters.prune_checks
|
|
<< ",\"prune_hits\":" << counters.prune_hits
|
|
<< ",\"valley_capacity_checks\":"
|
|
<< counters.valley_capacity_checks
|
|
<< ",\"valley_capacity_prunes\":"
|
|
<< counters.valley_capacity_prunes
|
|
<< ",\"large_square_checks\":"
|
|
<< counters.large_square_checks
|
|
<< ",\"large_square_prunes\":"
|
|
<< counters.large_square_prunes
|
|
<< ",\"component_area_checks\":"
|
|
<< counters.component_area_checks
|
|
<< ",\"component_area_prunes\":"
|
|
<< counters.component_area_prunes
|
|
<< ",\"component_gcd_prunes\":"
|
|
<< counters.component_gcd_prunes
|
|
<< ",\"component_subset_prunes\":"
|
|
<< counters.component_subset_prunes
|
|
<< ",\"generated_tasks\":" << counters.generated_tasks
|
|
<< ",\"completed_tasks\":" << counters.completed_tasks << "}}\n";
|
|
return validation_ok ? 0 : 1;
|
|
}
|