From bed2e6701e410099a594ac0a1e3d623bff746871 Mon Sep 17 00:00:00 2001 From: Sichao25 Date: Tue, 13 Oct 2026 17:21:39 -0400 Subject: [PATCH] implement GetCandidateRanks for RCBPtn --- CMakeLists.txt | 11 +++ redev.cpp | 94 +++++++++++++++++++++++ redev_partition.h | 54 +++++++++++++ test_boundaryRouting.cpp | 162 +++++++++++++++++++++++++++++++++++++++ test_candidateRanks.cpp | 160 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 481 insertions(+) create mode 100644 test_boundaryRouting.cpp create mode 100644 test_candidateRanks.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b2cdf53..c6f42c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,6 +158,17 @@ if(BUILD_TESTING) NAME2 app PROCS2 1 EXE2 ./test_setup_classPtn ARGS2 0) add_exe(test_query test_query.cpp) mpi_test(test_query_1p 1 ./test_query) + # Unit tests for RCBPtn::GetCandidateRanks. + add_exe(test_candidateRanks test_candidateRanks.cpp) + mpi_test(test_candidateRanks_1p 1 ./test_candidateRanks) + # A standalone test of the boundary routing logic. Routing uses the + # multi-candidate API (RCBPtn::GetCandidateRanks), which resolves boundary + # ambiguity within the rendezvous partition; this test still FAILS on rows + # whose true mesh owner is in a different rendezvous region (misalignment + # beyond adjacency). WILL_FAIL keeps CI green while the known gap exists. + add_exe(test_boundaryRouting test_boundaryRouting.cpp) + mpi_test(test_boundaryRouting_1p 1 ./test_boundaryRouting) + set_tests_properties(test_boundaryRouting_1p PROPERTIES WILL_FAIL TRUE) add_exe(test_send test_send.cpp) dual_mpi_test(TESTNAME test_send_3p TIMEOUT ${test_timeout} NAME1 rdv PROCS1 4 EXE1 ./test_send ARGS1 1 diff --git a/redev.cpp b/redev.cpp index a59481a..18951cd 100644 --- a/redev.cpp +++ b/redev.cpp @@ -9,6 +9,8 @@ #include // std::chrono::seconds #include // std::stoi #include // std::find_if +#include // std::abs, std::log2, std::pow +#include // std::numeric_limits namespace { //Wait for the file to be created by the writer. @@ -236,6 +238,98 @@ namespace redev { return cuts; } + std::array, 3> RCBPtn::BuildDimCuts() const { + const auto len = static_cast(cuts.size()); + std::array, 3> dc; + for(redev::LO i=1; i(std::log2(i)); + dc[level % dim].push_back(cuts[i]); + } + for(auto& list : dc) { + std::sort(list.begin(), list.end()); + list.erase(std::unique(list.begin(), list.end()), list.end()); + } + return dc; + } + + redev::Real RCBPtn::LocalSpacing( + const std::array, 3>& dimCuts, + redev::LO d, redev::Real v, redev::Real fallback) { + const auto& list = dimCuts[d]; + const auto it = std::lower_bound(list.begin(), list.end(), v); + if(it == list.end()) return fallback; + if(it != list.begin() && it + 1 != list.end()) + return std::min(v - *(it-1), *(it+1) - v); + if(it != list.begin()) return v - *(it-1); + if(it + 1 != list.end()) return *(it+1) - v; + return fallback; + } + + redev::Real RCBPtn::NodeTolerance( + const std::array, 3>& dimCuts, + redev::LO d, redev::Real v, redev::Real ptCoord, + redev::Real tolFactor) { + // hLocal = 0 when dim d has fewer than 2 distinct cuts: there are no + // adjacent sub-domains to broaden to on this axis, so the tolerance + // collapses to the machine-epsilon floor. + const auto hLocal = LocalSpacing(dimCuts, d, v, /*fallback=*/0); + const auto epsGuard = + defaultEpsFactor * std::numeric_limits::epsilon() * + std::max({std::abs(ptCoord), std::abs(v), redev::Real(1)}); + return std::max(tolFactor * hLocal, epsGuard); + } + + redev::LOs RCBPtn::TraverseCandidates( + std::array& pt, redev::Real tolFactor, + const std::array, 3>& dimCuts) const { + REDEV_FUNCTION_TIMER; + assert(ranks.size() && cuts.size()); + assert(dim>0 && dim<=3); + const auto levels = static_cast(std::log2(cuts.size())); + const auto leafStart = static_cast(std::pow(2, levels)); + // Standard multi-path traversal of the RCB cut tree + struct Frame { redev::LO node; redev::LO d; }; + std::vector stack; + stack.reserve(static_cast(levels) * 2); + redev::LOs cands; + stack.push_back({1, 0}); + while(!stack.empty()) { + const auto frame = stack.back(); + stack.pop_back(); + if(frame.node >= leafStart) { + const auto rankIdx = frame.node - leafStart; + assert(rankIdx >= 0 && rankIdx < static_cast(ranks.size())); + cands.push_back(ranks.at(rankIdx)); + continue; + } + const auto v = cuts.at(frame.node); + const auto dNext = static_cast((frame.d + 1) % dim); + const auto goLeft = pt[frame.d] < v; + const auto tol = NodeTolerance(dimCuts, frame.d, v, pt[frame.d], + tolFactor); + const auto nearPlane = std::abs(pt[frame.d] - v) <= tol; + stack.push_back({goLeft ? frame.node*2 : frame.node*2+1, dNext}); + if(nearPlane) + stack.push_back({goLeft ? frame.node*2+1 : frame.node*2, dNext}); + } + std::sort(cands.begin(), cands.end()); + cands.erase(std::unique(cands.begin(), cands.end()), cands.end()); + return cands; + } + + redev::LOs RCBPtn::GetCandidateRanks(std::array& pt) const { + REDEV_FUNCTION_TIMER; + const auto dimCuts = BuildDimCuts(); + return TraverseCandidates(pt, defaultTolFactor, dimCuts); + } + + redev::LOs RCBPtn::GetCandidateRanks(std::array& pt, + redev::Real tolFactor) const { + REDEV_FUNCTION_TIMER; + const auto dimCuts = BuildDimCuts(); + return TraverseCandidates(pt, tolFactor, dimCuts); + } + void RCBPtn::Write(adios2::Engine& eng, adios2::IO& io) { REDEV_FUNCTION_TIMER; const auto len = ranks.size(); diff --git a/redev_partition.h b/redev_partition.h index 20ba071..f7a4681 100644 --- a/redev_partition.h +++ b/redev_partition.h @@ -175,8 +175,62 @@ class RCBPtn { * are stored in the vector as (x0,y0(,z0),x1,y1(,z1),...xN-1,yN-1(,zN-1)) */ [[nodiscard]] std::vector GetCuts() const; + /** + * Return the default tolerance factor applied to the LOCAL sub-domain scale + * to form the default search tolerance of GetCandidateRanks(pt) (no + * argument). At each cut plane the local scale is the distance to the + * nearest other cut plane in the same dimension (zero when the dimension has + * only one cut); the tolerance is + * `tol = max(GetDefaultTolFactor() * localScale, machinePrecisionGuard)`, + * where machinePrecisionGuard keeps the tolerance above a few machine epsilons + * times the coordinate scale so floating-point representation error can + * never drive it to zero. + */ + [[nodiscard]] static constexpr redev::Real GetDefaultTolFactor() noexcept { + return defaultTolFactor; + } + /** + * Return the ranks of the sub-domains that could own the given point under a + * (possibly different) boundary convention. The returned set is complete: every + * sub-domain whose closure is within tol of the point on every axis is + * included. For points strictly interior to the owning sub-domain the + * returned vector is {GetRank(pt)}; on-boundary (and, with tol, near- + * boundary) points return 2..2^dim candidates (the returned vector is sorted + * and unique). + * The overload without arguments uses the default tolerance factor + * (GetDefaultTolFactor); pass tolFactor explicitly to override, + * e.g. `tolFactor = 0.0` for exact cut-plane detection only. + * @param[in] pt the cartesian point in space. The 3rd value is ignored for 2d + * domains. + * @param[in] tolFactor factor multiplied by the local sub-domain scale to + * form the search tolerance, clamped above a machine-epsilon guard + */ + [[nodiscard]] redev::LOs GetCandidateRanks(std::array& pt) const; + [[nodiscard]] redev::LOs GetCandidateRanks(std::array& pt, + redev::Real tolFactor) const; private: + // Fraction of the local sub-domain scale used by the default tolerance. + static constexpr redev::Real defaultTolFactor = 1e-3; + // Machine-precision guard multiplier: floor tolerance at + // defaultEpsFactor * epsilon * max(|coord|, |v|, 1). + static constexpr redev::Real defaultEpsFactor = 64; + + // Build per-dimension sorted-unique cut value arrays. + std::array, 3> BuildDimCuts() const; + // Distance from v to the nearest other cut in the same dimension. + static redev::Real LocalSpacing( + const std::array, 3>& dimCuts, + redev::LO d, redev::Real v, redev::Real fallback); + // Tolerance at node (d,v): max(tolFactor * localSpacing(d,v), epsGuard). + static redev::Real NodeTolerance( + const std::array, 3>& dimCuts, + redev::LO d, redev::Real v, redev::Real ptCoord, + redev::Real tolFactor); + // Fixed-radius multi-path traversal of the cut tree. + redev::LOs TraverseCandidates(std::array& pt, + redev::Real tolFactor, + const std::array, 3>& dimCuts) const; const std::string ranksVarName = "rcb partition ranks"; const std::string cutsVarName = "rcb partition cuts"; const std::string dimVarName = "rcb partition dim"; diff --git a/test_boundaryRouting.cpp b/test_boundaryRouting.cpp new file mode 100644 index 0000000..bba0eff --- /dev/null +++ b/test_boundaryRouting.cpp @@ -0,0 +1,162 @@ +#include // std::count +#include // std::array +#include // std::nextafter +#include // EXIT_FAILURE / EXIT_SUCCESS +#include +#include "redev.h" + +/** + * Distributed point-search routing contract test. + * + * The scenario + * ============ + * The rendezvous app owns the REDEZVOUS partition used to route query points to + * ranks (handcoded here as a 2D RCBPtn over [0,1]^2). The receiving + * application owns the TRUE MESH partition (handcoded 2x2 blocks) whose cut + * positions (x=0.49, y=0.76) are deliberately OFFSET from the rendezvous cuts + * (x=0.5 and y=0.75/0.25) to model a realistic mesh whose blocks do not + * coincide with the rendezvous grid. + * + * Expected result (as of writing) + * =============================== + * - Part 1 (rendezvous determinism): PASSES. redev's answers are + * deterministic and follow the documented half-open convention + * (pt[d] < cut -> left, pt[d] >= cut -> right). + * - Part 2 (candidate routing contract) : FAILS on 4 of 14 rows. The + * destination set is now formed with RCBPtn::GetCandidateRanks (send-to-all- + * candidates routing). Rows that still FAIL are exactly those where the + * true mesh owner's rendezvous sub-domain does NOT share a boundary with + * the rendezvous cell containing the point - i.e. rendezvous/mesh + * misalignment beyond the rendezvous adjacency structure. This is the + * intended signal: multi-candidate routing resolves boundary AMBIGUITY + * within the rendezvous partition but cannot heal MISALIGNMENT between the + * rendezvous and mesh partitions. + * + * Registered in CMakeLists.txt with WILL_FAIL TRUE so CI stays green while the + * known gap exists. Remove WILL_FAIL (and expect this to pass) once the + * misaligned case is addressed, e.g. via cell->rank-overlap routing or + * rendezvous partitions aligned with the mesh. + */ +namespace { + /** + * Handcoded TRUE mesh partition: 2x2 axis-aligned blocks over [0,1]^2. + * Cut positions (x=0.49, y=0.76) are deliberately OFFSET from the rendezvous + * cuts. Mesh convention: half-open blocks, x < 0.49 -> left column, + * y < 0.76 -> bottom row. rank = 2*col + row. + */ + struct MeshOwnership { + static redev::LO GetRank(const std::array& pt) { + redev::LO col = (pt[0] < 0.49) ? 0 : 1; + redev::LO row = (pt[1] < 0.76) ? 0 : 1; + return static_cast(2 * col + row); + } + }; + + /** One handcoded query point and its hand-verified expected answers. */ + struct Query { + const std::array pt; + const char* desc; + /** Hand-verified singular rendezvous answer (rdzv convention). */ + redev::LO rdzvExpected; + /** Hand-verified true mesh owner (mesh convention). */ + redev::LO meshExpected; + }; + + // 2D rendezvous partition (RCBPtn): regions are + // rank0=[0,.5)x[0,.75) rank1=[0,.5)x[.75,1] + // rank2=[.5,1]x[0,.25) rank3=[.5,1]x[.25,1] + // 2D mesh partition: regions are (xcut=0.49, ycut=0.76) + // mesh0=[0,.49)x[0,.76) mesh1=[0,.49)x[.76,1] + // mesh2=[.49,1]x[0,.76) mesh3=[.49,1]x[.76,1] + const std::vector queries = { + // --- Interior points (Part 2 must PASS today) --- + {{0.1, 0.1, 0.0}, "interior (both partitions)", 0, 0}, + {{0.3, 0.3, 0.0}, "interior (both partitions)", 0, 0}, + {{0.4, 0.8, 0.0}, "interior (both partitions)", 1, 1}, + {{0.6, 0.1, 0.0}, "interior (both partitions)", 2, 2}, + {{0.8, 0.9, 0.0}, "interior (both partitions)", 3, 3}, + {{0.48, 0.3, 0.0}, "just inside both (left of mesh-cut 0.49)", 0, 0}, + // --- Exactly on a rendezvous cut/corner (EXPECTED to FAIL) --- + {{0.5, 0.5, 0.0}, "EXACTLY on rdzv x-cut (0.5)", 3, 2}, + {{0.25, 0.75, 0.0}, "EXACTLY on rdzv y-cut (0.75)", 1, 0}, + {{0.75, 0.25, 0.0}, "EXACTLY on rdzv y-cut (0.25)", 3, 2}, + {{0.5, 0.25, 0.0}, "EXACTLY on rdzv 4-region corner", 3, 2}, + {{0.5, 0.75, 0.0}, "on rdzv x-cut AND y-cut", 3, 2}, + // --- FP-slop / misaligned-strip probes (EXPECTED to FAIL) --- + {{0.495, 0.5, 0.0}, "rdzv-interior BUT in stagger strip (0.49,0.5)", 0, 2}, + {{std::nextafter(0.5, 0.0), 0.5, 0.0}, + "one ulp below rdzv x-cut (FP slop)", 0, 2}, + // --- Educational: on a rdzv cut yet coincidentally consistent --- + {{0.5, 0.9, 0.0}, "on rdzv x-cut but mesh row agrees", 3, 3}, + }; +} // namespace +int main(int argc, char** argv) { + MPI_Init(&argc, &argv); + { + // Handcoded rendezvous partition. cuts: slot 0 unused, then BFS order + // (root x=0.5, then y=0.75 at node 2, y=0.25 at node 3). + const redev::LO dim = 2; + redev::LOs ranks = {0, 1, 2, 3}; + redev::Reals cuts = {0, 0.5, 0.75, 0.25}; + redev::RCBPtn rdzvPtn(dim, ranks, cuts); + redev::Redev rdv(MPI_COMM_WORLD, redev::Partition{rdzvPtn}, + redev::ProcessType::Server, /*noClients=*/true); + + std::cout << "test_boundaryRouting: handcoded rendezvous (RCBPtn) vs " + "handcoded mesh (offset 2x2 blocks)\n" + "row format: PASS/FAIL candidates={dest ranks} meshOwner=# " + "description\n\n"; + + // ---------------- Part 1: rendezvous determinism ---------------- + // redev's GetRank must be deterministic and identical whether queried + // directly or through the partition stored inside Redev. + for (const auto& q : queries) { + std::array pt = q.pt; + const auto destLocal = rdzvPtn.GetRank(pt); + const auto destRedev = + std::get(rdv.GetPartition()).GetRank(pt); + REDEV_ALWAYS_ASSERT(destLocal == q.rdzvExpected); + REDEV_ALWAYS_ASSERT(destRedev == destLocal); + } + std::cout << "Part 1 (rendezvous determinism + half-open convention): " + "PASSED\n\n"; + + // ---------------- Part 2: candidate routing contract ---------------- + // Contract: the destination-rank candidate set returned by + // GetCandidateRanks must contain the TRUE mesh owner. + std::size_t numUnrouted = 0; + for (const auto& q : queries) { + std::array pt = q.pt; + auto dest = std::get(rdv.GetPartition()) + .GetCandidateRanks(pt, /*tolFactor=*/0.0); + const auto ownerMesh = MeshOwnership::GetRank(pt); + REDEV_ALWAYS_ASSERT(std::count(dest.begin(), dest.end(), q.rdzvExpected) != 0); + REDEV_ALWAYS_ASSERT(ownerMesh == q.meshExpected); + const bool routed = std::count(dest.begin(), dest.end(), ownerMesh) != 0; + if (!routed) ++numUnrouted; + std::cout << (routed ? "PASS" : "FAIL") << " candidates={"; + for (std::size_t i = 0; i < dest.size(); i++) + std::cout << (i ? "," : "") << dest[i]; + std::cout << "} meshOwner=" << ownerMesh << " " << q.desc << "\n"; + } + + if (numUnrouted) { + std::cerr + << "\nPart 2 (candidate routing contract): " << numUnrouted << " of " + << queries.size() + << " query points have a TRUE mesh owner that is NOT in the candidate\n" + << "set returned by GetCandidateRanks. That set contains only the\n" + << "rendezvous sub-domains sharing a boundary with the point, so these\n" + << "failures are rows where the mesh owner lies in a different\n" + << "rendezvous region (misalignment beyond adjacency). This is the\n" + << "intended signal: multi-candidate routing resolves boundary\n" + << "AMBIGUITY within the rendezvous partition but cannot heal\n" + << "rendezvous/mesh MISALIGNMENT.\n"; + MPI_Finalize(); + return EXIT_FAILURE; + } + std::cout << "Part 2 (candidate routing contract) PASSED\n"; + } + MPI_Finalize(); + return EXIT_SUCCESS; +} \ No newline at end of file diff --git a/test_candidateRanks.cpp b/test_candidateRanks.cpp new file mode 100644 index 0000000..9d54db0 --- /dev/null +++ b/test_candidateRanks.cpp @@ -0,0 +1,160 @@ +#include // std::sort +#include // std::array +#include +#include "redev.h" + +/** + * Unit tests for RCBPtn::GetCandidateRanks() and its local-tolerance factor. + * + * Handcoded partitions: + * 2D: dim=2, ranks={0,1,2,3}, cuts={0, 0.5, 0.75, 0.25} + * rank0={x<0.5,y<0.75} rank1={x<0.5,y>=0.75} + * rank2={x>=0.5,y<0.25} rank3={x>=0.5,y>=0.25} + * 1D: dim=1, ranks={0,1,2,3}, cuts={0, 0.5, 0.25, 0.75} + * rank0={x<0.25} rank1=[0.25,0.5) rank2=[0.5,0.75) rank3={x>=0.75} + * + * The explicit-factor API is GetCandidateRanks(pt, tolFactor), where + * tol at node (d,v) = max(tolFactor * localSpacing(d,v), epsGuard). + * tolFactor=0 -> exact cut-plane detection only (epsGuard floors tol). + */ +namespace { + redev::LOs Set(std::initializer_list v) { + redev::LOs s(v); + std::sort(s.begin(), s.end()); + return s; + } + struct Probe { + std::array pt; + redev::Real tolFactor; // 0 = exact cuts only + redev::LOs expected; // sorted + const char* desc; + }; + void checkCandidates(const redev::RCBPtn& ptn, const char* which, + const std::vector& probes) { + for (const auto& p : probes) { + auto pt = p.pt; + auto actual = ptn.GetCandidateRanks(pt, p.tolFactor); + std::sort(actual.begin(), actual.end()); + const bool ok = (actual == p.expected); + std::cout << (ok ? "PASS" : "FAIL") << " candidates={"; + for (std::size_t i = 0; i < actual.size(); i++) + std::cout << (i ? "," : "") << actual[i]; + std::cout << "} expected={"; + for (std::size_t i = 0; i < p.expected.size(); i++) + std::cout << (i ? "," : "") << p.expected[i]; + std::cout << "} " << p.desc << "\n"; + REDEV_ALWAYS_ASSERT(ok); + } + std::cout << which << ": " << probes.size() << " candidate probes done\n"; + } +} // namespace + +int main(int argc, char** argv) { + MPI_Init(&argc, &argv); + { + { // 1D partition + const auto dim = 1; + redev::LOs ranks = {0, 1, 2, 3}; + redev::Reals cuts = {0, 0.5, 0.25, 0.75}; + redev::RCBPtn ptn(dim, ranks, cuts); + const std::vector probes = { + // tolFactor=0 -> exact cut-plane detection (epsGuard floors tol) + {{0.1, 0.0, 0.0}, 0.0, Set({0}), "interior rank0"}, + {{0.25, 0.0, 0.0}, 0.0, Set({0, 1}), "exactly on node2 cut"}, + {{0.49, 0.0, 0.0}, 0.0, Set({1}), "interior rank1"}, + {{0.5, 0.0, 0.0}, 0.0, Set({1, 2}), "exactly on root cut"}, + {{0.75, 0.0, 0.0}, 0.0, Set({2, 3}), "exactly on node3 cut"}, + {{0.9, 0.0, 0.0}, 0.0, Set({3}), "interior rank3"}, + // tolFactor=0.04 on uniform partition -> effective tol=0.01 + {{0.5, 0.0, 0.0}, 0.04, Set({1, 2}), + "small factor keeps exact-cut set"}, + {{0.5, 0.0, 0.0}, 1.04, Set({0, 1, 2, 3}), + "large factor adds all sub-domains within tol"}, + {{0.495, 0.0, 0.0}, 0.04, Set({1, 2}), + "tolFactor captures approach to root cut"}, + }; + checkCandidates(ptn, "1D", probes); + // default (no-arg) tolerance: factor * local inter-cut spacing + REDEV_ALWAYS_ASSERT(ptn.GetDefaultTolFactor() == 1e-3); + { + auto ptNear = std::array{0.4999, 0.0, 0.0}; + auto ptFar = std::array{0.499, 0.0, 0.0}; + REDEV_ALWAYS_ASSERT(ptn.GetCandidateRanks(ptNear) == Set({1, 2})); + REDEV_ALWAYS_ASSERT(ptn.GetCandidateRanks(ptFar) == Set({1})); + // the no-arg overload uses the default factor, i.e. + // GetCandidateRanks(pt) == GetCandidateRanks(pt, defaultTolFactor) + REDEV_ALWAYS_ASSERT( + ptn.GetCandidateRanks(ptNear) == + ptn.GetCandidateRanks(ptNear, redev::RCBPtn::GetDefaultTolFactor())); + } + } + { // 2D partition + const auto dim = 2; + redev::LOs ranks = {0, 1, 2, 3}; + redev::Reals cuts = {0, 0.5, 0.75, 0.25}; + redev::RCBPtn ptn(dim, ranks, cuts); + const std::vector probes = { + // tolFactor=0 -> exact cut-plane detection + {{0.1, 0.1, 0.0}, 0.0, Set({0}), "interior LL"}, + {{0.4, 0.8, 0.0}, 0.0, Set({1}), "interior UL"}, + {{0.6, 0.1, 0.0}, 0.0, Set({2}), "interior LR"}, + {{0.8, 0.9, 0.0}, 0.0, Set({3}), "interior UR"}, + {{0.5, 0.5, 0.0}, 0.0, Set({0, 3}), "on root x-cut"}, + {{0.25, 0.75, 0.0}, 0.0, Set({0, 1}), "on node2 y-cut"}, + {{0.75, 0.25, 0.0}, 0.0, Set({2, 3}), "on node3 y-cut (x interior)"}, + {{0.5, 0.25, 0.0}, 0.0, Set({0, 2, 3}), "on x-cut AND y-cut (corner)"}, + {{0.5, 0.75, 0.0}, 0.0, Set({0, 1, 3}), + "on root x-cut AND node2 y-cut (corner across subtrees)"}, + {{0.495, 0.5, 0.0}, 0.04, Set({0}), + "tolFactor on sparse x-axis has no local scale to broaden"}, + {{0.4, 0.745, 0.0}, 0.04, Set({0, 1}), + "tolFactor captures approach to node2 y-cut"}, + // NOTE: probe coordinates must keep clear margin from the cuts - + // e.g. 0.5 - 0.49 == 0.010000000000000009 in IEEE doubles (> 0.01), + // so 'at-tolerance' probes land on the wrong side of the compare. + {{0.4, 0.72, 0.0}, 0.04, Set({0}), "beyond tolFactor stays interior"}, + {{0.01, 0.01, 0.0}, 0.04, Set({0}), + "interior even with tolerance factor"}, + }; + checkCandidates(ptn, "2D", probes); + // default (no-arg) tolerance: local inter-cut spacing + eps guard + REDEV_ALWAYS_ASSERT(ptn.GetDefaultTolFactor() == 1e-3); + { + // On the sparse x-axis (one cut, no neighbor) the local spacing is + // zero, so the tolerance floor is ~1e-14 — even with a non-zero factor, + // near-boundary broadening only applies on dimensions with ≥2 cuts. + auto ptOnX = std::array{0.4999, 0.5, 0.0}; + REDEV_ALWAYS_ASSERT(ptn.GetCandidateRanks(ptOnX) == Set({0})); + // y-axis has two cuts (0.25/0.75, spacing 0.5), so the factor + // 5e-4): a 1e-4 approach to the y-cut broadens candidates, 1e-3 does + // not + auto ptYNear = std::array{0.4, 0.7499, 0.0}; + auto ptYFar = std::array{0.4, 0.749, 0.0}; + REDEV_ALWAYS_ASSERT(ptn.GetCandidateRanks(ptYNear) == Set({0, 1})); + REDEV_ALWAYS_ASSERT(ptn.GetCandidateRanks(ptYFar) == Set({0})); + } + } + { // non-uniform 1D partition: default tolerance is LOCAL, proportional + // to the inter-cut spacing along the axis. cuts {0, 0.8, 0.2, 0.9}: + // local spacing at the 0.2 cut is 0.6 (tol 6e-4) but at the 0.9 cut is + // 0.1 (tol 1e-4), so the SAME absolute distance (5e-4) to the two + // planes behaves differently. + redev::LOs ranks = {0, 1, 2, 3}; + redev::Reals cuts = {0, 0.8, 0.2, 0.9}; + redev::RCBPtn ptn(1, ranks, cuts); + auto ptCoarse = std::array{0.1995, 0.0, 0.0}; // 5e-4 below 0.2 + auto ptFine = std::array{0.8995, 0.0, 0.0}; // 5e-4 below 0.9 + // local default: the coarse-side cut has a wider local tolerance + REDEV_ALWAYS_ASSERT(ptn.GetCandidateRanks(ptCoarse) == Set({0, 1})); + REDEV_ALWAYS_ASSERT(ptn.GetCandidateRanks(ptFine) == Set({2})); + // same factor applied uniformly — the effective tolerance VARIES per cut + // (factor*localSpacing): 0.01*0.6=6e-3 at v=0.2, 0.01*0.1=1e-3 at v=0.9, + // so both points (each 5e-4 from their cut) are within the local tol + REDEV_ALWAYS_ASSERT(ptn.GetCandidateRanks(ptFine, 0.01) == Set({2, 3})); + REDEV_ALWAYS_ASSERT(ptn.GetCandidateRanks(ptCoarse, 0.01) == Set({0, 1})); + } + std::cout << "test_candidateRanks: ALL PASSED\n"; + } + MPI_Finalize(); + return 0; +} \ No newline at end of file