bastibe / transplant

Transplant is an easy way of calling Matlab from Python
https://transplant.readthedocs.io
Other
110 stars 26 forks source link

Calling methods on Matlab objects #19

Closed tsdev closed 7 years ago

tsdev commented 7 years ago

I have a user defined matlab object with a method, e.g. myObj.cut(). When I try the following lines in python:

import transplant
mat = transplant.Matlab()
a = mat.myObj()
mat.cut(a)

I get the following error:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/ttt/phys/transplant/transplant_master.py", line 110, in __getattr__
    return self._get_global(name)
  File "/Users/ttt/phys/transplant/transplant_master.py", line 92, in _get_global
    response = self.send_message('get_global', name=name)
  File "/Users/ttt/phys/transplant/transplant_master.py", line 173, in send_message
    response['stack'], response['identifier'], response['message'])
transplant.transplant_master.TransplantError: Undefined variable 'genlattice'. (TRANSPLANT:novariable)
Traceback (most recent call last):
  File "/Users/ttt/phys/transplant/transplant_remote.m", line 96.0, in transplant_remote
    error('TRANSPLANT:novariable' , ...

What would be the right way to call the cut() method? The above script works with the official Matlab Python interface. Also would it be possible to modify transplant, that object methods could be called directly: a.cut() instead of mat.cut(a)?

bastibe commented 7 years ago

As the error message states, some part of your code is calling genlattice. This seems unrelated to the code you show.

That said, for now the only way to call methods is to use the functional form, i.e. mat.cut(a).

The OO form shouldn't be very hard to implement, though. Maybe I'll find the time to do it in the next few weeks.

tsdev commented 7 years ago

BTW, the package is really amazing and thank you for sharing it!

You are right, I changed the method names in the example.

Implementing methods would make calling Matlab scripts more Python native. I guess you could just send the result of the methods(myClass) command from Matlab back to Python and then compare to the Python call. This would make all the standard functions on matrices also callable via methods (even C = A.plus(B) would work then in Python if class(A) is double for example). Then you could even overload standard python operators, such as add, etc. that would make even simpler calls between Matlab objects: C = A + B I guess the only problem with my above examples is that transplant automatically converts any Matlab matrix to numpy array. If there would be a way to tell transplant not to convert matlab arrays, we could call Matlab commands on arrays that don't exist in Python, e.g. accumarray using the simple call (as accumarray is also listed as a method of double): C = A.accumarray(...)

Another related question:

My custom class has some public properties, but I didn't define a get/set method to access them. Would it be possible to add access in transplant to public properties? It should work the same way as methods, you just need the store the output of properties(myClass) as well.

One last question:

I am using a similar package pymatbridge, that enables the direct call of any piece of Matlab code via sending a string and using eval(str) in Matlab. Is similar thing possible in transplant? I am interested because this mode combined the %matlab magic in Jupyter enables to write native Matlab scripts in Jupyter. At present I need both pymatbridge and transplant depending on the task I want to do.

bastibe commented 7 years ago

I guess the more "correct" implementation would return a function handle from get_proxy (object member access). Your problem is actually a result of a peculiarity of Matlab's get, since get_proxy currently gets translated as get(object, name), which works for properties, but fails for methods. It is easy to provide a fail-over for methods, though. Another complication is that Matlab does not provide a function for the @ operator, which means I will have to use eval instead to generate the necessary function handle (always ugly).

The accumarray example would already work, simply by calling mat.accumarray(numpy_array, ...).

Re: your related question:
Public properties can be set and read using normal Python access, i.e. obj.foo = "bar" for a Matlab object obj that has a property foo.

Re: your last question:
You can simply use Matlab's eval function: mat.eval('help disp') works just fine. However, you might want to use mat.evalin('base', 'help disp') instead, to avoid messing with Transplant's internal namespace (this is another problem I can't work around).

tsdev commented 7 years ago

Thanks for clarifying most of my questions. Indeed you can easily access arbitrary class method/property names defined in a string using the obj.('method') and obj.('property') notation. I don't know what Matlab version introduced this, but it works in Matlab R2016b. I also suggest to change calling the get/set methods to the above notation as they are not always defined for user classes (unless they are derived from the matlab.mixin.SetGet superclass).

bastibe commented 7 years ago

I added a few commits that should make your use case possible. There were indeed a few bugs concerning object properties and methods. As it turns out, the previous implementation could not access properties of non-graphical objects.

The current version should allow property access, and enable method(obj) and obj.property. obj.method() does not work yet, and turned out to be more difficult than anticipated. I am working on it, though.

tsdev commented 7 years ago

Thank you for the quick fix! I had a look at the update. The call matlab.method(obj) works, however the method(obj) returns the error: NameError: name 'method' is not defined. Do you have an easy way of debugging transplant in Matlab? At present, even when using the -desktop argument at startup, I cannot access the Matlab window for debugging.

bastibe commented 7 years ago

As of the latest commit, obj.method() should work as well.

bastibe commented 7 years ago

Here is a more complete example I used for testing:

Consider the very simplistic class

classdef testclass
    properties
        foo
    end
    methods
        function obj = testclass(x)
            obj.foo = x;
        end
        function it = giveittome(obj)
            % bar baz
            it = obj.foo;
        end
    end
end

With this in the Matlab path, the following code works:

import transplant
m = transplant.Matlab()
x = m.testclass(42)
print(x) # shows what Matlab would show
print(m.giveittome(x)) # call using function syntax
print(m.giveittome.__doc__)  # documentation still works
print(x.giveittome()) # call using method syntax
print(x.giveittome.__doc__) # documentation still works
bastibe commented 7 years ago

Please do report any errors you find. I found a few of them myself, but I'm sure there are more of them lurking in the code.

bastibe commented 7 years ago

With the latest version, the above code should continue to work, but the implementation was simplified tremendously by moving much of the meta programming to Python. There is a certain performance overhead for this, but it shouldn't be too bad.

bastibe commented 7 years ago

Can you confirm that this fixes your problem?

tsdev commented 7 years ago

It works perfectly!