-
Notifications
You must be signed in to change notification settings - Fork 321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement AbstractPath
and related API changes to support multiple path types
#3535
Conversation
Tests are failing because I haven't updated the bindings -- will do that next week. |
@aymanhab, I made some changes to the Java binding code to get everything to compile for now. We can discuss together what changes make the most sense for the GUI. Working on the other test failures now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly looks fine but I would strongly consider supplying [[deprecated]]
backwards-compatible methods, rather than entirely removing the old API, where those methods runtime-downcast-and-check that the implementation is point-based, throwing otherwise. By doing that, you'll drastically reduce this diff while making rollout of the change easier for downstream scripting users.
There's also some shortcuts w.r.t. copying paths in the code. I'd recommend carefully reading where I've mentioned a change changes intent/behavior. The new change may compile, but it is just installing a later issue that someone else has to dig up and figure out what's going on (e.g. "why does my UI crash if I load a model (with FBPs)", "why can't I convert these FBP-based muscles to DeGrootFregly
muscles", etc.)
@@ -62,6 +44,11 @@ Blankevoort1991Ligament::Blankevoort1991Ligament( | |||
set_slack_length(slack_length); | |||
} | |||
|
|||
GeometryPath& Blankevoort1991Ligament::initGeometryPath() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
init
methods on classes are usually a code smell - unless you're writing like a game developer with exceptions turned off or something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm trying to find a way to support creating new objects that own paths (of any type) that is scripting friendly. I'm not sure that this is the best approach. Another approach could be the pointer adoptee approach used elsewhere in OpenSim (although I'm not sure how to update the path
property in that case).
@@ -451,8 +453,9 @@ namespace { | |||
|
|||
// Trigger computing the wrapping path (realizing the state will not). | |||
model.getComponent<PathSpring>("spring").getLength(state); | |||
const WrapResult wrapResult = model.getComponent<PathSpring>("spring") | |||
.getGeometryPath() | |||
const auto& path = dynamic_cast<const GeometryPath&>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getPath<T>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like good changes.
The only thing I noticed was the new method on Muscle
, why's it there?
@@ -66,17 +66,35 @@ class OSIMSIMULATION_API PathActuator : public ScalarActuator { | |||
// GET AND SET | |||
//-------------------------------------------------------------------------- | |||
// Path | |||
bool hasPath() const override { return true;}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: space after ;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 1 of 62 files at r1, 1 of 43 files at r3, 1 of 16 files at r4.
Reviewable status: 2 of 67 files reviewed, 20 unresolved discussions (waiting on @adamkewley and @nickbianco)
CHANGELOG.md
line 11 at r2 (raw file):
Previously, nickbianco (Nicholas Bianco) wrote…
Per your suggestions, I'm opting to revert API changes since supporting
getGeometryPath
andupdGeometryPath
is straight-forward anyway. But I agree, in the end, I'd prefer minimal API changes for those reasons.
👍
OpenSim/Simulation/Model/AbstractPath.h
line 102 at r4 (raw file):
*/ virtual SimTK::Vec3 getColor(const SimTK::State& s) const = 0;
I feel most of the appearance and color stuff doesn't belong here and can be moved to GeometryPath but hard to tell looking at this class if that would cause other problems
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 2 of 16 files at r4.
Reviewable status: 4 of 67 files reviewed, 20 unresolved discussions (waiting on @adamkewley and @nickbianco)
Bindings/Java/OpenSimJNI/OpenSimContext.cpp
line 128 at r2 (raw file):
Previously, nickbianco (Nicholas Bianco) wrote…
Agreed. Reverting these changes for now since I've added back
getGeometryPath()
. @aymanhab and I can discuss what makes sense for a refactor since (I believe) this file is GUI-specific.
Indeed it is GUI specific, and some methods may not be used any more or can be easily refactored once the API settles
Bindings/Java/OpenSimJNI/OpenSimContext.cpp
line 136 at r2 (raw file):
Previously, nickbianco (Nicholas Bianco) wrote…
Similar to above, reverting these changes since I've reverted the
updGeometryPath
change. Will discuss with @aymanhab what makes sense for a refactor here.
This function seems to be unused and can be removed when we revisit.
Bindings/Java/OpenSimJNI/Test/testContext.cpp
line 0 at r2 (raw file):
Previously, adamkewley (Adam Kewley) wrote…
(Reviewable was unable to map this GitHub inline comment thread to the right spot — sorry!)
Is there any harm in keeping
updGeometryPath()
as an alias forupdPath<GeometryPath>
?By removing
addNewPathPoint
,updGeometryPath
,getGeometryPath
, etc. you're publishing a correct(er) API, yes, but any downstream MATLAB/python scripts are going to be broken - even though there's no logical reason for them to break if they are operating on point-based paths (with legacy scripts, it is likely that they will be).Prefer keeping any old API methods with a
[[deprecated]]
marking and have those legacy methods runtime-check the path type (in many cases, the paths will be point-based - for now). Maybe after sitting on it for a while the[[deprecated]]
stuff can be dropped as part of a larger OpenSim5 rollout.I wouldn't recommend breaking any existing OpenSim4 APIs. The vast majority of OpenSim's research userbase are using MATLAB/python, where they won't see the breakage coming and won't know why their scripts no longer work.
💯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had another pass
My main concern is adding those if (dynamic_cast<GeometryPath>...) else { THROW }
blocks. The reason why is because they're the type of thing that will be buried in the codebase and there will be no warning that there's an issue until a user reports it (e.g. it's unlikely that developers will proactively poke around Blankevoort etc. looking for downcasts when adding a new path abstraction).
Even if the solution is (e.g.) to have a copy-assignment method that throws if typeof(lhs) != typeof(rhs)
then at least the explosion is closer to where people are likely to be looking when working on AbstractPath/GeometryPath (rather than it being buried in a particular implementation somewhere)
.connect(pathPointSet.get(ipp) | ||
.getSocket(socketName) | ||
.getConnecteeAsObject()); | ||
// Copy the muscle's path. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd tackle copying centrally in this PR rather than leaving an else
as a future exercise: there's a high chance it'll be forgotten and undetected until a user runs into it at runtime (e.g. by using DeGrooteFregly2016Muscle
with a non-GeometryPath
)
Plus, if copy assignment of paths is something that's in downstream code (e.g. as it is here) then, architecturally speaking, it makes sense that copying is tackled centrally and code is ported accordingly.
OpenSim/Actuators/ModelFactory.cpp
Outdated
.connect(pathPointSet.get(ip) | ||
.getSocket(socketName) | ||
.getConnecteeAsObject()); | ||
// Copy the muscle's path. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't do this. it's a bad idea for a large C++ codebase to start having many if (downcastFailed) { throw ...; }
code, because it makes maintaining the codebase long-term much harder. The compiler won't be able to help developers when new abstractions are added, for example, and implementors will be forced to look for all of these dynamic_cast
s that you are bodging in.
Either:
- Make the abstraction have enough pieces to perform the replacement virtually (required, if you plan on having external code implement new
AbstractPath
s independently of OpenSim) - Add a visitor pattern to the abstraction, where the visitor pattern double-dispatches to a known set of path implementations (constrains downstream implementations to a limited set, but adds static checks that will trigger when new paths are added)
Then you can do what you're doing within the framework of those solutions, but only if the concrete implementation has no feasible way of working without points (e.g. the visitor would throw for non-point-based paths with "there's no way of doing this operation with a function-based path - sorry not sorry" or similar).
@adamkewley, thanks a bunch for the quick reviews across lots of changing files! Regarding the path copying utilities, I'm seeing your point clearer now. In this pass, I implemented a new virtual method @aymanhab, now would be a good time to jump in for a full review. Some thoughts I have about the current state of the PR:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nickbianco I'd agree to avoid breaking changes to Ligaments, with that option 1 seems more straightforward.
I didn't find any usage of the method hasGeometryPath() in opensim-core, it's used by the GUI for special handling of visualization of GeometryPaths and as such should return true only when there're visuals to display/update (ties into the first comment) so we'll need to reimplement with dynamic_cast now or later, or find an alternative.
Reviewed 1 of 62 files at r1, 1 of 6 files at r8.
Reviewable status: 6 of 70 files reviewed, 23 unresolved discussions (waiting on @adamkewley and @nickbianco)
OpenSim/Analyses/MuscleAnalysis.h
line 32 at r8 (raw file):
//============================================================================= #include <OpenSim/Common/PropertyStrArray.h> #include <OpenSim/Simulation/Model/Analysis.h>
Any reason this include is needed here, when the previous version didn't?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 all very good: thank you for enduring my feedback and making some nice changes.
Additional (very optional) suggestions:
- For copying things, because
AbstractPath
supports.clone()
, you may be able to clone the sourceAbstractPath
into a new pointer and then assign that pointer to the relevant property etc. in the target datastructure. The benefit of that approach is that it would work for any path implementation, whereas copy assignment (i.e.copyFrom
) is limited to only allowingT = T const&
expressions, which is why the downcast is necessary. Not your fault (the original code is copy assignment), but might be an idea. - If you need
copyFrom
(e.g. because doing it at a higher level withclone
is a no-go) then it might be better named asassign
, to matchAbstractProperty::assign
and because it's a common idiom (e.g.std::vector<T>::assign
)
Again, these are very optional suggestions (observations, even) - take them or leave them
Thanks @adamkewley and @aymanhab! A few more thoughts:
@adamkewley, I also considered this and like the idea of it, but couldn't fully wrap my head around it. Would default implementations of
@aymanhab, I am tempted to take the simplest path here, but it feels wrong to not support the new wrapping abstraction for half of the classes that own path objects. After a quick look, I did find a breaking change in @clnsmith's opensim-jam project. Would it be so bad to write deprecated methods, even just for
I see, so setting |
You want the For where the pointer needs to go, I imagine that any OpenSim collection of abstract objects must be storing them as pointers (they're abstract, so there is no way for the compiler to know how much space they need), so the "better" (clone-based) approach would need to install the |
@adamkewley, I've implemented your suggestions above for now. I'm thinking over your recent comments and taking a look @aymanhab, I've changed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 5 of 62 files at r1, 24 of 43 files at r3, 9 of 16 files at r4, 2 of 9 files at r5, 4 of 8 files at r7, 10 of 18 files at r9, 11 of 11 files at r10, all commit messages.
Reviewable status: 69 of 70 files reviewed, 26 unresolved discussions (waiting on @adamkewley and @nickbianco)
OpenSim/Simulation/Model/Model.cpp
line 326 at r10 (raw file):
} } if (versionNumber < 40500) {
@nickbianco Typically when we change xml representation of a class, it becomes responsible for its own update so knowledge about the class changes are kept local and the Model class doesn't grow into a bag of unrelated changes that it shouldn't be concerned with. Can you comment on whether its possible/difficult to move this change to another more localized place? Thank you
@aymanhab I moved the XML updates to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed 1 of 4 files at r11.
Reviewable status: 68 of 71 files reviewed, 26 unresolved discussions (waiting on @adamkewley and @nickbianco)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewable status: 68 of 71 files reviewed, 26 unresolved discussions (waiting on @adamkewley and @nickbianco)
OpenSim/Simulation/Model/Model.cpp
line 326 at r10 (raw file):
Previously, aymanhab (Ayman Habib) wrote…
@nickbianco Typically when we change xml representation of a class, it becomes responsible for its own update so knowledge about the class changes are kept local and the Model class doesn't grow into a bag of unrelated changes that it shouldn't be concerned with. Can you comment on whether its possible/difficult to move this change to another more localized place? Thank you
Thank you 👍
We can have a chat about it, but don't let that hold anything up! |
@adamkewley replacing the new virtual method with |
Fixes issue #3388
Brief summary of changes
This PR implements
AbstractPath
, a new base class from which all path-like objects can derive from, includingGeometryPath
. Specifically, derived objects will need to implement virtual methods that provide the path length, lengthening speed, moment arms, body and mobility forces, and visualization properties.My current attempt at this implementation led to a few API changes:
Muscle::addNewPathPoint
), since other path types might not usePathPoint
s at all.GeometryPath
property (i.e.,PathActuator
,PathSpring
,Ligament
, andBlankevoort1991Ligament
), now have anAbstractPath
property, which has been moved toprotected
(TODO: move toprivate
?).updGeometryPath
andgetGeometryPath
have been changed toupdPath
andgetPath
, which returnAbstractPath
references.updPath
andgetPath
for returning concrete objects (e.g.,GeometryPath
) have been added.initGeometryPath
, which is a safe method for overwriting the path-based object's current path with aGeometryPath
. While the current default value for the path properties isGeometryPath
, future methods can be introduced to initialize paths of different types (e.g.,initFunctionBasedPath
). I based this approach on how we initialize different solvers inMocoStudy
.These changes led to a lot of changed files, but most of these files are tests or examples with models with very simple paths (e.g., a single origin and insertion point).
Testing I've completed
Ran all the existing wrapping tests. I'm happy to expand or add new tests, especially for the interface changes.
Looking for feedback on...
Any feedback about the API changes and how they might affect users. This will obviously affect the GUI and OpenSim Creator, but my assumption is that most users do not interact much with
GeometryPath
directly and the effects of the API changes will be mostly internal. However, I am prepared to be totally wrong about that and I'm happy to make changes that best suit everyone.CHANGELOG.md (choose one)
This change is