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

Allow to set a robot's base (RCF) #762

Closed
wants to merge 10 commits into from
4 changes: 3 additions & 1 deletion src/compas/robots/base_artist/_artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from compas.geometry import Scale
from compas.geometry import Transformation
from compas.robots import Geometry

from compas.utilities import add_observer

__all__ = [
'BaseRobotModelArtist'
Expand Down Expand Up @@ -76,6 +76,8 @@ def __init__(self, model):
self.scale_factor = 1.
self.attached_tool_model = None

add_observer(self.model, 'updated_transformations', lambda _ctx: self.create())

def attach_tool_model(self, tool_model):
"""Attach a tool to the robot artist.

Expand Down
15 changes: 10 additions & 5 deletions src/compas/robots/model/robot.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from compas.robots.model.link import Visual
from compas.robots.resources import DefaultMeshLoader
from compas.topology import shortest_path

from compas.utilities import observable

__all__ = ['RobotModel']

Expand Down Expand Up @@ -79,7 +79,7 @@ def __init__(self, name, joints=[], links=[], materials=[], **kwargs):
self.root = None
self._rcf = None
self._rebuild_tree()
self._create(self.root, self._root_transformation)
self.update_transformations()
self._scale_factor = 1.

def get_urdf_element(self):
Expand Down Expand Up @@ -607,7 +607,7 @@ def rcf(self, frame):
else:
self._rcf = frame.copy()

self._create(self.root, self._root_transformation)
self.update_transformations()

@property
def _root_transformation(self):
Expand Down Expand Up @@ -793,6 +793,11 @@ def scale(self, factor, link=None):

self._scale_factor = factor

@observable(event_name='updated_transformations')
def update_transformations(self):
""""""
self._create(self.root, self._root_transformation)

def compute_transformations(self, joint_state, link=None, parent_transformation=None):
"""Recursive function to calculate the transformations of each joint.

Expand Down Expand Up @@ -1004,7 +1009,7 @@ def add_link(self, name, visual_meshes=None, visual_color=None, collision_meshes
# Must build the tree structure, if adding the first link to an empty robot
if len(self.links) == 1:
self._rebuild_tree()
self._create(self.root, self._root_transformation)
self.update_transformations()

return link

Expand Down Expand Up @@ -1095,7 +1100,7 @@ def add_joint(self, name, type, parent_link, child_link, origin=None, axis=None,
self._joints[joint.name] = joint
self._adjacency[joint.name] = [child_link.name]

self._create(self.root, self._root_transformation)
self.update_transformations()

return joint

Expand Down
140 changes: 130 additions & 10 deletions src/compas/utilities/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
import functools
import pstats

from functools import wraps

try:
from cStringIO import StringIO
except ImportError:
Expand All @@ -25,7 +23,10 @@
'abstractstaticmethod',
'abstractclassmethod',
'memoize',
'print_profile'
'print_profile',
'observable',
'add_observer',
'remove_observer',
]


Expand All @@ -46,7 +47,6 @@ def __init__(self, function):
function.__isabstractmethod__ = True
super(abstractstaticmethod, self).__init__(function)


class abstractclassmethod(classmethod):
"""Decorator for declaring a class method abstract.

Expand Down Expand Up @@ -117,7 +117,7 @@ def f(n):
print(f.__name__)

"""
@wraps(func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
profile = Profile.Profile()
profile.enable()
Expand All @@ -136,9 +136,129 @@ def wrapper(*args, **kwargs):
return wrapper


# ==============================================================================
# Main
# ==============================================================================
def _get_event_key(event_source, event_name):
return '/{}/{}'.format(event_source.__class__.__name__, event_name)


def observable(observable_method=None, event_name=None, *args, **kwargs):
"""Decorator to mark a property or method of a class as observable.

Observable methods/properties send notifications (events) that
other code can listen to and act when they are triggered. This
is useful to decouple classes where a class B depends on events
occurring on class A, but class A should not be in charge of
actively track and update class B on those events.

Notes
-----
This decorator should only be applied to methods and properties
of a class, not to stand-alone functions.

Examples
--------
>>> class Test(object):
... @observable(event_name='init')
Copy link
Member

Choose a reason for hiding this comment

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

I trust myself to type 'init' correctly, but I do not trust myself to type 'updated_transformations' correctly. I know this is what tests are for, but somethings are difficult to test (say, did the image of the robot in blender update correctly when the rcf changed?). Is there a nice way of throwing an exception somewhere if the observable event names for a class don't match up with the observed event names or vice versa? or is that something that shouldn't be protected against?

... def init(self):
... print('init')
...
>>> def init_observer(ctx):
... print('inside observer')
...
>>> t = Test()
>>> t.init()
init
>>> add_observer(t, 'init', init_observer)
>>> t.init()
init
inside observer
>>> remove_observer(t, 'init', init_observer)
>>> t.init()
init

Parameters
----------
observable_method
The method that is to be declared observable.
event_name : [type], optional
The name of the event that will be triggered when the method/property is invoked.
"""
def observable_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
_self = args[0] if len(args) else None
if not _self:
raise Exception('Cannot make an observable without a containing class')

event_observers = None
event_key = _get_event_key(_self, event_name)

if hasattr(_self, '__event_observers__'):
event_observers = _self.__event_observers__.get(event_key)

return_value = func(*args, **kwargs)

if event_observers:
for observer in event_observers:
ctx = EventContext(event_name, *args, **kwargs)
observer(ctx)

return return_value
return wrapper
if observable_method is None:
return observable_decorator
else:
return observable_decorator(observable_method)


def add_observer(event_source, event_name, callback):
"""Add an observer to an event in a class containing observable methods/properties.

Parameters
----------
event_source
Object containing ``@observable`` instances.
event_name : str
Name of the event to listen to.
callback : function
Function to execute every time the specified event is fired.
"""
if not hasattr(event_source, '__event_observers__'):
event_source.__event_observers__ = {}

event_key = _get_event_key(event_source, event_name)
observers = event_source.__event_observers__.get(event_key, set())
observers.add(callback)

event_source.__event_observers__[event_key] = observers


def remove_observer(event_source, event_name, callback):
"""Remove an observer from an event in a class containing observable methods/properties.

Parameters
----------
event_source
Object containing ``@observable`` instances.
event_name : str
Name of the event to listen to.
callback : function
Function to execute every time the specified event is fired.
"""
if not hasattr(event_source, '__event_observers__'):
return

event_key = _get_event_key(event_source, event_name)
event_observers = event_source.__event_observers__.get(event_key)

if event_observers:
event_observers.remove(callback)
if not event_observers:
del event_source.__event_observers__[event_key]
Copy link
Member

Choose a reason for hiding this comment

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

I see a lot of potential for exceptions being raised with this function. If there is a typo in event_name, there will be a KeyError. If the wrong callback is passed there could be a ValueError. Does it make sense to raise custom exceptions for this, rather than the generic and perhaps mysterious ones?



if __name__ == "__main__":
pass
class EventContext(object):
"""Provides context for the observers of an event."""
def __init__(self, event_name, *args, **kwargs):
self.event_name = event_name
self.args = args
self.kwargs = kwargs
Loading