Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions conductor/tracks/A10/plan.md
Original file line number Diff line number Diff line change
@@ -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
37 changes: 37 additions & 0 deletions include/OpenABF/HierarchicalLSCM.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, Mesh>(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.
*
Expand Down
37 changes: 37 additions & 0 deletions single_include/OpenABF/OpenABF.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, Mesh>(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.
*
Expand Down
85 changes: 85 additions & 0 deletions tests/src/TestParameterization.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<float>;
using PinMap = typename HLSCM::PinMap;

constexpr std::size_t levelRatio = 4;
constexpr std::size_t minCoarseVerts = 25;
PinMap pins{
{0u, Vec<float, 2>{0.f, 0.f}},
{19u, Vec<float, 2>{1.f, 0.f}},
{399u, Vec<float, 2>{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<HLSCM::Mesh>(20, 20);
const std::vector<std::size_t> pinIndices{0u, 19u, 399u};
auto [levels, _] = OpenABF::detail::hlscm::BuildHierarchy<float>(
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<HLSCM::Mesh>(20, 20);
HLSCM::Compute(mesh_static, pins, levelRatio, minCoarseVerts);

auto mesh_instance = ConstructWavySurface<HLSCM::Mesh>(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<float>;
using PinMap = typename HLSCM::PinMap;

auto mesh = ConstructPyramid<HLSCM::Mesh>();
PinMap pins{
{0u, Vec<float, 2>{0.f, 0.f}},
{1u, Vec<float, 2>{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<float, 2>{0.f, 0.f}}}; // only one pin
EXPECT_THROW(HLSCM::Compute(mesh, badPins, /*levelRatio=*/4, /*minCoarseVerts=*/10),
std::invalid_argument);
}
Loading