opensim-org / opensim-core

SimTK OpenSim C++ libraries and command-line applications, and Java/Python wrapping.
https://opensim.stanford.edu
Apache License 2.0
759 stars 308 forks source link

Add initial implementation of StationDefinedFrame #3694

Closed adamkewley closed 4 months ago

adamkewley commented 5 months ago

Adds a new component, StationDefinedFrame, which is a PhysicalFrame that has its position/orientation automatically derived from Stations in the model.

The utility of a StationDefinedFrame is that it more closely (note: not entirely closely) matches ISB's Grood-Suntay-style frame definitions. Those definitions are usually given in terms of fixed axes, cross products between points in space, etc., rather than as a hard (position, orientation) tuple (which is effectively what we're currently doing with OffsetFrames). More information available on the StationDefinedFrame's comment string.

Brief summary of changes (WIP)

Testing I've completed

Looking for feedback on...

CHANGELOG.md (choose one)


Demo

Available from GitHub Actions builds of this PR:

Lets you add StationDefinedFrame into a model. You can then Add Body and choose the StationDefinedFrame as the parent. BEWARE, THOUGH: there's a bug in opensim-core (mentioned+tested in the added unit test suites, requires #3697 to fix it) where chaining two dependent frames as a joint parent doesn't work (e.g. Ground <-- PoF <-- PoF <-- Joint --> Body does not work, even on opensim-core/main, when I tried).

image


Extra Information

The reason StationDefinedFrame is closer, but not the same as the ISB's frame definitions is because ISB's definitions also include concepts such as "the centroid between two condyls" or "the center of the femoral head", or "the cross product between this edge and another cross product edge".

OpenSim Creator's specialized Frame Definition UI (available on the splash screen) has allowances for this, and was built with concepts such as Edge, Midpoint, etc. However, OSC doesn't support using the frames as OpenSim::Joint frames. This is because of various technical issues, such as the fact that we built on top of OpenSim::Point, rather than OpenSim::Station. It also requires baking the resulting model into PhysicalOffsetFrames, for compatibility with non-OSC codebases.

Adding support for these concepts can come later to OpenSim in the form of StationDefinedCentroid, RigidPoint, etc. This PR is a very stripped down (mathematically, the bare minimum you can get away with) implementation that doesn't rely on other concepts (otherwise, this PR would contain 5 new classes).


F&Q

ℹ️ These questions/changes popped up during development, so I have written them here in case people ask in a few years time.

Q: Why StationDefinedFrame, rather than (e.g.) PointDefinedFrame?

A: OSC has components such as EdgeDefinedFrame and PointDefinedFrame, but that was a bad idea.

Points may be defined in frames that are indirectly dependent on other frames that are defined using the Points, creating a circular dependency. Additionally, Point is incompatible with simbody's system creation: the base frames, and relative orientation within those base frames, needs to be known before a SimTK::State is available (Point is only usable after a SimTK::State is available).

Station is defined as rigidly fixed with respect to a base frame and is guaranteed have a state-independent motion with respect to that frame. However, Stations aren't computed, which means that concepts in OSC (e.g. Midpoint) can't be used with a StationDefinedFrame. Doing so requires defining a new virtual API called (e.g.) RigidPoint, which Station (and computed locations) would derive from. That would be a later PR.

Q: Why "three triangle points, plus a location"

A: Guarantees that the implementation can create a right-handed coordinate system at a given offset w.r.t. a base frame. We tried designs like "pick two orthogonal edges", but it's error-prone if the user doesn't select two actually-orthogonal edges. This design ensures that--so long as none of the 3 points are co-located--the implementation will be able to render a Transform from the inputs.

Q: Why are ab_axis and ab_x_ac_axis exposed as user-editable properties?

A: Practical ease-of-use. The user may be able to swap around the point_a, point_b, and point_c sockets to achieve a similar effect, but that's quite a lot of faffing around - especially if swapping sockets isn't easy. The proposed UX that StationDefinedFrame aims for is to ask the user for 3/4 locations, followed by allowing the user to tweak ab_axis/ab_x_ac_axis in the property editor of OpenSim Creator or OpenSim GUI until they have the orientation they desire.

Q: Why must all stations be defined w.r.t. the same base frame?


This change is Reviewable

adamkewley commented 5 months ago

Downstream throwaway PR created in opensim-creator that uses this PR, so that I can play with StationDefinedFrame as a user-facing feature:

adamkewley commented 5 months ago

The PR built, passed tests, etc. and I was able to load a very basic example model with the topology:

Into OSC, yielding a model that has a frame that can be modified by moving the associated stations around, or by changing (e.g.) ab_axis:

image

The rotation + position of the pictured frame is entirely controlled by the locations of the 4 stations (the origin one being separate from the points in this example - but it doesn't have to be).

adamkewley commented 5 months ago

The implementation now mostly works, but there appears to be a bug (or a mis-use in the test suite) that makes pathological use-cases not work as intended.

E.g. this kind of topology appears to be broken in opensim-core/main and this PR:

Because, when the Joint is being added to the system, it ends up asking for MobilizedBodyIndexes that aren't yet available (note: the same problem doesn't happen if there's only one PhysicalOffsetFrame, which implies immediate connectees are handled).

The model in the test suite (failing) topology with many dependent frames/stations, and I'm going to look into why this, or the simpler example above, don't work.

adamkewley commented 4 months ago

Downstream updated with this latest commit:

I'll test it in the UI to see if it's behaving itself (within the known constraints caused by model graph traversal issues) and then un-WIP this

adamkewley commented 4 months ago

OSC appears to be fine with adding/using StationDefinedFrames in the limited case that they are used for (e.g.) joint parents. They still cannot be used in longer chains of PhysicalOffsetFrames, or as joint children, because of graph traversal bugs. However, fixing those won't change the general implementation strategy and user-facing API of StationDefinedFrame, so this should be good for review/shipping now.

adamkewley commented 4 months ago

Apart from fixing the review comments, the model graph traversal algorithm needs to be re-checked: there was some discussion about whether it introduces different behavior for PoFs

adamkewley commented 4 months ago

My general conclusions from a longer discussion on this:

Details:

That code will need to be changed to account for both transitive dependencies (e.g. chains of PoFs would have this issue, but StationDefinedFrame is more likely to have it because of its transitive dependencies on other frames) and to account for the fact that PhysicalOffsetFrame isn't the only thing that can be a dependent frame.

Further discussion was around the idea that we might need another interface between PhysicalOffsetFrame and PhysicalFrame, e.g. called PhysicalComputedFrame or similar, so that there is one uniform interface to downcast against (having two dynamic_casts for PhysicalOffsetFrame and StationDefinedFrame is a code smell). There is also the idea of re-engineering the graph traversal entirely to account for Sockets, but I am going to keep that separate.

adamkewley commented 4 months ago

@nickbianco

Optional: Would there be value in adding a constructor that only requires the four points and uses your default ab_axis and ab_x_ac_axis values? Or do you think this could cause confusion later on for the user?

I'd prefer it to be explicit for now: if users start demanding it, then it's easy enough to patch in, though!

adamkewley commented 4 months ago

Merging this, because the feature works, the model graph traversal is equivalent to the previous code etc.

However, there's a few issues with how opensim-core traverses dependent frames, such as PhysicalOffsetFrames, which means that you can't (e.g.) chain them on either side of a joint.

I have patches for those issues, but will PR them separately to prevent this PR from becoming a death-walk - thanks @pepbos and @nickbianco for reviewing it (the patches should be less treacherous :D)