Source code for blueprints.thing.base

"""
.. note::
	If you found yourself here not being a dev, you should probably :attr:`skip this section <BaseThing.ID>` or 
	look up the correspinging docs page of the Thing you were looking after for which this module only 
	implements its corresponding mirror types. A link to the docs page is to be found in the Types doc 
	string.
"""


import blueprints as blue
from blueprints import restrict

import numpy as np
import inspect
import xml.etree.ElementTree as xml
from copy import copy
from collections import defaultdict



[docs] class BaseThing(blue.ThingType): """ BaseThing implements the basic attributes that are used by all Things. All Things have certain attributes that are included in the mujoco XML construction. The names of those attributes can be obtained from the :attr:`_MUJOCO_ATTR` property which is dictionary with attribute names as keys and the types their value will have in the Thing instance. If a new Thing class is defined that inherits from BaseThing that introduces new attributes to be written to xml, they can be added by defining a class property ``_NEW_MUJOCO_ATTR`` in the same structure that ``_MUJOCO_ATTR`` is defined. ``_MUJOCO_ATTR`` then simply aggregates the new additions from ``_NEW_MUJOCO_ATTR``. If a new Thing class inherits a mujoco attribute, that is no longer used, it can be deleted from ``_MUJOCO_ATTR`` by defining it in a class property ``_DEL_MUJOCO_ATTR``, must be a set of the names to be detached. When a Thing is copied, all attributes needed to reconstruct it are retrieved to create a new instance. For this the :attr:`blueprints.type.ThingType._BLUEPRINT_ATTR` property is used. New attributes that are feed into the inheriting ``Thing.__init__`` are registered in the class property ``_NEW_BLUEPRINTS_ATTR`` and old attributes that are no longer used are registered in ``_DEL_BLUEPRINTS_ATTR``. Their structures are analogous to the ``_MUJOCO_ATTR``. If a Thing gets copied all attributes necessary to instantiate the copy are themselves copied to ensure that the Thing and its copy do not share a reference to the same object in any attribute. However, if an attribute is added to :attr:`blueprints.type.ThingType._NO_COPY_VALS` it is not copied. New attributes to be excluded from being copied can be added by setting the class property ``_NEW_NO_COPY_VALS`` to the classes Type as a set of attribute names. If a Thing is reconstructed from an xml string, some attribute in the xml tag are not attributes of the Thing class. For example, a geom xml tag will have a type defining its shape ``<geom type='sphere'>`` but the corresponding :class:`blueprints.geoms.Sphere` class specifies the type only implicitly in its name. To retrieve the correct class in reconstruction from xml then, those implicit attributes are gathered in the class property :attr:`blueprints.type.ThingType._DERIVED_ATTR` to explicitly derived later. Adding new attributes to it is done by setting the class property ``_NEW_DERIVED_ATTR`` of the Things Type as a set of attribute names. To ensure readability of the resulting xml, attributes that are set to their mujoco defaults are omitted from the xml representations (even though all attributes are set in the Thing instance). To check whether an attributes value equals its default, it can be obtained from the ``_DEFAULT_VALS`` class property, which is structured as a dictionary of attribute names as keys and their defaults as values. New default values of inheriting classes are added analogously to the previous cases by setting them in a class property ``_NEW_DEFAULT_VALS`` of its Type. Parameters ---------- name : str | None, optional This is the name for the object specified by the user. It differs from other user specified properties, in that it might be altered without the users knowledge to resolve name conflicts. parent : blue.NodeThingType | None, optional The parent to which the Thing is attach, if it unattached parent is None. **kwargs The aggregation of keyword arguments that have not yet been caught by other inheriting Things `__init__` are not used and serve as a dummy variable. Attributes ---------- ID : int Each Thing has a unique ID that is gets on initialization from the global REGISTER. """
[docs] @restrict def __init__(self, name: str|None = None, parent: blue.NodeThingType|None = None, **kwargs): """ Parameters ---------- name : str | None, optional This is the name for the object specified by the user. It differs from other user specified properties, in that it might be altered without the users knowledge to resolve name conflicts. parent : blue.NodeThingType | None, optional The parent to which the Thing is attach, if it unattached parent is None. **kwargs The aggregation of keyword arguments that have not yet been caught by other inheriting Things `__init__` are not used and serve as a dummy variable. """ self.ID = blue.REGISTER.get_ID() self._name_scope = None self._name = name or f'anonymous_{self.__class__.__name__.lower()}' self.parent = parent #def __setattr__(self, attr, value): """ If the Thing is attach to a :class:`blueprints.World` it is informed that its child was altered such that on the next method call on the World, that require the current build, that it needs to rebuild. Parameters ---------- attr : str The name of the attribute to be set. value : object The value assigned to the attribute. """
#root = self.root #if isinstance(root, blue.WorldType) and not isinstance(self, blue.WorldType): # root.built = False #super().__setattr__(attr, value) def __str__(self) -> str: """ Returns ------- str The representation of a Thing is the xml tag. Children are not included and not all properties are necessarily represented in the xml tag, either if they are located in an external Thing as in :class:`blueprints.geoms.Mesh` or is they are set to default values in which case they are omitted from xml to ensure readability. """ if hasattr(self, '_MUJOCO_OBJ'): xml_element = xml.Element(self._MUJOCO_OBJ, **self._mujoco_specs()) return xml.tostring(xml_element, encoding='unicode') else: return repr(self) def __repr__(self) -> str: """ Returns ------- str A Thing is represented starting with its class name followed by its name property and its ID. """ return f'{self.__class__.__name__}<{self.name}:{self.ID}>' @restrict def _build(self, parent, world, indicies, **kwargs): """ This method is called to build the xml. Parameters ---------- parent : xml.etree.ElementTree.Element The xml element of its parent world : WorldType The World from which the build method was called initially Returns ------- xml.etree.ElementTree.Element The builded xml element of the Thing. """ self._xml_root = xml.SubElement(parent, self._MUJOCO_OBJ, **self._mujoco_specs(kwargs)) return self._xml_root
[docs] @restrict def copy(self, **kwargs) -> blue.ThingType: """ This method constructs a copy of the Thing with possible alterations to its attributes as specified in kwargs. Parameters ---------- **kwargs Keyword arguments are passed to the Thing.__init__ to replace the attributes of the current Thing from which copy is called. Returns ------- blue.ThingType A new instance of the Thing """ blueprint_specs = self._blueprint_specs() blueprint_specs.update(kwargs) #print('kw', kwargs, blueprint_specs['pos']) if 'name' not in kwargs: blueprint_specs['name'] = self._name thing = self.__class__(**blueprint_specs) return thing
def _clear_step_cache(self): """ Clears the cached properties derived from simulation values after each time step. """ for attr in self._STEP_CACHE(): self.__delattr__(attr) @property def _location_range(self) -> tuple[np.ndarray, np.ndarray]|None: """ This is a dummy function that is used by other Things that have a physical size and location to determine the range of space it occupies. Returns ------- None | None None """ return None @property def _launched(self): """ This property indicates whether the world the Thing belongs to has been build and a simulation is launched. Returns ------- bool """ root = self.root return isinstance(root, blue.WorldType) and root._built # XML INITIALIZATION METHODS @restrict @classmethod def _xml_element_args(cls, xml_element: xml.Element) -> tuple: """ This method extracts the attributes from xml elements and converts them into a type compatible to the Things corresponding attribute. Parameters ---------- xml_element : xml.Element The xml element from which attributes are reconstructed. Returns ------- tuple Three dictionaries are returned, the `init_args` containing arguments passed to :meth:`Thing.__init__`, the `post_args` containing the arguments that are set via :meth:`Thing.__setattr__` after initialization and `rest_args` containing those arguments which must be handled manually. """ init_args = dict() post_args = dict() rest_args = dict() for key, val in xml_element.items(): if key in cls._DERIVED_ATTR(): arg_type = cls._DERIVED_ATTR()[key] rest_args[key] = cls._convert_from_string(val, arg_type) elif key in cls._BLUEPRINT_ATTR(): arg_type = cls._BLUEPRINT_ATTR()[key] init_args[key] = cls._convert_from_string(val, arg_type) elif key in cls._MUJOCO_ATTR(): arg_type = cls._MUJOCO_ATTR()[key] if not val: continue post_args[key] = cls._convert_from_string(val, arg_type) else: rest_args[key] = val return init_args, post_args, rest_args @restrict @classmethod def _from_xml_element(cls, xml_element: xml.Element) -> blue.ThingType: """ This method reconstructs a Thing from an xml element. If any argument for an inheriting class has to be set manually this method must be overwritten. Parameters ---------- xml_element : xml.Element The xml element from which a Thing is reconstructed. Returns ------- blue.ThingType The reconstructed Thing. """ init_args, post_args, rest_args = cls._xml_element_args(xml_element) init_args['copy'] = False obj = object.__new__(cls) obj.__init__(**init_args) for key, val in post_args.items(): setattr(obj, key, val) return obj @restrict def _mujoco_specs(self, specs: dict|None = None) -> dict: """ This method assists the construction of the Things xml representations. It constructs a dictionary containing the names of the attributes as keys and the string representations of their values. Returns ------- dict A dictionary from which the xml tags attributes are build. Parameters ---------- specs : dict | None, optional Specs can be set as a dictionary to overwrite some attributes with custom values. """ specs = specs or {} condition = lambda name, attr: attr is not None and (name not in self._DEFAULT_VALS() or np.any(attr != self._DEFAULT_VALS()[name])) #convert = lambda x: str(x) if not isinstance(x, np.ndarray) else self._numpy_to_string(x) #convert = lambda x: str(x).lower() if not isinstance(x, np.ndarray) else self._numpy_to_string(x) convert = self._convert_to_string mujoco_attrs = map(lambda name: (name, self.__getattribute__(name)), self._MUJOCO_ATTR().keys()) mujoco_specs = {name: convert(attr) for name, attr in mujoco_attrs if condition(name, attr)} conv_specs = {name: convert(attr) for name, attr in specs.items()} mujoco_specs.update(conv_specs) for spec, val in specs.items(): if val is None: del mujoco_specs[spec] return mujoco_specs @restrict def _blueprint_specs(self, specs: dict|None = None) -> dict: """ This method is used to assist the copying of a Thing. It obtains all attributes from the Thing and potentially copies them to avoid multiple Things referencing the same attribute value. Returns ------- dict The returned dictionary has the attribute names as keys and their corresponding values. Parameters ---------- specs : dict | None, optional Specs can be specified as a dictionary to overwrite some attributes with custom values. """ specs = specs or {} #condition = lambda attr, name: True condition = lambda name, attr: attr is not None and (name not in self._DEFAULT_VALS() or np.any(attr != self._DEFAULT_VALS()[name])) convert = lambda name, attr: attr if name in self._NO_COPY_ATTR() else copy(attr) blueprint_attrs = map(lambda name: (name, self.__getattribute__(name)), self._BLUEPRINT_ATTR().keys()) #converted_attrs = {name: convert(name, attr) for name, attr in blueprint_attrs if condition(name, attr)} blueprint_specs = {name: convert(name, attr) for name, attr in blueprint_attrs if condition(name, attr)} #blueprint_specs = {name: attr if name not in self._COPY_BLUE_ATTR else attr.copy() for name, attr in converted_attrs.items()} blueprint_specs.update(specs) for spec, val in specs.items(): if val is None: del blueprint_specs[spec] return blueprint_specs # XML CONVERSION METHODS @restrict @classmethod def _convert_to_string(cls, obj) -> str: """ This method converts the string representation of a value from an xml attribute into its corresponding blueprints attribute. Parameters ---------- string : str value representation dtype : TYPE data type to which the value is to be converted Returns ------- object The returned value can be assigned to the corresponding Things attribute. """ if isinstance(obj, bool): return cls._bool_to_string(obj) elif isinstance(obj, np.ndarray): return cls._numpy_to_string(obj) else: return str(obj) @restrict @classmethod def _convert_from_string(cls, string: str, dtype) -> object: """ This method converts the string representation of a value from an xml attribute into its corresponding blueprints attribute. Parameters ---------- string : str value representation dtype : TYPE data type to which the value is to be converted Returns ------- object The returned value can be assigned to the corresponding Things attribute. """ if dtype == bool: return cls._string_to_bool(string) elif dtype == np.ndarray: return cls._string_to_numpy(string) else: return dtype(string) @restrict @staticmethod def _string_to_numpy(string: str) -> np.ndarray: """ Convertes a string representation of a value to np.ndarray. Parameters ---------- string : str value representation Returns ------- np.ndarray reconstructed np.ndarray """ sep = ',' if ',' in string else ' ' return np.array(list(map(float, string.split(sep)))) @restrict @staticmethod def _numpy_to_string(array: np.ndarray) -> str: """ Convertes a np.ndarray to a string representation of its value. Parameters ---------- array : np.ndarray any np.ndarray Returns ------- str reconstructed string representation """ return str(array) if array.shape == () else ' '.join(map(str, array)) @restrict @staticmethod def _string_to_bool(string: str) -> bool|None: """ Convertes a string representation of a value boolean. Parameters ---------- string : str value representation Returns ------- bool reconstructed boolean Raises ------ ValueError If the string is not 'false', 'true' in any capitalization an Error is raised """ clean_string = string.lower().strip() if clean_string == 'true': return True elif clean_string == 'false': return False elif clean_string == 'auto': return None else: raise ValueError(f"_string_to_bool takes either 'true' or 'false' in any capitalization as argument, got {string}") @restrict @staticmethod def _bool_to_string(value: bool|None) -> str: """ Converts a boolean to a string representation of its value. Parameters ---------- value : bool any boolean value Returns ------- str reconstructed string representation """ if value is None: return 'auto' return str(value).lower() # KINEMATIC TREE PROPERTIES @property def root(self) -> blue.ThingType: """ The root attribute is immutable and changes only if the structure of the kinematic tree is altered. Returns ------- blue.ThingType Retrieves the root of the kinematic tree the Thing in part of, possibly the Thing itself if it has no parent. """ root = self parent = getattr(root, '_parent', None) while parent is not None: root = parent parent = getattr(root, '_parent', None) return root @property def path(self) -> list: """ The path attribute is immutable and changes only if the structure of the kinematic tree is altered. Returns ------- list Retrieves a list of all Things that lead from the Thing itself to its root. """ node = self path = [node] parent = getattr(node, '_parent', None) while parent is not None: node = parent path.append(node) parent = getattr(node, '_parent', None) path.reverse() return path @property def parent(self) -> blue.ThingType | None: """ The parent attribute is mutable. However it is recommended to use the :meth:`NodeThing.attach` method to alter the kinematic tree. Returns ------- blue.ThingType | None If the Thing is attach in a kinematic tree, its parent is returned else None. """ if hasattr(self, '_parent'): return self._parent else: return None @parent.setter @restrict def parent(self, parent: blue.ThingType|None) -> None: """ The parent attribute is mutable. However it is recommended to use the :meth:`NodeThing.attach` method to alter the kinematic tree. Parameters ---------- parent : blue.ThingType | None The parent references another Thing in a kinematic tree to which the Thing is attached. Setting this property manually is generally not recommended since failure to also attach it to its new parent may lead to unintended consequences. Modifications of the kinematic tree should be done via the :meth:`NodeThing.attach` or the :meth:`NodeThing.detach` methods instead. """ self._parent = parent # MUJOCO PROPERTIES @property def name(self) -> str: """ The returned name might differ from the user specification by an enumerations scheme that is applied if two Things of the same type in a kinematic tree share the same name. Returns ------- str possibly extended name of the Thing """ if self._name_scope is not None: return self._name_scope.name(self) else: return self._name @name.setter @restrict def name(self, name: str): """ Parameters ---------- name : str The name assigned to the Thing might be altered by enumeration in the case of a naming conflict. """ if self._name_scope is not None: #self._name_scope.assign(self, name) assert not self.root._built raise Exception("""Name are only allowed to be changed for Things in unbuild Worlds. To change the of this Thing use :meth:`World.unbuild`.""") else: self._name = name # INHERITANCE PROPERTIES @classmethod #@property def _NO_COPY_ATTR(cls) -> dict: """ Returns ------- dict Attributes in this dictionary will not be copied when the Thing is copied. """ if hasattr(cls, '_NEW_NO_COPY_ATTR'): NO_COPY_ATTR = cls._NEW_NO_COPY_ATTR.copy() else: NO_COPY_ATTR = set() for base in cls.__bases__: if hasattr(base, '_NO_COPY_ATTR'): NO_COPY_ATTR.update(base._NO_COPY_ATTR()) return NO_COPY_ATTR @classmethod #@property def _SINGLE_CHILD_ATTR(cls) -> dict: """ Returns ------- dict Attributes in this dictionary will not be copied when the Thing is copied. """ if hasattr(cls, '_NEW_SINGLE_CHILD_ATTR'): SINGLE_CHILD_ATTR = cls._NEW_SINGLE_CHILD_ATTR.copy() else: SINGLE_CHILD_ATTR = dict() for base in cls.__bases__: if hasattr(base, '_SINGLE_CHILD_ATTR'): SINGLE_CHILD_ATTR.update(base._SINGLE_CHILD_ATTR()) return SINGLE_CHILD_ATTR @classmethod #@property def _DERIVED_ATTR(cls) -> dict: """ Returns ------- dict Attributes in this dictionary will have to be derived by the global `REGISTER`. """ if hasattr(cls, '_NEW_DERIVED_ATTR'): DERIVED_ATTR = cls._NEW_DERIVED_ATTR.copy() else: DERIVED_ATTR = dict() for base in cls.__bases__: if hasattr(base, '_DERIVED_ATTR'): DERIVED_ATTR.update(base._DERIVED_ATTR()) return DERIVED_ATTR @classmethod #@property def _STEP_CACHE(cls) -> dict: """ Returns ------- set Attributes in this set are cached for each time step and deleted afterwards. """ if hasattr(cls, '_NEW_STEP_CACHE'): STEP_CACHE = cls._NEW_STEP_CACHE.copy() else: STEP_CACHE = set() for base in cls.__bases__: if hasattr(base, '_STEP_CACHE'): STEP_CACHE.update(base._STEP_CACHE()) return STEP_CACHE @classmethod #@property def _MUJOCO_ATTR(cls) -> dict: """ Returns ------- dict Attributes from this dictionary are used to construct the xml representation. """ if hasattr(cls, '_NEW_MUJOCO_ATTR'): MUJOCO_ATTR = cls._NEW_MUJOCO_ATTR.copy() else: MUJOCO_ATTR = dict() for base in cls.__bases__: if hasattr(base, '_MUJOCO_ATTR'): MUJOCO_ATTR.update(base._MUJOCO_ATTR()) if hasattr(cls, '_DEL_MUJOCO_ATTR'): for attr in cls._DEL_MUJOCO_ATTR: if attr in MUJOCO_ATTR: del MUJOCO_ATTR[attr] return MUJOCO_ATTR @classmethod #@property def _BLUEPRINT_ATTR(cls) -> dict: """ Returns ------- dict Attributes from this dictionary are used to copy the Thing. """ if hasattr(cls, '_NEW_BLUEPRINT_ATTR'): BLUEPRINT_ATTR = cls._NEW_BLUEPRINT_ATTR.copy() else: BLUEPRINT_ATTR = dict() for base in cls.__bases__: if hasattr(base, '_BLUEPRINT_ATTR'): BLUEPRINT_ATTR.update(base._BLUEPRINT_ATTR()) if hasattr(cls, '_DEL_BLUEPRINT_ATTR'): for attr in cls._DEL_BLUEPRINT_ATTR: if attr in BLUEPRINT_ATTR: del BLUEPRINT_ATTR[attr] return BLUEPRINT_ATTR @classmethod #@property def _DEFAULT_VALS(cls) -> dict: """ Returns ------- dict This dictionary stores the default values of all attributes. """ if hasattr(cls, '_NEW_DEFAULT_VALS'): DEFAULT_VALS = cls._NEW_DEFAULT_VALS.copy() else: DEFAULT_VALS = dict() for base in cls.__bases__: if hasattr(base, '_DEFAULT_VALS'): DEFAULT_VALS.update(base._DEFAULT_VALS()) return DEFAULT_VALS