Engine Graph
In this section we will discuss the concept of the EngineGraph.
We will do this by going through an example.
In this case, we will construct the EngineGraph for the OdeEngine. implementation within the Pendulum Object.
The EngineGraph defines how the nodes of type EngineNode are connected to eachother within an engine-specific implementation of an Object.
Constructing the EngineGraph
The EngineGraph defines how the nodes of type EngineNode are connected to eachother within an engine-specific implementation of an Object.
Therefore, it should be constructed within an engine-specific implementation of an :class:`~eagerx.core.entities.Object.
In this case, we will construct an EngineGraph with three sensors, i.e. pendulum_output, image and action_applied and one actuator, i.e. pendulum_input.
@staticmethod
@register.engine(entity_id, OdeEngine) # This decorator pre-initializes engine implementation with default object_params
def ode_engine(spec: ObjectSpec, graph: EngineGraph):
"""Engine-specific implementation (OdeEngine) of the object."""
# Import any object specific entities for this engine
import eagerx_dcsc_setups.pendulum.ode # noqa # pylint: disable=unused-import
# Set object arguments (nothing to set here in this case)
spec.OdeEngine.ode = "eagerx_dcsc_setups.pendulum.ode.pendulum_ode/pendulum_ode"
# Set default params of pendulum ode [J, m, l, b0, K, R, c, a].
spec.OdeEngine.ode_params = [0.000189238, 0.0563641, 0.0437891, 0.000142205, 0.0502769, 9.83536, 1.49553, 0.00183742]
# Create engine_states (no agnostic states defined in this case)
spec.OdeEngine.states.model_state = EngineState.make("OdeEngineState")
spec.OdeEngine.states.model_parameters = EngineState.make("OdeParameters", list(range(7)))
# Create sensor engine nodes
obs = EngineNode.make("OdeOutput", "pendulum_output", rate=spec.sensors.pendulum_output.rate, process=2)
image = EngineNode.make(
"PendulumImage", "image", shape=spec.config.render_shape, rate=spec.sensors.image.rate, process=0
)
# Create actuator engine nodes
action = EngineNode.make(
"OdeInput", "pendulum_actuator", rate=spec.actuators.pendulum_input.rate, process=2, default_action=[0]
)
# Connect all engine nodes
graph.add([obs, image, action])
graph.connect(source=obs.outputs.observation, sensor="pendulum_output")
graph.connect(source=obs.outputs.observation, target=image.inputs.theta)
graph.connect(source=image.outputs.image, sensor="image")
graph.connect(actuator="pendulum_input", target=action.inputs.action)
# Add action applied
applied = EngineNode.make("ActionApplied", "applied", rate=spec.sensors.action_applied.rate, process=0)
graph.add(applied)
graph.connect(source=action.outputs.action_applied, target=applied.inputs.action_applied, skip=True)
graph.connect(source=applied.outputs.action_applied, sensor="action_applied")
Note
Mind the usage of the engine() decorator.
Also, we want to point out that the API for creating the EngineGraph is similar to the one from Graph.
Visualization and Validation
We can use the GUI to inspect the EngineGraph.
This can be done by calling the gui() method:
graph.gui()
Also, after using the make() method to make an object, we can visualize the EngineGraph, using the gui() method:
import eagerx
import eagerx_dcsc_setups
pendulum = eagerx.Object.make("Pendulum", "pendulum")
pendulum.gui(engine_id="OdeEngine")
Note
We have to call the gui() method with the argument engine_id, since an Object can have implementations for more than one Engine, where each has its own EngineGraph.
When clicking Show Graph, the output should look similar to the image below:
The EngingeGraph for the OdeEngine of the Pendulum Object.
Here we can see three sensors (pendulum_output, action_applied, image) and one actuator (pendulum_input).
Note that each EngineNode with the input tick is synchronized with the Engine.
We can also check whether the EngineGraph is valid by clicking Check Validity.
Among other things, this checks whether the graph is a directed acyclical graph (DAG).
We can perform the same check using the is_valid() method.