-
Notifications
You must be signed in to change notification settings - Fork 322
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
Iterator Matlab Class for easier access #1851
Conversation
An interface like this could be really helpful for MATLAB users who are unfamiliar with C++ iterators. I'm wondering whether |
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.
Something like this would be extremely helpful to users. It's great you included a test. We should make use of this in our example files so people know how to use it.
I think we could improve the clarity of the interface a little more. I think there are two different ways in which users want to access subcomponents: looping through components of a specific type, and getting a specific single component by name.
Right now, these two concepts seem to be intertwined unnecessarily. If you want to get a single component by name, you don't need the ComponentLists. Just use your eval()
-getConcreteClassName()
trick (however, this will not always work; some classes have weird names I think).
This class does not really seem to support iterating through components in a simpler fashion (e.g., by index).
My proposal is:
- Change
get()
to take an index rather than a name, thereby allowing one to use this class to iterate through components. - Perhaps add a
getByName()
but this use should be discouraged. - Add a separate function to Utilities named
osimGetComponent()
orosimGetComp()
etc. that fetches a single component and downcasts it for you. The signature would beosimGetComponent(model, path)
.
We should discuss in a dev meeting.
elseif nargin == 1 | ||
error('constructor takes two inputs, not one') | ||
elseif nargin > 2 | ||
error(['2 inputs required, num2str(nargin) ' given]) |
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.
The num2str(nargin)
is inside the string, and therefore will not be evaluated. I think you intended to place it outside the string.
while ~li.equals(list.end()) | ||
if strcmp(char(li.getName()),name) | ||
pathname = li.getAbsolutePathName(); | ||
comp = model.getComponent(pathname); |
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.
You already have the component in the list; no need to fetch it out of the model.
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.
Oh now I think I remember...it doesn't quite work. Well it should be an easy fix to the swig interface files to make that work. I think the issue is described here ("MATLAB methods can't start with underscore muscle = muscleIter.deref();").
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.
Looking at the iterator methods, it is not clear then how to return a reference to a particular object.
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.
Why can't you return li
since it is the Component you are querying?
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.
@aseth1 the type of li
is something like ComponentListIterator<Component>
. It is not the actual component and we should hide it from the user.
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.
We could add a fix to the SWIG interface as a performance enhancement sometime in the future.
My opinion is that it's a good start but really this should be done on the opensim-core side or at worst in SWIG bindings side. Forcing Matlab users to use lists and iterating is cumbersome, error-prone and counter-intuitive as far as I can tell. We should create methods to build Sets (or any other preferred container) of proper type and return them. Most scripters are using pre-made models so cached, properly-typed containers will make such an easy task easy while the iterators allow more involved model building/modification type of tasks possible. |
Potentially relevant discussion in Forum topic 8086. There, the suggestion was "As of 4.0 Beta API, we're discouraging the use of BodySet, JointSet etc. as they may not provide a complete list of Bodies, Joints, ... since the model structure is potentially hierarchical, instead you'd use model.getComponent("PathName") and then safeDowncast to proper type (if you need to)." I'm unclear about how we're expecting users to interact with the new API. These expectations may not align with what's most natural. 💡 Also note that |
Good point @tkuchida, my answer on the forum though was in the context of the current (what's available today) rather than what's the ideal 4.0 final API. The forum post goes to highlight the issues with the interface as it stands. Thank you. |
Ok, thanks for clarifying. I actually encountered this issue in my own code yesterday and I was confused because I thought use of BodySet, JointSet, etc. were now discouraged so I was surprised to see them used internally by |
@aseth1, if you could review and let me know. I would like to write some documentation for using lists— perhaps we could chat (Zoom)?. |
Per discussion in stand-up the problem is caused by the ' deref()' method not available in Matlab. Will look into the swig bindings and create an alias that's allowable (since other code already uses deref we don't want to just rename the method). |
…available in matlab
@jimmyDunne deref() method added, and should show up for all iterators, please let me know if that works for you. |
Thanks, @aymanhab! That made the Matlab implementation doable. This seems to work well now. |
@aseth1, should be good for a re-review 👍 |
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.
LGTM. I made some comments that might make it more helpful.
@@ -69,25 +89,36 @@ | |||
end | |||
outputnames = outputnames'; | |||
end | |||
function reference = get(obj,name) | |||
function reference = getByName(obj,name) |
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.
Do Matlab style comments after the method appear if you type help classname.methodname
in Matlab? If so, it might be nice to describe the method and input arguments.
comp = model.getComponent(pathname); | ||
class = comp.getConcreteClassName(); | ||
eval(['reference = ' char(class) '.safeDownCast(comp);']) | ||
reference = li.deref; |
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.
👍
error(['Component name not found: ' name]) | ||
end | ||
end | ||
function reference = getByIndex(obj,index) |
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.
This seems fine for getting a single object by index, but I'm not sure we would want to then use this method to iterate through the list by index. You could imagine a user doing this:
for i = 1: osimList.getSize()
comp = osimList.getByIndex(i);
comp.setSomePropertyValue(val);
end
which essentially iterates through the same ComponentList
numerous times. Instead if we could access the internal list directly we could do something like:
list = osimList.getList()
li = list.begin()
while ~li.equals(list.end())
li.next();
li.setSomePropertyValue(val);
end
which would be more efficient.
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.
FWIW, I would expect most MATLABers to use a for
loop by default. To avoid inefficiency, might it be possible to keep track of the last item that was retrieved and start searching from there (essentially doing li.next()
under the covers until list.end()
, then starting from the beginning)? Alternatively, perhaps just documentation (with the above warning and code snippets) would be helpful under the getByIndex()
method.
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.
Thanks @tkuchida, @aseth1, and @chrisdembia for the awesome comments! This design feedback is great— I have been spending some time thinking about the philosophy of the list design and how that conflicts with the typical Matlab user for-loop strategy. The original reason I wanted to make this was to make it easier for me to get a single component from a list without having to write a while loop everytime. Perhaps it would be better to simplify this class to a set of static methods that are for single use only;
% Get the number of components in the model
nComps = osimList.getNumberOfComponents(model, 'Body')
% Get all the body names (as strings)
bNames = osimList.getNames(model, 'Body')
% I only want to work on the Pelvis, so I get it directly.
pelvis = osimList.getComponent(model, 'Body', names{i} )
This wouldn't stop anyone from using it in a for-loop, but we could write in documentation, and in the help section of the m-file, when to use the above method and when to use the list directly (when you want to deal with most components in the list).
I am happy to jump on a call to discuss since I feel this may need discussion.
|
||
classdef osimList < matlab.mixin.SetGet | ||
% osimList(classname) | ||
% Use OpenSim lists to iterate through model components |
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.
Perhaps a clearer description would be:
OsimList provides convenient access to a Model's components listed according to type by name or index. To make edits to all items in a list, consider iterating through a ComponentList directly.
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 made a few comments. This will help users. I wouldn't worry about optimizing the interface/implementation of this class, because as we have discussed, a long term solution will be in C++.
% OpenSim Utility Class | ||
% Inputs: | ||
% model Reference to OpenSim Model (Model()) | ||
% classname OpenSim component class name ('Body', 'Muscle' |
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.
Missing a parenthesis?
elseif nargin == 1 | ||
error('constructor takes two inputs, not one') | ||
elseif nargin > 2 | ||
error(['2 inputs required, ' num2str(nargin) 'given']) |
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.
error(['2 inputs required, ' num2str(nargin) 'given']) | |
error(['2 inputs required, ' num2str(nargin) ' given']) |
eval(['list = model.get' classname 'List();']); | ||
disp('List creation Successful'); | ||
catch | ||
error(['OpenSim classname, ' classname ', does not exist']); |
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.
It's not necessarily that the classname does not exist, it's that there is no Model::getXList()
.
eval(['list = model.get' classname 'List();']); | ||
disp('List creation Successful'); | ||
catch | ||
error(['OpenSim classname, ' classname ', does not exist']); |
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.
error(['OpenSim classname, ' classname ', does not exist']); | |
error(['Class ' classname ' not supported by osimList']); |
while ~li.equals(list.end()) | ||
if strcmp(char(li.getName()),name) | ||
reference = li.deref; | ||
break |
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.
break | |
return |
end | ||
if ~exist('reference','var') | ||
error(['Component name not found: ' name]) | ||
end |
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.
Could remove the if
here and just create the error
(if you return in the loop).
@chrisdembia and @aseth1, once we agreed on generating a Matlab cell array of references, it was better to turn this into a simple function. Now the user inputs a model and a classname and the function returns a Matlab cell array of references. The user can then make their own decisions on how to deal with the cell array. I added a test (which is more of an example for now) and ran it locally (it passed) |
👍 |
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 great :) I made 2 minor comments. Thanks for dealing with the back and forth.
import org.opensim.modeling.* | ||
|
||
%% Instantiate a model from file | ||
model = Model('../../../../OpenSim/Tests/shared/arm26.osim'); |
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.
Because arm26.osim is copied into the test directory, you can simply use 'arm26.osim'
here. We don't want to read files from the repository in the test case.
import org.opensim.modeling.* | ||
|
||
%% Instantiate a model from file | ||
model = Model('../../../../OpenSim/Tests/shared/arm26.osim'); |
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.
model = Model('../../../../OpenSim/Tests/shared/arm26.osim'); | |
model = Model('arm26.osim'); |
Fixes issue
Deals with #1465 and effects #1436
Brief summary of changes
A Matlab class for doing basic operations with OpenSim Lists. This is a utility for working with lists on Matlab, allowing similar operation as the Sets.
Testing I've completed
Basic operation (does it return the correct things). I would just like to show implementation before building a proper test.
Looking for feedback on...
I have spent a few hours mocking this up and seeing if it is at all feasible. I would like feedback on (i) using Matlab classes instead of a function (ii) the interface, how does the implementation look (seen in testOsimList), (iii) any spec requests (additions or deletions).
I can also make a DevProp if requested?
This change is