Skip to content

Commit

Permalink
Add ModelWarperConfiguration::tryMatchStrategy
Browse files Browse the repository at this point in the history
  • Loading branch information
adamkewley committed Jul 2, 2024
1 parent d4cdb6d commit 97998f2
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<ComponentWarpingStrategy>()) {
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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <OpenSim/Simulation/Model/PhysicalOffsetFrame.h>
#include <OpenSim/Simulation/Model/Station.h>

#include <compare>
#include <concepts>
#include <filesystem>
#include <sstream>
Expand All @@ -14,15 +15,42 @@

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 {
OpenSim_DECLARE_ABSTRACT_OBJECT(ComponentWarpingStrategy, OpenSim::Component);
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();
}
Expand All @@ -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();
Expand Down Expand Up @@ -66,16 +123,25 @@ namespace osc::mow
}
}
}

const std::type_info* _targetComponentType;
};

// abstract interface to a component that is capable of warping `n` other
// components (`StrategyTargets`) of type `T` during a model warp
template<std::derived_from<OpenSim::Component> 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<const T*>(&component) != nullptr;
}
};

// abstract interface to a component that is capable of warping `n`
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include <TestOpenSimCreator/TestOpenSimCreatorConfig.h>

#include <gtest/gtest.h>
#include <OpenSim/Simulation/Model/Model.h>
#include <OpenSimCreator/Utils/OpenSimHelpers.h>
#include <oscar/Utils/TemporaryFile.h>

#include <filesystem>
Expand All @@ -18,6 +20,12 @@ namespace
}
}

static_assert(StrategyMatchQuality::none() < StrategyMatchQuality::wildcard());
static_assert(StrategyMatchQuality::wildcard() < StrategyMatchQuality::exact());
static_assert(static_cast<bool>(StrategyMatchQuality::none()) == false);
static_assert(static_cast<bool>(StrategyMatchQuality::wildcard()) == true);
static_assert(static_cast<bool>(StrategyMatchQuality::exact()) == true);

TEST(ModelWarperConfiguration, CanDefaultConstruct)
{
[[maybe_unused]] ModelWarperConfiguration configuration;
Expand Down Expand Up @@ -171,3 +179,147 @@ TEST(ModelWarperConfiguration, finalizeFromPropertiesDoesNotThrowWhenGivenConfig

ASSERT_NO_THROW({ configuration.finalizeFromProperties(); });
}

TEST(ModelWarperConfiguration, MatchingAnOffsetFrameStrategyToExactPathWorksAsExpected)
{
OpenSim::Model model;
OpenSim::PhysicalOffsetFrame& pof = AddComponent<OpenSim::PhysicalOffsetFrame>(
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<OpenSim::PhysicalOffsetFrame>(
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<OpenSim::PhysicalOffsetFrame>(
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<OpenSim::PhysicalOffsetFrame>(
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<OpenSim::PhysicalOffsetFrame>(
model,
"someoffsetframe",
model.getGround(),
SimTK::Transform{}
);
model.finalizeConnections();
ASSERT_EQ(pof.getAbsolutePathString(), "/someoffsetframe");

ModelWarperConfiguration configuration;
// add less-specific strategy
{
auto strategy = std::make_unique<ProduceErrorOffsetFrameWarpingStrategy>();
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<IdentityOffsetFrameWarpingStrategy>();
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<const IdentityOffsetFrameWarpingStrategy*>(matchedStrategy), nullptr);
}

TEST(ModelWarperConfiguration, tryMatchStrategyDoesNotThrowIfTwoWildcardsForDifferentTargetsMatch)
{
OpenSim::Model model;
OpenSim::PhysicalOffsetFrame& pof = AddComponent<OpenSim::PhysicalOffsetFrame>(
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<ProduceErrorStationWarpingStrategy>();
strategy->append_StrategyTargets("*");
configuration.addComponent(strategy.release());
}
// add a wildcard strategy specifically for `OpenSim::PhysicalOffsetFrame`
{
auto strategy = std::make_unique<ProduceErrorOffsetFrameWarpingStrategy>();
strategy->append_StrategyTargets("*");
configuration.addComponent(strategy.release());
}
configuration.finalizeConnections(configuration);

const ComponentWarpingStrategy* matchedStrategy = configuration.tryMatchStrategy(pof);

ASSERT_NE(matchedStrategy, nullptr);
ASSERT_NE(dynamic_cast<const ProduceErrorOffsetFrameWarpingStrategy*>(matchedStrategy), nullptr);
}

0 comments on commit 97998f2

Please sign in to comment.