How to configure an experiment
In this section, we will provide an explanation in how to use the YAML and launch files in order to configure our experiments.
These files appear in other How To Do sections, but here we will do a complete explanation of both.
YAML files
The YAML file defines the parameters of the experiment that determine the behavior of the cognitive architecture. There are two files to provide: The commander configuration file and the
Commander configuration file
This file, commander.yaml, is located in the core package, in the config directory (core/config). Here it’s possible to decide the number of execution nodes that the commander is going to create and the number of threads of each one. By default, there are 5 execution nodes with 1 thread for each one:
Commander:
ExecutionNode:
-
threads: 1
-
threads: 1
-
threads: 1
-
threads: 1
-
threads: 1
Experiment configuration file
This file contains the startup configuration that the commander will apply. It includes configurations for the cognitive processes to launch, default node connectors and the initial nodes in the architecture. Additionally, it can contain experiment-specific configurations such as the required for the simulators.
Cognitive processes:
We can select the cognitive processes to execute and define their parameters.
Currently, we only have one cognitive process: the main loop, which executes the classical e-MDB loop: reading perceptions, calculation of activations (determining relevant contexts), selecting policies, and executing policies.
Here we can define the following:
Experiment:
name: main_loop
class_name: cognitive_processes.main_loop.MainLoop
new_executor: True
threads: 2
parameters:
iterations: 10000
trials: 20
subgoals: False
softmax_selection: True
softmax_temperature: 0.3
kill_on_finish: True
The relevant parameters that we can change are the number of iterations of the experiment and the number of trials, or policies executed, before resetting the simulated world or the real environment to prevent the experiment from getting stuck. Additional parameters include the option to use a probabilistic selection of policies and killing the architecture processes once the iterations have been reached (Useful for batch executions).
Control channel:
If we need a control channel that communicates the cognitive architecture with another module, such as a simulator or real robot, we can configure that:
Control:
id: ltm_simulator
control_topic: /main_loop/control
control_msg: cognitive_processes_interfaces.msg.ControlMsg
episodes_topic: /main_loop/episodes
episodes_msg: cognitive_processes_interfaces.msg.Episode
executed_policy_service: /emdb/simulator/executed_policy
executed_policy_msg: cognitive_node_interfaces.srv.Policy
world_reset_service: /emdb/simulator/world_reset
world_reset_msg: cognitive_processes_interfaces.srv.WorldReset
Here we can configure the interfaces names and message types to communicate with the architecture. This includes the current iteration of the experiment, the episodes generated and the services for policy execution and world resets.
LTM elements:
We can decide the elements that the cognitive architecture will create when it’s launched or during execution:
Files:
We can decide the output files that the architecture will generate and in which we can check the results of the experiment.
LTM:
Files:
-
id: goodness
class: core.file.FileGoodness
file: goodness.txt
-
id: pnodes_success
class: core.file.FilePNodesSuccess
file: pnodes_success.txt
By default, we use the **pnodes_success.txt**, in which we can see the points and anti-points added in each P-Node, and the **goodness.txt**, in which we can see the policy executed in one iteration, the reward obtained, if there have been sensory changes, and more.
More files could be configured in the *file.py* script, see the [API reference](https://docs.pillar-robots.eu/projects/emdb_core/en/latest/core/as_API.html#file) for full details.
Cognitive Nodes:
We can add Cognitive Nodes that will be in the LTM from the beginning of the experiment. We have to specify the node type, its name, the class to use, and its parameters:
LTM:
Nodes:
Perception:
-
name: ball_in_left_hand
class_name: cognitive_nodes.perception.DiscreteEventSimulatorPerception
parameters:
default_msg: std_msgs.msg.Bool
default_topic: /mdb/baxter/sensor/ball_in_left_hand
-
name: ball_in_right_hand
class_name: cognitive_nodes.perception.DiscreteEventSimulatorPerception
parameters:
default_msg: std_msgs.msg.Bool
default_topic: /mdb/baxter/sensor/ball_in_right_hand
-
name: boxes
class_name: cognitive_nodes.perception.DiscreteEventSimulatorPerception
parameters:
default_msg: simulators_interfaces.msg.ObjectListMsg
default_topic: /mdb/baxter/sensor/boxes
normalize_data:
distance_min: 0.2
distance_max: 1.9
angle_min: -1.4
angle_max: 1.4
diameter_min: 0.0
diameter_max: 0.15
-
name: cylinders
class_name: cognitive_nodes.perception.DiscreteEventSimulatorPerception
parameters:
default_msg: simulators_interfaces.msg.ObjectListMsg
default_topic: /mdb/baxter/sensor/cylinders
normalize_data:
distance_min: 0.2
distance_max: 1.9
angle_min: -1.4
angle_max: 1.4
diameter_min: 0.0
diameter_max: 0.15
WorldModel:
-
name: GRIPPER_AND_LOW_FRICTION
class_name: cognitive_nodes.world_model.WorldModel
RobotPurpose:
-
name: object_in_box_mission
class_name: cognitive_nodes.robot_purpose.RobotPurpose
parameters:
weight: 1.0
drive_id: 'object_in_box_drive'
purpose_type: 'Mission'
terminal: True
-
name: novelty_need
class_name: cognitive_nodes.robot_purpose.RobotPurpose
parameters:
weight: 0.2
drive_id: 'novelty_drive'
purpose_type: 'Need'
Drive:
-
name: object_in_box_drive
class_name: cognitive_nodes.drive.DriveExponential
parameters:
input_topic: /mdb/baxter/sensor/progress
input_msg: std_msgs.msg.Float32
min_eval: 0.8
neighbors: [{"name": "object_in_box_mission", "node_type": "RobotPurpose"}]
-
name: novelty_drive
class_name: cognitive_nodes.novelty.DriveNovelty
parameters:
neighbors: [{"name": "novelty_need", "node_type": "RobotPurpose"}]
Connectors:
The main loop cognitive process can create Cognitive Nodes during execution. To do that, it has to know which class it has to use to create each one. That can be configured in this file:
LTM:
Connectors:
-
data: Space
default_class: cognitive_nodes.space.SVMSpace
-
data: Perception
default_class: cognitive_nodes.perception.Perception
-
data: PNode
default_class: cognitive_nodes.pnode.PNode
-
data: CNode
default_class: cognitive_nodes.cnode.CNode
-
data: Goal
default_class: cognitive_nodes.goal.Goal
-
data: WorldModel
default_class: cognitive_nodes.world_model.WorldModel
-
data: Policy
default_class: cognitive_nodes.policy.Policy
Others:
We can add all the parameters that will be needed in our experiment:
SimulatedBaxter:
Perceptions:
-
name: cylinders
perception_topic: /mdb/baxter/sensor/cylinders
perception_msg: simulators_interfaces.msg.ObjectListMsg
-
name: boxes
perception_topic: /mdb/baxter/sensor/boxes
perception_msg: simulators_interfaces.msg.ObjectListMsg
-
name: ball_in_left_hand
perception_topic: /mdb/baxter/sensor/ball_in_left_hand
perception_msg: std_msgs.msg.Bool
-
name: ball_in_right_hand
perception_topic: /mdb/baxter/sensor/ball_in_right_hand
perception_msg: std_msgs.msg.Bool
-
name: progress_ball_in_box
perception_topic: /mdb/baxter/sensor/progress
perception_msg: std_msgs.msg.Float32
You can check complete YAML files in the /experiments directory of the experiments package in the emdb_experiments repository.
Launch file
This file launches the processes needed for the operation of the cognitive architecture and defines their input parameters.
The launch file is divided into two Python functions:
Launch setup:
Here we have to put the input arguments and parameters that the execution nodes will take. Also, we have to put the essential execution nodes to launch the architecture (commander, ltm) and the other ones that launch our specific modules, such as the Discrete Event Simulator, for example.
def launch_setup(context: LaunchContext, *args, **kwargs):
logger = LaunchConfiguration("log_level")
random_seed = LaunchConfiguration("random_seed")
experiment_file = LaunchConfiguration("experiment_file")
experiment_package = LaunchConfiguration("experiment_package")
config_package = LaunchConfiguration("config_package")
config_file = LaunchConfiguration("config_file")
core_node = Node(
package="core",
executable="commander",
output="screen",
arguments=["--ros-args", "--log-level", logger],
parameters=[{"random_seed": random_seed}],
)
ltm_node = Node(
package="core",
executable="ltm",
output="screen",
arguments=["0", "--ros-args", "--log-level", logger],
)
simulator_node = Node(
package="simulators",
executable="fruit_shop_simulator",
output="screen",
parameters=[
{
"random_seed": random_seed,
"config_file": PathJoinSubstitution(
[FindPackageShare(experiment_package), "experiments", experiment_file]
),
}
],
)
config_service_call = ExecuteProcess(
cmd=[
[
FindExecutable(name="ros2"),
" ",
"service call",
" ",
"commander/load_config",
" ",
"core_interfaces/srv/LoadConfig",
" ",
'"{file:',
" ",
PathJoinSubstitution(
[FindPackageShare(config_package), "config", config_file]
),
'}"',
]
],
shell=True,
)
shutdown_on_exit = RegisterEventHandler(
OnProcessExit(
target_action=core_node,
on_exit=[Shutdown()],
)
)
nodes_to_start = [config_service_call, core_node, ltm_node, simulator_node, shutdown_on_exit]
return nodes_to_start
Generate launch description:
Here we have to declare the input arguments and parameters, indicating their name, which has to be the same as the put in the function above; their default value, and their description.
def generate_launch_description():
declared_arguments = []
declared_arguments.append(
DeclareLaunchArgument(
"log_level",
default_value=["info"],
description="Logging level",
)
)
declared_arguments.append(
DeclareLaunchArgument(
"random_seed",
default_value="0",
description="The seed to the random numbers generator",
)
)
declared_arguments.append(
DeclareLaunchArgument(
"experiment_file",
default_value="fruit_shop_experiment.yaml",
description="The file that loads the experiment config",
)
)
declared_arguments.append(
DeclareLaunchArgument(
"config_file",
default_value="commander.yaml",
description="The file that loads the commander config",
)
)
declared_arguments.append(
DeclareLaunchArgument(
"config_package",
default_value="core",
description="Package where the config file is located",
)
)
declared_arguments.append(
DeclareLaunchArgument(
"experiment_package",
default_value="experiments",
description="Package where the experiment file is located",
)
)
return LaunchDescription(
declared_arguments + [OpaqueFunction(function=launch_setup)]
)
Remember that we can change the default values on the command line when we launch our experiment. For example:
ros2 launch experiments example_launch.py random_seed:=10 log_level:=debug
Warning
The name of the launch file has to finish in launch (i.e example_launch.py) to be found when the ROS workspace is compiled.
Obviously, other configurations can be changed and used in the launch files to get the desired behavior, so you can check the ROS 2 launch documentation.
You can check complete launch files in the /launch directory of the experiments package in the emdb_experiments repository.