Skip to content
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

Merged
merged 15 commits into from
Oct 19, 2018
Merged

Conversation

jimmyDunne
Copy link
Member

@jimmyDunne jimmyDunne commented Aug 1, 2017

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 Reviewable

@jimmyDunne jimmyDunne changed the title {WIP} Iterator Matlab Class for easier access [WIP] Iterator Matlab Class for easier access Aug 1, 2017
@tkuchida
Copy link
Member

tkuchida commented Aug 1, 2017

An interface like this could be really helpful for MATLAB users who are unfamiliar with C++ iterators. I'm wondering whether get() would be slow if, e.g., used with a large model or within an optimizer. For example, if scale factors were being set on every Body in the Model, then get() would be performing n(n+1)/2 string comparisons and calls to eval() (which is notoriously slow) on each iteration. One strategy to avoid this would be to cache a map between the string and a reference (UUIDs would be useful here 😉)—though, to avoid using out-of-date information, the struct would then need to detect changes to the Model and regenerate the map.

Copy link
Member

@chrisdembia chrisdembia left a 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:

  1. Change get() to take an index rather than a name, thereby allowing one to use this class to iterate through components.
  2. Perhaps add a getByName() but this use should be discouraged.
  3. Add a separate function to Utilities named osimGetComponent() or osimGetComp() etc. that fetches a single component and downcasts it for you. The signature would be osimGetComponent(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])
Copy link
Member

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.

Bindings/Java/Matlab/Utilities/osimList.m Outdated Show resolved Hide resolved
while ~li.equals(list.end())
if strcmp(char(li.getName()),name)
pathname = li.getAbsolutePathName();
comp = model.getComponent(pathname);
Copy link
Member

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.

Copy link
Member

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();").

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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.

@aymanhab
Copy link
Member

aymanhab commented Aug 3, 2017

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.

@tkuchida
Copy link
Member

tkuchida commented Aug 3, 2017

We should create methods to build Sets (or any other preferred container) of proper type and return them.

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 Model::getNumBodies() returns the size of the BodySet, so if we're discouraging use of the BodySet because it might contain incomplete information, Model::getNumBodies() shouldn't be using it.

@aymanhab
Copy link
Member

aymanhab commented Aug 3, 2017

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.

@tkuchida
Copy link
Member

tkuchida commented Aug 3, 2017

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 getNumBodies(), etc. Might be good to update those methods.

@jimmyDunne jimmyDunne self-assigned this Oct 10, 2018
@scrum2a scrum2a added this to the 20181001 milestone Oct 10, 2018
@chrisdembia chrisdembia modified the milestones: 20181001, 20181015 Oct 12, 2018
@jimmyDunne
Copy link
Member Author

@aseth1, if you could review and let me know. I would like to write some documentation for using lists— perhaps we could chat (Zoom)?.

@aymanhab
Copy link
Member

aymanhab commented Oct 16, 2018

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).

@aymanhab
Copy link
Member

@jimmyDunne deref() method added, and should show up for all iterators, please let me know if that works for you.

@jimmyDunne
Copy link
Member Author

jimmyDunne commented Oct 17, 2018

Thanks, @aymanhab! That made the Matlab implementation doable.

This seems to work well now.

@jimmyDunne
Copy link
Member Author

@aseth1, should be good for a re-review 👍

Copy link
Member

@aseth1 aseth1 left a 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)
Copy link
Member

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;
Copy link
Member

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)
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

@jimmyDunne jimmyDunne Oct 18, 2018

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
Copy link
Member

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.

Copy link
Member

@chrisdembia chrisdembia left a 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'
Copy link
Member

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'])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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']);
Copy link
Member

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']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
break
return

end
if ~exist('reference','var')
error(['Component name not found: ' name])
end
Copy link
Member

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).

@jimmyDunne
Copy link
Member Author

@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)

@aseth1
Copy link
Member

aseth1 commented Oct 19, 2018

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.

👍

Copy link
Member

@chrisdembia chrisdembia left a 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');
Copy link
Member

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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
model = Model('../../../../OpenSim/Tests/shared/arm26.osim');
model = Model('arm26.osim');

@jimmyDunne jimmyDunne merged commit 083fcc4 into master Oct 19, 2018
@jimmyDunne jimmyDunne deleted the matlabScripts_iterators branch October 19, 2018 20:01
@jimmyDunne jimmyDunne changed the title [WIP] Iterator Matlab Class for easier access Iterator Matlab Class for easier access Oct 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants