Agnostic

Each Object requires an agnostic implementation. With agnostic, we mean agnostic to the type of engine that is used. This concerns for example the action and observation spaces of the Object, which are the same no matter whether the system is simulated or not.

An Object has two abstract classes:

  • agnostic()

  • spec()

Full code is available here.

agnostic

The agnostic() method should be used for defining the information that is agnostic of the engine that is being used. Here we specify what actuators, sensors and states the Object has. An actuator can be used to apply an action in the environment, a sensor can obtain observations, while a state is something that can be reset before starting an episode. In our case, we have three sensors (pendulum_output, action_applied and image), one actuator (pendulum_input) and two states (model_state, model_parameters). We use the model_state to reset the angle and angular velocity of the pendulum during a reset, while we use the model_parameters state to randomize the model parameters over the episodes in order to improve robustness against model inaccuracies. Furthermore, the agnostic() method should be used to define all agnostic config parameters. These are the parameters that are independent of the Engine that is used. We will set the agnostic parameters for each of the actuators, sensors and states, i.e. rates, windows and space converters. The rates define at which rate the callback of that entity is called, window sizes determine the window size for incoming messages, while space converters define how to convert the messages to an OpenAI Gym space. More information on these parameters is available at the API Reference sections on actuators, sensors and states.

# ROS IMPORTS
from std_msgs.msg import Float32MultiArray
from sensor_msgs.msg import Image

# EAGERx IMPORTS
from eagerx_reality.engine import RealEngine
from eagerx_ode.engine import OdeEngine
from eagerx import Object, EngineNode, SpaceConverter, EngineState, Processor
from eagerx.core.specs import ObjectSpec
from eagerx.core.graph_engine import EngineGraph
import eagerx.core.register as register


class Pendulum(Object):
  entity_id = "Pendulum"

  @staticmethod
  @register.sensors(pendulum_output=Float32MultiArray, action_applied=Float32MultiArray, image=Image)
  @register.actuators(pendulum_input=Float32MultiArray)
  @register.engine_states(model_state=Float32MultiArray, model_parameters=Float32MultiArray)
  @register.config(always_render=False, render_shape=[480, 480], camera_index=0)
  def agnostic(spec: ObjectSpec, rate):
      """Agnostic definition of the Pendulum"""
      # Register standard converters, space_converters, and processors
      import eagerx.converters  # noqa # pylint: disable=unused-import

      # Set observation properties: (space_converters, rate, etc...)
      spec.sensors.pendulum_output.rate = rate
      spec.sensors.pendulum_output.space_converter = SpaceConverter.make(
          "Space_AngleDecomposition", low=[-1, -1, -9], high=[1, 1, 9], dtype="float32"
      )

      spec.sensors.action_applied.rate = rate
      spec.sensors.action_applied.space_converter = SpaceConverter.make(
          "Space_Float32MultiArray", low=[-3], high=[3], dtype="float32"
      )

      spec.sensors.image.rate = 15
      spec.sensors.image.space_converter = SpaceConverter.make(
          "Space_Image", low=0, high=1, shape=spec.config.render_shape, dtype="float32"
      )

      # Set actuator properties: (space_converters, rate, etc...)
      spec.actuators.pendulum_input.rate = rate
      spec.actuators.pendulum_input.window = 1
      spec.actuators.pendulum_input.space_converter = SpaceConverter.make(
          "Space_Float32MultiArray", low=[-3], high=[3], dtype="float32"
      )

      # Set model_state properties: (space_converters)
      spec.states.model_state.space_converter = SpaceConverter.make(
          "Space_Float32MultiArray", low=[-3.14159265359, -9], high=[3.14159265359, 9], dtype="float32"
      )

      # Set model_parameters properties: (space_converters) # [J, m, l, b0, K, R, c, a]
      fixed = [0.000189238, 0.0563641, 0.0437891, 0.000142205, 0.0502769, 9.83536, 1.49553, 0.00183742]
      diff = [0, 0, 0, 0.08, 0.08, 0.08, 0.08]  # Percentual delta with respect to fixed value
      low = [val - diff * val for val, diff in zip(fixed, diff)]
      high = [val + diff * val for val, diff in zip(fixed, diff)]
      # low = [1.7955e-04, 5.3580e-02, 4.1610e-02, 1.3490e-04, 4.7690e-02, 9.3385e+00, 1.4250e+00, 1.7480e-03]
      # high = [1.98450e-04, 5.92200e-02, 4.59900e-02, 1.49100e-04, 5.27100e-02, 1.03215e+01, 1.57500e+00, 1.93200e-03]
      spec.states.model_parameters.space_converter = SpaceConverter.make(
          "Space_Float32MultiArray", low=low, high=high, dtype="float32"
      )

Note

Mind the use of the sensors(), actuators() and engine_states() decorators. Registration is required to be able to set the parameters within the ObjectSpec. The config() decorator registers the agnostic configuration parameters of the Object. These agnostic configuration parameters define the signature of the spec() method, which we will see in the next subsection. Also, note that we import eagerx.converters. While it might look like this import is unused, it actually registers the converters from that module, such that we can use them. The Space_Float32MultiArray and Space_Image can therefore be used. The Space_AngleDecomposition space converter can be used because it is imported during initialization of the package in which the object is defined. This space converter is defined here.

spec

The ObjectSpec() specifies how BaseEnv should initialize the object. Here we can for example specify what actuators, sensors and states should be used by default, because this does not necessarily have to be all of them. Per default, we will e.g. use the model_state EngineState only.

@staticmethod
@register.spec(entity_id, Object)
def spec(
    spec: ObjectSpec, name: str, sensors=None, states=None, rate=30, always_render=False, render_shape=None, camera_index=2
):
    """Object spec of Pendulum"""
    # Modify default agnostic params
    # Only allow changes to the agnostic params (rates, windows, (space)converters, etc...
    spec.config.name = name
    spec.config.sensors = sensors if sensors else ["pendulum_output", "action_applied", "image"]
    spec.config.actuators = ["pendulum_input"]
    spec.config.states = states if states else ["model_state"]

    # Add registered agnostic params
    spec.config.always_render = always_render
    spec.config.render_shape = render_shape if render_shape else [480, 480]
    spec.config.camera_index = camera_index

    # Add engine implementation
    Pendulum.agnostic(spec, rate)

Note

Mind the usage of the spec() for initialization of the ObjectSpec. Also, the parameters that were added to the config() (always_render, render_shape, camera_index), become arguments to the spec() method.