diff --git a/conductor/tracks/A10/plan.md b/conductor/tracks/A10/plan.md index cd6ae71..04d249a 100644 --- a/conductor/tracks/A10/plan.md +++ b/conductor/tracks/A10/plan.md @@ -1,13 +1,31 @@ # A10 Implementation Plan ## Phase 1: API Design -- [ ] 1.1 Review existing static Compute() signatures -- [ ] 1.2 Design new overload signatures +- [x] 1.1 Review existing static Compute() signatures +- [x] 1.2 Design new overload signatures + +Note: The original issue (#68) proposed two new overloads: + + ```cpp + Compute(mesh, levelRatio, minCoarseVerts); + Compute(mesh, pin0, pin1, levelRatio, minCoarseVerts); + ``` + +After PR #93 introduced the `PinMap` interface, the second shape was +adapted to `Compute(mesh, const PinMap&, levelRatio, minCoarseVerts)`. + +The first shape (`Compute(mesh, levelRatio, minCoarseVerts)`) cannot be +added today because its `(Mesh::Pointer&, size_t, size_t)` signature +collides with the still-present `[[deprecated]] Compute(mesh, pin0Idx, +pin1Idx)`. It is blocked on the deprecated-overload cleanup tracked in +a separate issue (see #68 "blocked by" link). Until then, auto-pin +users who need custom tuning use the instance API (`set_level_ratio`, +`set_min_coarse_vertices`). ## Phase 2: Implementation -- [ ] 2.1 Implement static overloads in HLSCM -- [ ] 2.2 Update single-header via amalgamation script +- [x] 2.1 Implement static overload in HLSCM (PinMap variant) +- [x] 2.2 Update single-header via amalgamation script ## Phase 3: Testing -- [ ] 3.1 Write unit tests for new overloads -- [ ] 3.2 Run full test suite +- [x] 3.1 Write unit tests for new overload +- [x] 3.2 Run full test suite — all tests pass diff --git a/include/OpenABF/HierarchicalLSCM.hpp b/include/OpenABF/HierarchicalLSCM.hpp index b4f6dff..55b2ad5 100644 --- a/include/OpenABF/HierarchicalLSCM.hpp +++ b/include/OpenABF/HierarchicalLSCM.hpp @@ -1097,6 +1097,43 @@ class HierarchicalLSCM ComputeImpl(mesh, pins); } + /** + * @brief Compute with caller-specified pin UVs and hierarchy tuning + * + * Static counterpart to configuring `set_level_ratio()` and + * `set_min_coarse_vertices()` on an instance. + * + * @param levelRatio Target vertex ratio between consecutive hierarchy + * levels. Must be >= 2. + * @param minCoarseVerts Minimum vertex count at the coarsest level. Must + * be >= 3. + * + * @throws std::invalid_argument If `pins` is invalid (see PinMap overload), + * or if `levelRatio < 2`, or if `minCoarseVerts < 3`. + * @throws SolverException If any hierarchy level fails to solve. + * + * @note An auto-pin counterpart `Compute(mesh, levelRatio, minCoarseVerts)` + * is not provided because its signature would collide with the + * deprecated `Compute(mesh, pin0Idx, pin1Idx)` overload. It will be + * added when that overload is removed in 3.0; until then, use the + * instance API for auto-pin selection with custom tuning. + */ + static void Compute(typename Mesh::Pointer& mesh, const PinMap& pins, std::size_t levelRatio, + std::size_t minCoarseVerts) + { + // Validate in argument-declaration order so a bad-pins-and-bad-tuning + // call reports the pin error first, matching the existing + // Compute(mesh, PinMap) overload's behavior. + detail::lscm::ValidatePins(mesh, pins); + if (levelRatio < 2) { + throw std::invalid_argument("HierarchicalLSCM: level_ratio must be >= 2"); + } + if (minCoarseVerts < 3) { + throw std::invalid_argument("HierarchicalLSCM: min_coarse_vertices must be >= 3"); + } + ComputeImpl(mesh, pins, levelRatio, minCoarseVerts); + } + /** * @brief Deprecated: compute with an explicit pin pair by index. * diff --git a/single_include/OpenABF/OpenABF.hpp b/single_include/OpenABF/OpenABF.hpp index 0b5e88b..b21d488 100644 --- a/single_include/OpenABF/OpenABF.hpp +++ b/single_include/OpenABF/OpenABF.hpp @@ -4733,6 +4733,43 @@ class HierarchicalLSCM ComputeImpl(mesh, pins); } + /** + * @brief Compute with caller-specified pin UVs and hierarchy tuning + * + * Static counterpart to configuring `set_level_ratio()` and + * `set_min_coarse_vertices()` on an instance. + * + * @param levelRatio Target vertex ratio between consecutive hierarchy + * levels. Must be >= 2. + * @param minCoarseVerts Minimum vertex count at the coarsest level. Must + * be >= 3. + * + * @throws std::invalid_argument If `pins` is invalid (see PinMap overload), + * or if `levelRatio < 2`, or if `minCoarseVerts < 3`. + * @throws SolverException If any hierarchy level fails to solve. + * + * @note An auto-pin counterpart `Compute(mesh, levelRatio, minCoarseVerts)` + * is not provided because its signature would collide with the + * deprecated `Compute(mesh, pin0Idx, pin1Idx)` overload. It will be + * added when that overload is removed in 3.0; until then, use the + * instance API for auto-pin selection with custom tuning. + */ + static void Compute(typename Mesh::Pointer& mesh, const PinMap& pins, std::size_t levelRatio, + std::size_t minCoarseVerts) + { + // Validate in argument-declaration order so a bad-pins-and-bad-tuning + // call reports the pin error first, matching the existing + // Compute(mesh, PinMap) overload's behavior. + detail::lscm::ValidatePins(mesh, pins); + if (levelRatio < 2) { + throw std::invalid_argument("HierarchicalLSCM: level_ratio must be >= 2"); + } + if (minCoarseVerts < 3) { + throw std::invalid_argument("HierarchicalLSCM: min_coarse_vertices must be >= 3"); + } + ComputeImpl(mesh, pins, levelRatio, minCoarseVerts); + } + /** * @brief Deprecated: compute with an explicit pin pair by index. * diff --git a/tests/src/TestParameterization.cpp b/tests/src/TestParameterization.cpp index 326dec1..704d8cf 100644 --- a/tests/src/TestParameterization.cpp +++ b/tests/src/TestParameterization.cpp @@ -1665,3 +1665,88 @@ TEST(HLSCM, SetPins_MatchesStatic) } } } + +TEST(HLSCM, StaticTuning_MatchesInstance) +{ + // Static Compute(mesh, pins, levelRatio, minCoarseVerts) must produce + // identical output to the equivalent instance API configuration. Uses a + // wavy 20x20 grid so the hierarchy parameters actually drive multi-level + // decimation (defaults would yield a single-level fallback). + using HLSCM = HierarchicalLSCM; + using PinMap = typename HLSCM::PinMap; + + constexpr std::size_t levelRatio = 4; + constexpr std::size_t minCoarseVerts = 25; + PinMap pins{ + {0u, Vec{0.f, 0.f}}, + {19u, Vec{1.f, 0.f}}, + {399u, Vec{1.f, 1.f}}, + }; + + // Verify the chosen tuning actually drives a multi-level hierarchy at this + // mesh size — otherwise both call paths fall back to single-level LSCM and + // the static-vs-instance comparison below would be trivially equal even if + // the overload silently dropped levelRatio/minCoarseVerts. + { + auto probe = ConstructWavySurface(20, 20); + const std::vector pinIndices{0u, 19u, 399u}; + auto [levels, _] = OpenABF::detail::hlscm::BuildHierarchy( + probe, pinIndices, levelRatio, minCoarseVerts); + ASSERT_GE(levels.size(), std::size_t(2)) + << "test setup expected >=2 hierarchy levels; got " << levels.size(); + } + + auto mesh_static = ConstructWavySurface(20, 20); + HLSCM::Compute(mesh_static, pins, levelRatio, minCoarseVerts); + + auto mesh_instance = ConstructWavySurface(20, 20); + HLSCM hlscm; + hlscm.set_pins(pins); + hlscm.set_level_ratio(levelRatio); + hlscm.set_min_coarse_vertices(minCoarseVerts); + hlscm.compute(mesh_instance); + + for (std::size_t v = 0; v < mesh_static->num_vertices(); ++v) { + const auto& vs = mesh_static->vertex(v)->pos; + const auto& vi = mesh_instance->vertex(v)->pos; + for (auto i = 0; i < 3; i++) { + EXPECT_FLOAT_EQ(vi[i], vs[i]) << "vertex " << v << " comp " << i; + } + } + + // Pins must land exactly at the requested UVs. + for (const auto& [vIdx, uv] : pins) { + const auto& p = mesh_static->vertex(vIdx)->pos; + EXPECT_FLOAT_EQ(p[0], uv[0]) << "pin v" << vIdx << " u"; + EXPECT_FLOAT_EQ(p[1], uv[1]) << "pin v" << vIdx << " v"; + EXPECT_FLOAT_EQ(p[2], 0.f) << "pin v" << vIdx << " z"; + } +} + +TEST(HLSCM, StaticTuning_RejectsBadParameters) +{ + // Validation must match the instance setters: level_ratio < 2 and + // min_coarse_vertices < 3 both throw std::invalid_argument. + using HLSCM = HierarchicalLSCM; + using PinMap = typename HLSCM::PinMap; + + auto mesh = ConstructPyramid(); + PinMap pins{ + {0u, Vec{0.f, 0.f}}, + {1u, Vec{2.f, 0.f}}, + }; + + EXPECT_THROW(HLSCM::Compute(mesh, pins, /*levelRatio=*/1, /*minCoarseVerts=*/10), + std::invalid_argument); + EXPECT_THROW(HLSCM::Compute(mesh, pins, /*levelRatio=*/0, /*minCoarseVerts=*/10), + std::invalid_argument); + EXPECT_THROW(HLSCM::Compute(mesh, pins, /*levelRatio=*/4, /*minCoarseVerts=*/2), + std::invalid_argument); + EXPECT_THROW(HLSCM::Compute(mesh, pins, /*levelRatio=*/4, /*minCoarseVerts=*/0), + std::invalid_argument); + + // PinMap validation must still apply through the tuning overload. + PinMap badPins{{0u, Vec{0.f, 0.f}}}; // only one pin + EXPECT_THROW(HLSCM::Compute(mesh, badPins, /*levelRatio=*/4, /*minCoarseVerts=*/10), + std::invalid_argument); +}