Source code for blueprints.agent

"""
To make blueprints amneable for multi agent settings the :class:`Agent` class provides easily structured
access to the relevant Things for observation (:class:`Sensors <blueprints.sensors.BaseSensor>` and :class:`Cameras <blueprints.Camera>`) 
and action (:class:`Actuators <blueprints.actuators.General>`).

Agents are used similar to :class:`Bodies <blueprints.bodies.Body>` with some minor differences.
The child attributes for normal Things map only to direct children, but for an :class:`Agent` instance 
the attributes :attr:`actuators <Agent.actuators>`, :attr:`sensors <Agent.sensors>` and :attr:`cameras <Agent.cameras>` 
map to all descendants as a more intuitive shortcut.
All observations from Sensors and Cameras are bundeled in an :attr:`observation <Agent.observation>` attribute and 
all actions can be applied through the :attr:`force <Agent.force>` and :attr:`activation <Agent.activation>` attribute.

.. code-block:: mxml
	:caption: Agent XML structure
	
	<sensors>
		<force site='site_1'>
		<force site='site_2'>
	</sensors>
	<actuators>
		<general body='AGENT:body_1' dynprm='filter'>
		<general body='body_2'>
	</actuators>
	<body name='AGENT:body_1'>
		<body name='body_2'>
			<site name='site_1'>
		</body>
		<site name='site_2'>
		<camera name='cam'>
		<joint name='joint'>
	</body>

.. code-block::
	:caption: Agent Interface

	>>> agent.observation
	{'<Camera>cam': np.ndarray([...], shape=(width, height)), 
	 '<Force>_(0)': np.ndarray([0.]), 
	 '<Force>_(1)': np.ndarray([0.])}
	>>> agent.action_shape
	{'force': 1, 
	 'activation': 1}
	>>> agent.force = np.darray([...])
	>>> agent.activation = = np.darray([...])



Additionally shapes for observations and actions are available as attributes as well.
"""

import blueprints as blue
import numpy as np



[docs] class Agent(blue.AgentType, blue.Body): """ This class emulates a Body which serves as an interface for sensors, cameras and actuators of an Agent. It provides uniform access to the observation and action relevant Things. """ def __str__(self) -> str: """ Returns ------- str A representation of the Agent. The xml will be parsed as :class:`Body <blue.bodies.Body>`. """ return f'<agent/body name="{self.name}">' @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 f"AGENT:{self._name_scope.name(self).replace('AGENT:', '')}" else: return f"AGENT:{self._name.replace('AGENT:', '')}" @blue.restrict @classmethod def _from_xml_element(cls, xml_element, actuators: list = []) -> blue.ThingType: init_args, post_args, rest_args = cls._xml_element_args(xml_element) init_args['copy'] = False # Strip AGENT: prefix — the name property re-adds it name = post_args.pop('name', None) if name is not None: name = name.replace('AGENT:', '') init_args['name'] = name agent = object.__new__(cls) agent.__init__(**init_args) for key, val in post_args.items(): setattr(agent, key, val) for actuator in actuators: agent.attach(actuator, copy=False) actuator.body = agent return agent # AGENT PROPERTIES @property def cameras(self) -> blue.ViewType: """ Instead of returning just the children all descending Cameras are returned. Returns ------- View The view is routed to all cameras of the agent (instead of just those that follow directly in the kinematic hierarchy). """ cameras = self.descendants['cameras']['descendants'] return blue.View(cameras, 'cameras', self) @property def sensors(self) -> blue.ViewType: """ Instead of returning just the children all descending Sensors are returned. Returns ------- View The view is routed to all sensors of the agent (instead of just those that follow directly in the kinematic hierarchy). """ sensors = self.descendants['sensors']['descendants'] return blue.View(sensors, 'sensors', self) @property def actuators(self) -> blue.ViewType: """ Instead of returning just the children all descending Actuators are returned. Returns ------- View The view is routed to all actuators of the agent (instead of just those that follow directly in the kinematic hierarchy). """ actuators = self.descendants['actuators']['descendants'] return blue.View(actuators, 'actuators', self) @property def sensor_observation_shape(self) -> dict: """ The dictionary contains the names of all sensors and their shapes. Returns ------- dict """ if not self.root._built: raise Exception('Agent.observation_shape is only accessable after the World has been built.') return {f'<{sensor.__class__.__name__}>{sensor.name}': (sensor.DIMENSIONS,) \ for sensor in self.descendants['sensors']['descendants']} @property def camera_observation_shape(self) -> dict: """ The dictionary contains the names of all cameras and their shapes. Returns ------- dict """ if not self.root._built: raise Exception('Agent.observation_shape is only accessable after the World has been built.') return {f'<{camera.__class__.__name__}>{camera.name}': tuple(camera.resolution) \ for camera in self.descendants['cameras']['descendants']} @property def observation_shape(self) -> dict: """ The dictionary contains the names of all observables and their shapes. Returns ------- dict """ if not self.root._built: raise Exception('Agent.observation_shape is only accessable after the World has been built.') cameras = self.camera_observation_shape sensors = self.sensor_observation_shape return {**cameras, **sensors} @property def sensor_observation(self) -> dict: """ The dictionary contains the name of all sensor observables and their data. Returns ------- dict """ if not self.root._built: raise Exception('Agent.observation is only accessable after the World has been built.') return {f'<{sensor.__class__.__name__}>{sensor.name}': sensor.observation \ for sensor in self.descendants['sensors']['descendants']} @property def camera_observation(self) -> dict: """ The dictionary contains the name of all camera observables and their data. Returns ------- dict """ if not self.root._built: raise Exception('Agent.observation is only accessable after the World has been built.') return {f'<{camera.__class__.__name__}>{camera.name}': camera.observation \ for camera in self.descendants['cameras']['descendants']} @property def observation(self) -> dict: """ The dictionary contains the name of all observables and their data. Returns ------- dict """ if not self.root._built: raise Exception('Agent.observation is only accessable after the World has been built.') cameras = self.camera_observation sensors = self.sensor_observation return {**cameras, **sensors} @property def activation(self) -> list: """ All activations of Actuators that have an activation state. Actuators with ``'dyntype' == 'none'`` do not have an activation. Returns ------- list """ activation = self.actuators.activation return [x for x in activation if x is not None] @activation.setter @blue.restrict def activation(self, activation: list[int|float]|np.ndarray) -> None: """ A 1-D vector of activations for the actuators activation states. Parameters ---------- activation : list[int | float] | np.ndarray """ actuators = self.actuators mask = [x is not None for x in activation] for i, (x, m) in enumerate(zip(activation, mask)): if m: actuators[i] = x @property def force(self) -> list: """ All forces of Actuators of the Agent Returns ------- list """ return self.actuators.force @force.setter @blue.restrict def force(self, force: list[int|float]|np.ndarray) -> None: """ Parameters ---------- force : list[int | float] | np.ndarray A 1-D vector of forces to be applied to the actuators """ actuators = self.actuators for i, f in enumerate(force): actuators[i].force = float(f) @property def action_shape(self) -> dict: """ The shapes of both action types (activations and forces) Returns ------- dict """ if not self.root._built: raise Exception('Agent.action_shape is only accessable after the World has been built.') activation = self.activation force = self.force return {'activation': len(activation), 'force': len(force)}