From 97998f21177e81c37a50a9eaa12846ef64949e19 Mon Sep 17 00:00:00 2001 From: Adam Kewley Date: Tue, 2 Jul 2024 16:32:47 +0200 Subject: [PATCH] Add ModelWarperConfiguration::tryMatchStrategy --- .../ModelWarper/ModelWarperConfiguration.cpp | 25 +++ .../ModelWarper/ModelWarperConfiguration.h | 80 ++++++++- .../TestModelWarperConfiguration.cpp | 152 ++++++++++++++++++ 3 files changed, 251 insertions(+), 6 deletions(-) diff --git a/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.cpp b/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.cpp index cd69f06c95..1e597762e6 100644 --- a/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.cpp +++ b/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.cpp @@ -48,3 +48,28 @@ void osc::mow::ModelWarperConfiguration::extendFinalizeFromProperties() } } } + +const osc::mow::ComponentWarpingStrategy* osc::mow::ModelWarperConfiguration::tryMatchStrategy(const OpenSim::Component& component) const +{ + struct StrategyMatch { + const ComponentWarpingStrategy* strategy = nullptr; + StrategyMatchQuality quality = StrategyMatchQuality::none(); + }; + + StrategyMatch bestMatch; + for (const ComponentWarpingStrategy& strategy : getComponentList()) { + const auto quality = strategy.calculateMatchQuality(component); + if (quality == StrategyMatchQuality::none()) { + continue; // no quality + } + else if (quality == bestMatch.quality) { + std::stringstream ss; + ss << "ambigous match detected: both " << strategy.getAbsolutePathString() << " and " << bestMatch.strategy->getAbsolutePathString() << " match to " << component.getAbsolutePathString(); + OPENSIM_THROW_FRMOBJ(OpenSim::Exception, std::move(ss).str()); + } + else if (quality > bestMatch.quality) { + bestMatch = {&strategy, quality}; // overwrite with better quality + } + } + return bestMatch.strategy; +} diff --git a/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.h b/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.h index 3d87252a82..87576fedbc 100644 --- a/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.h +++ b/src/OpenSimCreator/Documents/ModelWarper/ModelWarperConfiguration.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -14,6 +15,34 @@ namespace osc::mow { + // describes how closely (if at all) a `ComponentWarpingStrategy` matches a + // given `OpenSim::Component` + // + // used for resolving potentially-ambiguous matches across multiple strategies + class StrategyMatchQuality final { + public: + static constexpr StrategyMatchQuality none() { return StrategyMatchQuality{State::None}; } + static constexpr StrategyMatchQuality wildcard() { return StrategyMatchQuality{State::Wildcard}; } + static constexpr StrategyMatchQuality exact() { return StrategyMatchQuality{State::Exact}; } + + constexpr operator bool () const { return _state != State::None; } + + friend constexpr bool operator==(StrategyMatchQuality, StrategyMatchQuality) = default; + friend constexpr auto operator<=>(StrategyMatchQuality, StrategyMatchQuality) = default; + private: + enum class State { + None, + Wildcard, + Exact + }; + + explicit constexpr StrategyMatchQuality(State state) : + _state{state} + {} + + State _state = State::None; + }; + // abstract interface to a component that is capable of warping `n` other // components (`StrategyTargets`) during a model warp class ComponentWarpingStrategy : public OpenSim::Component { @@ -21,8 +50,7 @@ namespace osc::mow public: OpenSim_DECLARE_LIST_PROPERTY(StrategyTargets, std::string, "a sequence of strategy target strings that this strategy applies to"); protected: - ComponentWarpingStrategy(const std::type_info& targetComponentType) : - _targetComponentType{&targetComponentType} + ComponentWarpingStrategy() { constructProperty_StrategyTargets(); } @@ -35,9 +63,38 @@ namespace osc::mow const std::type_info& getTargetComponentTypeInfo() const { - return *_targetComponentType; + return implGetTargetComponentTypeInfo(); + } + + StrategyMatchQuality calculateMatchQuality(const OpenSim::Component& candidateComponent) const + { + if (not implIsMatchForComponentType(candidateComponent)) { + return StrategyMatchQuality::none(); + } + + const auto componentAbsPath = candidateComponent.getAbsolutePathString(); + + // loop through strategy targets and select the best one, throw if any match + // is ambiguous + StrategyMatchQuality best = StrategyMatchQuality::none(); + for (int i = 0; i < getProperty_StrategyTargets().size(); ++i) { + const std::string& target = get_StrategyTargets(i); + if (target == componentAbsPath) { + // you can't do any better than this, and `extendFinalizeFromProperties` + // guarantees no other `StrategyTarget`s are going to match exactly, so + // exit early + return StrategyMatchQuality::exact(); + } + else if (target == "*") { + best = StrategyMatchQuality::wildcard(); + } + } + return best; } private: + virtual const std::type_info& implGetTargetComponentTypeInfo() const = 0; + virtual bool implIsMatchForComponentType(const OpenSim::Component&) const = 0; + void extendFinalizeFromProperties() override { assertStrategyTargetsNotEmpty(); @@ -66,8 +123,6 @@ namespace osc::mow } } } - - const std::type_info* _targetComponentType; }; // abstract interface to a component that is capable of warping `n` other @@ -75,7 +130,18 @@ namespace osc::mow template T> class ComponentWarpingStrategyFor : public ComponentWarpingStrategy { public: - ComponentWarpingStrategyFor() : ComponentWarpingStrategy{typeid(T)} {} + ComponentWarpingStrategyFor() = default; + + private: + const std::type_info& implGetTargetComponentTypeInfo() const override + { + return typeid(T); + } + + bool implIsMatchForComponentType(const OpenSim::Component& component) const override + { + return dynamic_cast(&component) != nullptr; + } }; // abstract interface to a component that is capable of warping `n` @@ -143,6 +209,8 @@ namespace osc::mow // constructs a `ModelWarperConfiguration` by loading its properties from an XML file // at the given filesystem location explicit ModelWarperConfiguration(const std::filesystem::path& filePath); + + const ComponentWarpingStrategy* tryMatchStrategy(const OpenSim::Component&) const; private: void constructProperties(); void extendFinalizeFromProperties() override; diff --git a/tests/TestOpenSimCreator/Documents/ModelWarper/TestModelWarperConfiguration.cpp b/tests/TestOpenSimCreator/Documents/ModelWarper/TestModelWarperConfiguration.cpp index 212bad859c..e70ef08255 100644 --- a/tests/TestOpenSimCreator/Documents/ModelWarper/TestModelWarperConfiguration.cpp +++ b/tests/TestOpenSimCreator/Documents/ModelWarper/TestModelWarperConfiguration.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include @@ -18,6 +20,12 @@ namespace } } +static_assert(StrategyMatchQuality::none() < StrategyMatchQuality::wildcard()); +static_assert(StrategyMatchQuality::wildcard() < StrategyMatchQuality::exact()); +static_assert(static_cast(StrategyMatchQuality::none()) == false); +static_assert(static_cast(StrategyMatchQuality::wildcard()) == true); +static_assert(static_cast(StrategyMatchQuality::exact()) == true); + TEST(ModelWarperConfiguration, CanDefaultConstruct) { [[maybe_unused]] ModelWarperConfiguration configuration; @@ -171,3 +179,147 @@ TEST(ModelWarperConfiguration, finalizeFromPropertiesDoesNotThrowWhenGivenConfig ASSERT_NO_THROW({ configuration.finalizeFromProperties(); }); } + +TEST(ModelWarperConfiguration, MatchingAnOffsetFrameStrategyToExactPathWorksAsExpected) +{ + OpenSim::Model model; + OpenSim::PhysicalOffsetFrame& pof = AddComponent( + model, + "someoffsetframe", + model.getGround(), + SimTK::Transform{} + ); + model.finalizeConnections(); + ASSERT_EQ(pof.getAbsolutePathString(), "/someoffsetframe"); + + ProduceErrorOffsetFrameWarpingStrategy strategy; + strategy.append_StrategyTargets("/someoffsetframe"); + strategy.finalizeConnections(strategy); + + ASSERT_EQ(strategy.calculateMatchQuality(pof), StrategyMatchQuality::exact()); +} + +TEST(ModelWarperConfiguration, MatchingAnOffsetFrameStrategyToWildcardWorksAsExpected) +{ + OpenSim::Model model; + OpenSim::PhysicalOffsetFrame& pof = AddComponent( + model, + "someoffsetframe", + model.getGround(), + SimTK::Transform{} + ); + model.finalizeConnections(); + ASSERT_EQ(pof.getAbsolutePathString(), "/someoffsetframe"); + + ProduceErrorOffsetFrameWarpingStrategy strategy; + strategy.append_StrategyTargets("*"); + strategy.finalizeConnections(strategy); + + ASSERT_EQ(strategy.calculateMatchQuality(pof), StrategyMatchQuality::wildcard()); +} + +TEST(ModelWarperConfiguration, MatchesExactlyEvenIfWildcardMatchIsAlsoPresent) +{ + OpenSim::Model model; + OpenSim::PhysicalOffsetFrame& pof = AddComponent( + model, + "someoffsetframe", + model.getGround(), + SimTK::Transform{} + ); + model.finalizeConnections(); + ASSERT_EQ(pof.getAbsolutePathString(), "/someoffsetframe"); + + ProduceErrorOffsetFrameWarpingStrategy strategy; + strategy.append_StrategyTargets("*"); + strategy.append_StrategyTargets("/someoffsetframe"); // should match this + strategy.finalizeConnections(strategy); + + ASSERT_EQ(strategy.calculateMatchQuality(pof), StrategyMatchQuality::exact()); +} + +TEST(ModelWarperConfiguration, MatchesWildcardIfInvalidPathPresent) +{ + OpenSim::Model model; + OpenSim::PhysicalOffsetFrame& pof = AddComponent( + model, + "someoffsetframe", + model.getGround(), + SimTK::Transform{} + ); + model.finalizeConnections(); + ASSERT_EQ(pof.getAbsolutePathString(), "/someoffsetframe"); + + ProduceErrorOffsetFrameWarpingStrategy strategy; + strategy.append_StrategyTargets("/someinvalidpath"); + strategy.append_StrategyTargets("*"); // should match this, because the exact one isn't valid for the component + strategy.finalizeConnections(strategy); + + ASSERT_EQ(strategy.calculateMatchQuality(pof), StrategyMatchQuality::wildcard()); +} + +TEST(ModelWarperConfiguration, MatchesMoreSpecificStrategyWhenTwoStrategiesAreAvailable) +{ + OpenSim::Model model; + OpenSim::PhysicalOffsetFrame& pof = AddComponent( + model, + "someoffsetframe", + model.getGround(), + SimTK::Transform{} + ); + model.finalizeConnections(); + ASSERT_EQ(pof.getAbsolutePathString(), "/someoffsetframe"); + + ModelWarperConfiguration configuration; + // add less-specific strategy + { + auto strategy = std::make_unique(); + strategy->append_StrategyTargets("*"); // should match this, because the exact one isn't valid for the component + configuration.addComponent(strategy.release()); + } + // add more-specific one + { + auto strategy = std::make_unique(); + strategy->append_StrategyTargets("/someoffsetframe"); + configuration.addComponent(strategy.release()); + } + configuration.finalizeConnections(configuration); + + const ComponentWarpingStrategy* matchedStrategy = configuration.tryMatchStrategy(pof); + + ASSERT_NE(matchedStrategy, nullptr); + ASSERT_NE(dynamic_cast(matchedStrategy), nullptr); +} + +TEST(ModelWarperConfiguration, tryMatchStrategyDoesNotThrowIfTwoWildcardsForDifferentTargetsMatch) +{ + OpenSim::Model model; + OpenSim::PhysicalOffsetFrame& pof = AddComponent( + model, + "someoffsetframe", + model.getGround(), + SimTK::Transform{} + ); + model.finalizeConnections(); + ASSERT_EQ(pof.getAbsolutePathString(), "/someoffsetframe"); + + ModelWarperConfiguration configuration; + // add a wildcard strategy specifically for `OpenSim::Station` + { + auto strategy = std::make_unique(); + strategy->append_StrategyTargets("*"); + configuration.addComponent(strategy.release()); + } + // add a wildcard strategy specifically for `OpenSim::PhysicalOffsetFrame` + { + auto strategy = std::make_unique(); + strategy->append_StrategyTargets("*"); + configuration.addComponent(strategy.release()); + } + configuration.finalizeConnections(configuration); + + const ComponentWarpingStrategy* matchedStrategy = configuration.tryMatchStrategy(pof); + + ASSERT_NE(matchedStrategy, nullptr); + ASSERT_NE(dynamic_cast(matchedStrategy), nullptr); +}