solver: break board dihedral symmetry #25
@@ -18,6 +18,9 @@ the probe renders into an in-memory stream so grids do not perturb terminal I/O.
|
|||||||
Override the policy with `--orders`, `--warmup`, `--repetitions`, `--timeout`,
|
Override the policy with `--orders`, `--warmup`, `--repetitions`, `--timeout`,
|
||||||
and `--candidate-order`. The choices are `ascending`, `descending`, and
|
and `--candidate-order`. The choices are `ascending`, `descending`, and
|
||||||
`best-fit`; ascending candidate sizes are the production default.
|
`best-fit`; ascending candidate sizes are the production default.
|
||||||
|
The production default also removes equivalent D4 board orientations by
|
||||||
|
constraining the unique unit square. Pass `--no-symmetry` to obtain an
|
||||||
|
otherwise identical unconstrained baseline.
|
||||||
Order 9 uses the constructive odd-order path, searching order 8 and then tiling
|
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:
|
the enlarged border, so it is suitable for normal local benchmarking:
|
||||||
|
|
||||||
@@ -69,6 +72,50 @@ Do not use wall-clock thresholds as correctness checks. Keep the generated
|
|||||||
JSON outside version control unless it is being deliberately added as a named
|
JSON outside version control unless it is being deliberately added as a named
|
||||||
comparison baseline.
|
comparison baseline.
|
||||||
|
|
||||||
|
## D4 board symmetry
|
||||||
|
|
||||||
|
Every solution contains exactly one 1-by-1 square. Rotations and reflections
|
||||||
|
of the whole board preserve square sizes, multiplicities, and coverage, so the
|
||||||
|
unit square can select the orientation without assigning identities to any of
|
||||||
|
the repeated larger squares. For a board of width `W`, the solver accepts the
|
||||||
|
unit square only in the closed fundamental triangle
|
||||||
|
`x <= y <= floor((W - 1) / 2)`. Reflecting a cell toward the left edge,
|
||||||
|
swapping its coordinates if necessary, and reflecting toward the top edge
|
||||||
|
maps every D4 orbit into this triangle. Closed diagonal and midline
|
||||||
|
boundaries retain the smaller orbits of symmetric cells.
|
||||||
|
|
||||||
|
The check is made only when the skyline search is ready to place the unit
|
||||||
|
square, so it never rejects a partial state before the square's position is
|
||||||
|
decidable. The implementation remains compile-time counter-free in normal
|
||||||
|
solver calls and keeps the single-threaded deterministic search policy.
|
||||||
|
Instrumented runs count examined and rejected unit-square placements as prune
|
||||||
|
checks and hits.
|
||||||
|
|
||||||
|
Measurements used the issue #3 dirty working tree based on commit `74bde26`,
|
||||||
|
Apple Clang 21.0.0, `-O3 -DNDEBUG`, macOS arm64, one worker, one warm-up, and
|
||||||
|
five sequential measured repetitions. All results passed the benchmark's
|
||||||
|
independent cell-coverage validator and counts were stable:
|
||||||
|
|
||||||
|
| Route | D4 constraint | Median solve (range) | Nodes | Prune hits |
|
||||||
|
| --- | --- | ---: | ---: | ---: |
|
||||||
|
| Order 8 public | enabled | 0.245 s (0.244-0.248 s) | 2,931,203 | 1,329,567 |
|
||||||
|
| Order 8 public | disabled | 0.679 s (0.659-0.709 s) | 7,735,369 | 0 |
|
||||||
|
| Order 9 direct | enabled | 1.705 s (1.671-1.740 s) | 16,231,918 | 6,755,171 |
|
||||||
|
| Order 9 direct | disabled | 4.542 s (4.482-4.662 s) | 45,840,266 | 0 |
|
||||||
|
|
||||||
|
Thus the constraint reduced nodes by 62% for order 8 and 65% for direct order
|
||||||
|
9; counted median solve time fell by 64% and 62%, respectively.
|
||||||
|
|
||||||
|
A public order-10 probe with the D4 constraint, ascending policy, and a
|
||||||
|
45-second per-process bound did not complete. Public order 11 first performs
|
||||||
|
that identical order-10 core search and only then adds its inexpensive odd
|
||||||
|
border, so adding either order to routine correctness tests would duplicate
|
||||||
|
the same unresolved bottleneck. They remain useful opt-in heavyweight
|
||||||
|
benchmark targets with explicit timeouts. A direct order-11 benchmark is a
|
||||||
|
different experiment: it bypasses the public odd construction and searches
|
||||||
|
the larger core itself, so it must not be presented as public order-11
|
||||||
|
performance.
|
||||||
|
|
||||||
## Smallest-valley skyline
|
## Smallest-valley skyline
|
||||||
|
|
||||||
The solver stores one filled height per board column instead of one value per
|
The solver stores one filled height per board column instead of one value per
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ if(BUILD_TESTING)
|
|||||||
add_test(NAME solver-completion COMMAND partridge_tests solver-completion)
|
add_test(NAME solver-completion COMMAND partridge_tests solver-completion)
|
||||||
add_test(NAME search-counters COMMAND partridge_tests search-counters)
|
add_test(NAME search-counters COMMAND partridge_tests search-counters)
|
||||||
add_test(NAME skyline-search COMMAND partridge_tests skyline-search)
|
add_test(NAME skyline-search COMMAND partridge_tests skyline-search)
|
||||||
|
add_test(NAME d4-symmetry COMMAND partridge_tests d4-symmetry)
|
||||||
find_package(Python3 COMPONENTS Interpreter)
|
find_package(Python3 COMPONENTS Interpreter)
|
||||||
if(Python3_Interpreter_FOUND)
|
if(Python3_Interpreter_FOUND)
|
||||||
add_test(NAME benchmark-format
|
add_test(NAME benchmark-format
|
||||||
|
|||||||
+5
-1
@@ -18,7 +18,11 @@ check smallest-width valley selection and deterministic tie-breaking, validate
|
|||||||
an order-8 result with descending candidates, and independently validate a
|
an order-8 result with descending candidates, and independently validate a
|
||||||
direct order-9 search with ascending candidates. An
|
direct order-9 search with ascending candidates. An
|
||||||
exhaustive infeasible order also checks that all three policies visit the same
|
exhaustive infeasible order also checks that all three policies visit the same
|
||||||
search space. The direct order-9 test is deliberately separate from the
|
search space. Focused D4 tests cover all eight rotations and reflections,
|
||||||
|
even and odd midlines, diagonals, corners, and the odd-board centre. They
|
||||||
|
independently validate order-8 solutions with symmetry enabled and disabled
|
||||||
|
and check that only the enabled search records symmetry pruning. The direct
|
||||||
|
order-9 test is deliberately separate from the
|
||||||
public order-9 route, which uses even-predecessor construction. Orders 10 and
|
public order-9 route, which uses even-predecessor construction. Orders 10 and
|
||||||
11 are not routine tests because both public routes require the same long
|
11 are not routine tests because both public routes require the same long
|
||||||
direct order-10 search; the route-boundary test still checks that order 11
|
direct order-10 search; the route-boundary test still checks that order 11
|
||||||
|
|||||||
+22
-7
@@ -28,6 +28,7 @@ namespace {
|
|||||||
|
|
||||||
auto solve(std::uint64_t order, bool instrument,
|
auto solve(std::uint64_t order, bool instrument,
|
||||||
SearchPolicy policy, bool direct_search,
|
SearchPolicy policy, bool direct_search,
|
||||||
|
SymmetryBreaking symmetry,
|
||||||
SearchCounters &counters)
|
SearchCounters &counters)
|
||||||
-> TimedSolution {
|
-> TimedSolution {
|
||||||
auto const predecessor_order =
|
auto const predecessor_order =
|
||||||
@@ -36,8 +37,8 @@ namespace {
|
|||||||
auto predecessor =
|
auto predecessor =
|
||||||
instrument
|
instrument
|
||||||
? search_solution_instrumented(predecessor_order, counters,
|
? search_solution_instrumented(predecessor_order, counters,
|
||||||
policy)
|
policy, symmetry)
|
||||||
: search_solution(predecessor_order, policy);
|
: search_solution(predecessor_order, policy, symmetry);
|
||||||
auto const search_end = Clock::now();
|
auto const search_end = Clock::now();
|
||||||
|
|
||||||
auto const construction_begin = Clock::now();
|
auto const construction_begin = Clock::now();
|
||||||
@@ -93,9 +94,10 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
if (argc < 3 || argc > 5) {
|
if (argc < 3 || argc > 6) {
|
||||||
std::cerr << "usage: partridge_benchmark ORDER counters|plain "
|
std::cerr << "usage: partridge_benchmark ORDER counters|plain "
|
||||||
"[ascending|descending|best-fit] [public|direct]\n";
|
"[ascending|descending|best-fit] [public|direct] "
|
||||||
|
"[d4|none]\n";
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
auto const order = static_cast<std::uint64_t>(std::strtoull(argv[1], nullptr, 10));
|
auto const order = static_cast<std::uint64_t>(std::strtoull(argv[1], nullptr, 10));
|
||||||
@@ -117,15 +119,25 @@ int main(int argc, char **argv) {
|
|||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
auto const direct_search =
|
auto const direct_search =
|
||||||
argc == 5 && std::string_view(argv[4]) == "direct";
|
argc >= 5 && std::string_view(argv[4]) == "direct";
|
||||||
if (argc == 5 && std::string_view(argv[4]) != "public" &&
|
if (argc >= 5 && std::string_view(argv[4]) != "public" &&
|
||||||
std::string_view(argv[4]) != "direct") {
|
std::string_view(argv[4]) != "direct") {
|
||||||
std::cerr << "search route must be public or direct\n";
|
std::cerr << "search route must be public or direct\n";
|
||||||
return 2;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
SearchCounters counters;
|
SearchCounters counters;
|
||||||
auto timed = solve(order, instrument, policy, direct_search, counters);
|
auto timed =
|
||||||
|
solve(order, instrument, policy, direct_search, symmetry, counters);
|
||||||
|
|
||||||
auto const validation_begin = Clock::now();
|
auto const validation_begin = Clock::now();
|
||||||
auto const validation_ok = valid(order, timed.result);
|
auto const validation_ok = valid(order, timed.result);
|
||||||
@@ -150,6 +162,9 @@ int main(int argc, char **argv) {
|
|||||||
<< "\""
|
<< "\""
|
||||||
<< ",\"search_route\":\""
|
<< ",\"search_route\":\""
|
||||||
<< (direct_search ? "direct" : "public") << "\""
|
<< (direct_search ? "direct" : "public") << "\""
|
||||||
|
<< ",\"symmetry_breaking\":\""
|
||||||
|
<< (symmetry == SymmetryBreaking::d4_unit_square ? "d4" : "none")
|
||||||
|
<< "\""
|
||||||
<< ",\"solved\":" << boolean(!timed.result.squares().empty())
|
<< ",\"solved\":" << boolean(!timed.result.squares().empty())
|
||||||
<< ",\"valid\":" << boolean(validation_ok)
|
<< ",\"valid\":" << boolean(validation_ok)
|
||||||
<< ",\"timing_seconds\":{\"solve\":" << timed.search_seconds
|
<< ",\"timing_seconds\":{\"solve\":" << timed.search_seconds
|
||||||
|
|||||||
+19
-2
@@ -79,11 +79,20 @@ def environment(binary):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def run_once(binary, order, mode, search_policy, search_route, timeout):
|
def run_once(
|
||||||
|
binary, order, mode, search_policy, search_route, symmetry, timeout
|
||||||
|
):
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
try:
|
try:
|
||||||
process = subprocess.run(
|
process = subprocess.run(
|
||||||
[str(binary), str(order), mode, search_policy, search_route],
|
[
|
||||||
|
str(binary),
|
||||||
|
str(order),
|
||||||
|
mode,
|
||||||
|
search_policy,
|
||||||
|
search_route,
|
||||||
|
symmetry,
|
||||||
|
],
|
||||||
check=False,
|
check=False,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@@ -223,6 +232,11 @@ def main():
|
|||||||
default="ascending",
|
default="ascending",
|
||||||
)
|
)
|
||||||
parser.add_argument("--direct-search", action="store_true")
|
parser.add_argument("--direct-search", action="store_true")
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-symmetry",
|
||||||
|
action="store_true",
|
||||||
|
help="disable unique-unit-square D4 symmetry breaking",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--measure-overhead",
|
"--measure-overhead",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@@ -254,6 +268,7 @@ def main():
|
|||||||
mode,
|
mode,
|
||||||
args.search_policy,
|
args.search_policy,
|
||||||
"direct" if args.direct_search else "public",
|
"direct" if args.direct_search else "public",
|
||||||
|
"none" if args.no_symmetry else "d4",
|
||||||
args.timeout,
|
args.timeout,
|
||||||
)
|
)
|
||||||
for trial in range(args.repetitions):
|
for trial in range(args.repetitions):
|
||||||
@@ -265,6 +280,7 @@ def main():
|
|||||||
mode,
|
mode,
|
||||||
args.search_policy,
|
args.search_policy,
|
||||||
"direct" if args.direct_search else "public",
|
"direct" if args.direct_search else "public",
|
||||||
|
"none" if args.no_symmetry else "d4",
|
||||||
args.timeout,
|
args.timeout,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -293,6 +309,7 @@ def main():
|
|||||||
"per_run_timeout_seconds": args.timeout,
|
"per_run_timeout_seconds": args.timeout,
|
||||||
"candidate_order": args.search_policy,
|
"candidate_order": args.search_policy,
|
||||||
"search_route": "direct" if args.direct_search else "public",
|
"search_route": "direct" if args.direct_search else "public",
|
||||||
|
"symmetry_breaking": "none" if args.no_symmetry else "d4",
|
||||||
"stdout": "captured; rendered grid suppressed by probe",
|
"stdout": "captured; rendered grid suppressed by probe",
|
||||||
},
|
},
|
||||||
"cases": cases,
|
"cases": cases,
|
||||||
|
|||||||
@@ -162,6 +162,29 @@ namespace {
|
|||||||
best_fit,
|
best_fit,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Whether to remove equivalent board orientations from the search. */
|
||||||
|
enum class SymmetryBreaking {
|
||||||
|
disabled,
|
||||||
|
d4_unit_square,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Return whether a cell is the canonical representative of its D4 orbit.
|
||||||
|
*
|
||||||
|
* Reflect a cell into the left half of the board, rotate so its distance
|
||||||
|
* from the left edge is no greater than its distance from the top edge, and
|
||||||
|
* finally reflect it into the top half. The resulting fundamental region is
|
||||||
|
* the closed triangle x <= y <= floor((length - 1) / 2). Closed boundaries
|
||||||
|
* retain representatives whose orbit is smaller than eight.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] auto canonical_unit_position(size_t const x,
|
||||||
|
size_t const y,
|
||||||
|
size_t const length) noexcept
|
||||||
|
-> bool {
|
||||||
|
assert(x < length);
|
||||||
|
assert(y < length);
|
||||||
|
return x <= y && y <= (length - 1) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
/** A maximal level skyline segment which is lower than its neighbours. */
|
/** A maximal level skyline segment which is lower than its neighbours. */
|
||||||
struct Valley {
|
struct Valley {
|
||||||
size_t x;
|
size_t x;
|
||||||
@@ -200,7 +223,7 @@ namespace {
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<bool Instrument>
|
template<bool Instrument, bool BreakD4Symmetry>
|
||||||
auto search_skyline(size_t const n, size_t const length,
|
auto search_skyline(size_t const n, size_t const length,
|
||||||
SearchPolicy const policy,
|
SearchPolicy const policy,
|
||||||
std::vector<size_t> &skyline, Avail &available,
|
std::vector<size_t> &skyline, Avail &available,
|
||||||
@@ -226,6 +249,20 @@ namespace {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if constexpr (BreakD4Symmetry) {
|
||||||
|
if (side == 1) {
|
||||||
|
if constexpr (Instrument) {
|
||||||
|
++counters->prune_checks;
|
||||||
|
}
|
||||||
|
if (!canonical_unit_position(valley.x, valley.height, length)) {
|
||||||
|
if constexpr (Instrument) {
|
||||||
|
++counters->prune_hits;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if constexpr (Instrument) {
|
if constexpr (Instrument) {
|
||||||
++counters->attempted_placements;
|
++counters->attempted_placements;
|
||||||
}
|
}
|
||||||
@@ -234,8 +271,8 @@ namespace {
|
|||||||
side, valley.height + side);
|
side, valley.height + side);
|
||||||
squares.emplace_back(valley.x + valley.height * length, side);
|
squares.emplace_back(valley.x + valley.height * length, side);
|
||||||
|
|
||||||
if (search_skyline<Instrument>(n, length, policy, skyline,
|
if (search_skyline<Instrument, BreakD4Symmetry>(
|
||||||
available, squares, counters)) {
|
n, length, policy, skyline, available, squares, counters)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +318,7 @@ namespace {
|
|||||||
* candidates and updating up to n columns for each costs O(n^2), for
|
* candidates and updating up to n columns for each costs O(n^2), for
|
||||||
* O(board width + n^2) local work per node and the same total state.
|
* O(board width + n^2) local work per node and the same total state.
|
||||||
*/
|
*/
|
||||||
template<bool Instrument>
|
template<bool Instrument, bool BreakD4Symmetry>
|
||||||
auto search_solution_impl(size_t const n, SearchPolicy const policy,
|
auto search_solution_impl(size_t const n, SearchPolicy const policy,
|
||||||
SearchCounters *const counters) noexcept
|
SearchCounters *const counters) noexcept
|
||||||
-> Results {
|
-> Results {
|
||||||
@@ -293,7 +330,7 @@ namespace {
|
|||||||
}
|
}
|
||||||
std::vector<Square> squares;
|
std::vector<Square> squares;
|
||||||
squares.reserve(length);
|
squares.reserve(length);
|
||||||
static_cast<void>(search_skyline<Instrument>(
|
static_cast<void>(search_skyline<Instrument, BreakD4Symmetry>(
|
||||||
n, length, policy, skyline, available, squares, counters));
|
n, length, policy, skyline, available, squares, counters));
|
||||||
|
|
||||||
return {length, std::move(squares)};
|
return {length, std::move(squares)};
|
||||||
@@ -301,18 +338,28 @@ namespace {
|
|||||||
|
|
||||||
auto search_solution(
|
auto search_solution(
|
||||||
size_t const n,
|
size_t const n,
|
||||||
SearchPolicy const policy = SearchPolicy::ascending) noexcept
|
SearchPolicy const policy = SearchPolicy::ascending,
|
||||||
|
SymmetryBreaking const symmetry =
|
||||||
|
SymmetryBreaking::d4_unit_square) noexcept
|
||||||
-> Results {
|
-> Results {
|
||||||
return search_solution_impl<false>(n, policy, nullptr);
|
if (symmetry == SymmetryBreaking::d4_unit_square) {
|
||||||
|
return search_solution_impl<false, true>(n, policy, nullptr);
|
||||||
|
}
|
||||||
|
return search_solution_impl<false, false>(n, policy, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto search_solution_instrumented(size_t const n,
|
auto search_solution_instrumented(size_t const n,
|
||||||
SearchCounters &counters,
|
SearchCounters &counters,
|
||||||
SearchPolicy const policy =
|
SearchPolicy const policy =
|
||||||
SearchPolicy::ascending) noexcept
|
SearchPolicy::ascending,
|
||||||
|
SymmetryBreaking const symmetry =
|
||||||
|
SymmetryBreaking::d4_unit_square) noexcept
|
||||||
-> Results {
|
-> Results {
|
||||||
counters = {};
|
counters = {};
|
||||||
return search_solution_impl<true>(n, policy, &counters);
|
if (symmetry == SymmetryBreaking::d4_unit_square) {
|
||||||
|
return search_solution_impl<true, true>(n, policy, &counters);
|
||||||
|
}
|
||||||
|
return search_solution_impl<true, false>(n, policy, &counters);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Construct an odd-order solution from its even-order predecessor. */
|
/** Construct an odd-order solution from its even-order predecessor. */
|
||||||
@@ -347,23 +394,29 @@ namespace {
|
|||||||
|
|
||||||
auto find_solution(
|
auto find_solution(
|
||||||
size_t const n,
|
size_t const n,
|
||||||
SearchPolicy const policy = SearchPolicy::ascending) noexcept -> Results {
|
SearchPolicy const policy = SearchPolicy::ascending,
|
||||||
|
SymmetryBreaking const symmetry =
|
||||||
|
SymmetryBreaking::d4_unit_square) noexcept -> Results {
|
||||||
if (uses_odd_construction(n)) {
|
if (uses_odd_construction(n)) {
|
||||||
return construct_odd_solution(n, search_solution(n - 1, policy));
|
return construct_odd_solution(
|
||||||
|
n, search_solution(n - 1, policy, symmetry));
|
||||||
}
|
}
|
||||||
return search_solution(n, policy);
|
return search_solution(n, policy, symmetry);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto find_solution_instrumented(size_t const n,
|
auto find_solution_instrumented(size_t const n,
|
||||||
SearchCounters &counters,
|
SearchCounters &counters,
|
||||||
SearchPolicy const policy =
|
SearchPolicy const policy =
|
||||||
SearchPolicy::ascending) noexcept
|
SearchPolicy::ascending,
|
||||||
|
SymmetryBreaking const symmetry =
|
||||||
|
SymmetryBreaking::d4_unit_square) noexcept
|
||||||
-> Results {
|
-> Results {
|
||||||
if (uses_odd_construction(n)) {
|
if (uses_odd_construction(n)) {
|
||||||
return construct_odd_solution(
|
return construct_odd_solution(
|
||||||
n, search_solution_instrumented(n - 1, counters, policy));
|
n, search_solution_instrumented(
|
||||||
|
n - 1, counters, policy, symmetry));
|
||||||
}
|
}
|
||||||
return search_solution_instrumented(n, counters, policy);
|
return search_solution_instrumented(n, counters, policy, symmetry);
|
||||||
}
|
}
|
||||||
} // anon namespace
|
} // anon namespace
|
||||||
|
|
||||||
|
|||||||
+122
-3
@@ -166,6 +166,35 @@ namespace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto d4_orbit(Placement const &placement, std::uint64_t const length)
|
||||||
|
-> std::array<Placement, 8> {
|
||||||
|
auto const far_x = length - 1 - placement.x;
|
||||||
|
auto const far_y = length - 1 - placement.y;
|
||||||
|
return {{
|
||||||
|
{placement.x, placement.y, placement.side},
|
||||||
|
{far_y, placement.x, placement.side},
|
||||||
|
{far_x, far_y, placement.side},
|
||||||
|
{placement.y, far_x, placement.side},
|
||||||
|
{far_x, placement.y, placement.side},
|
||||||
|
{far_y, far_x, placement.side},
|
||||||
|
{placement.x, far_y, placement.side},
|
||||||
|
{placement.y, placement.x, placement.side},
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto canonical_members(Placement const &placement, std::uint64_t const length)
|
||||||
|
-> std::vector<Placement> {
|
||||||
|
auto orbit = d4_orbit(placement, length);
|
||||||
|
std::vector<Placement> members;
|
||||||
|
for (auto const &member: orbit) {
|
||||||
|
if (canonical_unit_position(member.x, member.y, length) &&
|
||||||
|
std::ranges::find(members, member) == members.end()) {
|
||||||
|
members.push_back(member);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|
||||||
auto test_validator() -> int {
|
auto test_validator() -> int {
|
||||||
int failures = 0;
|
int failures = 0;
|
||||||
auto const valid = known_order_8();
|
auto const valid = known_order_8();
|
||||||
@@ -337,11 +366,98 @@ namespace {
|
|||||||
failures += expect(counters.attempted_placements > 0 &&
|
failures += expect(counters.attempted_placements > 0 &&
|
||||||
counters.backtracks > 0,
|
counters.backtracks > 0,
|
||||||
"instrumented solver did not count placements/backtracks");
|
"instrumented solver did not count placements/backtracks");
|
||||||
failures += expect(counters.prune_checks == 0 &&
|
failures += expect(counters.prune_checks >= counters.prune_hits &&
|
||||||
counters.prune_hits == 0 &&
|
|
||||||
counters.generated_tasks == 0 &&
|
counters.generated_tasks == 0 &&
|
||||||
counters.completed_tasks == 0,
|
counters.completed_tasks == 0,
|
||||||
"unimplemented solver counters were not zero");
|
"search counters are inconsistent");
|
||||||
|
return failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto test_d4_symmetry() -> int {
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
// A generic cell has eight distinct images: identity, three rotations,
|
||||||
|
// and four reflected rotations. Exactly one must survive.
|
||||||
|
auto const generic = d4_orbit({5, 9, 1}, 36);
|
||||||
|
auto distinct_generic = std::vector<Placement>(generic.begin(),
|
||||||
|
generic.end());
|
||||||
|
std::ranges::sort(
|
||||||
|
distinct_generic, {}, [](Placement const &placement) {
|
||||||
|
return std::array{placement.x, placement.y};
|
||||||
|
});
|
||||||
|
distinct_generic.erase(
|
||||||
|
std::ranges::unique(distinct_generic).begin(),
|
||||||
|
distinct_generic.end());
|
||||||
|
failures += expect(distinct_generic.size() == generic.size(),
|
||||||
|
"generic D4 test point does not have eight images");
|
||||||
|
for (std::size_t index = 0; index < generic.size(); ++index) {
|
||||||
|
failures += expect(
|
||||||
|
canonical_members(generic[index], 36) ==
|
||||||
|
std::vector<Placement>{{5, 9, 1}},
|
||||||
|
"D4 transform " + std::to_string(index) +
|
||||||
|
" did not select the same canonical representative");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closed boundaries are important because diagonal, midline, corner, and
|
||||||
|
// centre cells have non-trivial stabilizers and therefore smaller orbits.
|
||||||
|
for (auto const test_case:
|
||||||
|
std::array<std::pair<std::uint64_t, Placement>, 8>{{
|
||||||
|
{36, {0, 0, 1}},
|
||||||
|
{36, {0, 17, 1}},
|
||||||
|
{36, {7, 7, 1}},
|
||||||
|
{36, {7, 17, 1}},
|
||||||
|
{45, {0, 22, 1}},
|
||||||
|
{45, {11, 11, 1}},
|
||||||
|
{45, {11, 22, 1}},
|
||||||
|
{45, {22, 22, 1}},
|
||||||
|
}}) {
|
||||||
|
failures += expect(
|
||||||
|
canonical_members(test_case.second, test_case.first).size() == 1,
|
||||||
|
"boundary orbit did not retain exactly one distinct representative");
|
||||||
|
}
|
||||||
|
failures += expect(
|
||||||
|
canonical_unit_position(7, 17, 36) &&
|
||||||
|
!canonical_unit_position(8, 7, 36) &&
|
||||||
|
!canonical_unit_position(7, 18, 36) &&
|
||||||
|
canonical_unit_position(22, 22, 45) &&
|
||||||
|
!canonical_unit_position(22, 23, 45),
|
||||||
|
"canonical triangle mishandled a diagonal or midline boundary");
|
||||||
|
|
||||||
|
SearchCounters enabled_counters;
|
||||||
|
SearchCounters disabled_counters;
|
||||||
|
auto const enabled = search_solution_instrumented(
|
||||||
|
8, enabled_counters, SearchPolicy::ascending,
|
||||||
|
SymmetryBreaking::d4_unit_square);
|
||||||
|
auto const disabled = search_solution_instrumented(
|
||||||
|
8, disabled_counters, SearchPolicy::ascending,
|
||||||
|
SymmetryBreaking::disabled);
|
||||||
|
auto enabled_validation = validate(8, enabled);
|
||||||
|
auto disabled_validation = validate(8, disabled);
|
||||||
|
failures += expect(
|
||||||
|
enabled_validation.valid(),
|
||||||
|
"symmetry-enabled solution failed independent validation:\n" +
|
||||||
|
enabled_validation.text());
|
||||||
|
failures += expect(
|
||||||
|
disabled_validation.valid(),
|
||||||
|
"symmetry-disabled solution failed independent validation:\n" +
|
||||||
|
disabled_validation.text());
|
||||||
|
|
||||||
|
auto const unit = std::ranges::find_if(
|
||||||
|
enabled.squares(),
|
||||||
|
[](Square const &square) { return square.length() == 1; });
|
||||||
|
failures += expect(
|
||||||
|
unit != enabled.squares().end() &&
|
||||||
|
canonical_unit_position(unit->pos() % enabled.length(),
|
||||||
|
unit->pos() / enabled.length(),
|
||||||
|
enabled.length()),
|
||||||
|
"symmetry-enabled search placed the unique unit square outside the "
|
||||||
|
"canonical region");
|
||||||
|
failures += expect(
|
||||||
|
enabled_counters.prune_checks > 0 && enabled_counters.prune_hits > 0 &&
|
||||||
|
disabled_counters.prune_checks == 0 &&
|
||||||
|
disabled_counters.prune_hits == 0,
|
||||||
|
"symmetry instrumentation did not distinguish enabled and disabled "
|
||||||
|
"search");
|
||||||
return failures;
|
return failures;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,6 +556,9 @@ int main(int argc, char **argv) {
|
|||||||
if (test == "skyline-search") {
|
if (test == "skyline-search") {
|
||||||
return test_skyline_search();
|
return test_skyline_search();
|
||||||
}
|
}
|
||||||
|
if (test == "d4-symmetry") {
|
||||||
|
return test_d4_symmetry();
|
||||||
|
}
|
||||||
std::cerr << "unknown test: " << test << '\n';
|
std::cerr << "unknown test: " << test << '\n';
|
||||||
return 2;
|
return 2;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user