Writing YAML Configs#

The swarmsim.yaml module provides a custom YAML loader that defines some convenience tags. This article will explain how the YAML files are loaded and how to use the custom tags.

See also

Looking for how to load or dump YAML files with our custom tags? See swarmsim.yaml

Or for information on how to use your custom class in a YAML file, see Config Registry.

When we set up a world configuration, swarmsim lets us either create the configuration in Python, or define it in a YAML file. Here’s a simple example:

world.yaml#
type: RectangularWorld
size: [8, 8]
time_step: !np 1 / 40
agents:
- type: MazeAgent
  position: [4, 4]
  controller:
    type: StaticController
    output: [0.1, 0.2]

This YAML file can be loaded and run like this:

from swarmsim import config_from_yaml, run_sim

world_config = config_from_yaml('world.yaml')
run_sim(world_config)

This is equivalent to the following Python code:

from swarmsim import RectangularWorldConfig, run_sim
from swarmsim.agent.MazeAgent import MazeAgentConfig
from swarmsim.agent.control.StaticController import StaticController

world_config = RectangularWorldConfig(
   size=[8, 8],
   time_step=1 / 40,
   agents=[
      MazeAgentConfig(
         position=[4, 4],
         controller=StaticController(output=[0.1, 0.2]),
      ),
   ],
)
run_sim(world_config)

As you can see, the YAML file is much more concise than the equivalent Python code.

The YAML Language#

YAML is a human-readable data serialization format. We use it to write configurations that describe simulations.

It’s a superset of JSON, so it’s easy to read and write.

We use the PyYAML library to load and dump YAML files.

See also

Here’s a nice and quick tutorial for YAML: Learn YAML in Y minutes

Or have a look at the YAML specification

YAML Tags#

The YAML standard allows for !tags which provide information on how to parse a YAML entry. These tags usually start with ! and are defined in the YAML specification: Tags. An example of standard tags are type specifiers, such as !!str and !!int.

explicitly-typed-value: !!int 42

RobotSwarmSimulator uses a custom PyYAML loader to allow for some nice features. The custom YAML tags are defined in the yaml module.

Here’s an example of a crazy YAML file that uses a bunch of YAML features and our custom tags:

world.yaml (example)#
type: "RectangularWorld"
size: [10, 10]  # yaml flow style
agents:
- !include robot1.yaml  # add robot1 agent
- !include robot2.yaml  # add robot2 agent
- &anchor003  # save this robot as an anchor
  type: MazeAgent
  name: robot3
  agent_radius: 0.1
  angle: !np radians(90 + 45)  # convert degrees to radians
  poly: !include body_shape.svg  # load an SVG file
  controller:
    type: StaticController
    output: !np [1e-2, pi / 2]  # pi constant from numpy
spawners:
- type: ExcelSpawner
  path: !relpath positions.xlsx  # path is relative to cwd or this YAML file
  agent: *anchor003  # use the robot3 agent from above

If you’re new to YAML or haven’t seen the &anchor and *anchor syntax, check out Learn YAML in Y minutes.

To understand what the !include, !relpath, and !np tags do, read on.

Evaluating Simple Math Expressions with !np#

This tag is used to convert a YAML string, sequence, or mapping to a numpy object.

In this example, the following YAML files are in the same directory:

foo.yaml#
example: !np complex('2+2j')

See the mathexpr module for more information on what you can do.

Creating Hierarchical Configurations with !include#

This tag is used to include another YAML file as a mapping.

For example, see the following YAML files:

bar.yaml#
my_list:
  - 1
  - 2
  - 3
foo.yaml#
foo: !include bar.yaml

Here’s what that looks like when loading:

Loading foo.yaml#
>>> from swarmsim.yaml import load
>>> mapping = load('foo.yaml')

>>> print(mapping)
{'foo': {'my_list': [1, 2, 3]}}

Here’s the single-file equivalent:

Equivalent Single YAML file#
foo:
  my_list:
     - 1
     - 2
     - 3

The file extension of what you’re including affects the behavior of the !include tag:

  • .yaml files will be loaded using the load() function

  • .json files will be loaded using json.load

  • All other files are read as text and returned as a string

We have added a special case for using !include with YAML 1.1’s Merge Keys (<<: ). You can use <<: !include FILENAME.yaml to include a YAML file, and the file’s contents will be placed into the same level as the <<:   key. This is in contrast to key: !include FILENAME.yaml, which would place the file’s contents under 'key'.

This is powered by PyYAML’s support for the now deprecated Merge Key Language-Independent Type, so if that feature is removed, this feature will be removed as well.

root.yaml#
hello: world
<<: !include merged.yaml
merged.yaml#
my_list:
  - 1
  - 2
  - 3

is equivalent to:

hello: world
my_list:
  - 1
  - 2
  - 3

Referencing File Paths with !relpath#

This tag is used to resolve the relative path given, but unlike the !include tag, it does not load the file, and instead returns the absolute path as a string.

/home/user/project/foo.yaml#
path: !relpath bar.yaml

This is equivalent to:

path: /home/user/project/bar.yaml

Path Resolution Order#

When loading a YAML file, the !include and !relpath tags will resolve the path by testing the following assumptions in order:

  1. Path is not relative to the current working directory

    (i.e. the path is absolute or relative to the user home directory)

  2. Path is relative to the .yaml file with the tag

  3. Path is relative to the current working directory (where you were when you ran python).

    This is the default behavior for relative paths in Python, but it is the last place we look.

If a file isn’t found at any of these locations, an error will be raised. See include.search_file().

 

Parametric YAML Configs via Jinja Templating#

When working with Python-based configuration files, you can create parametric configurations by passing a variable in a class or dataclass instantiation:

def world_configurator(width):
    height = width / 2
    RectangularWorldConfig(size=[width, height])

You can actually do this with YAML too by using Jinja templating! You can create a YAML file that acts like a function, taking in variables and returning a configuration:

world.yaml#
#%> set height = width / 2
type: RectangularWorld
size:
- <{ width }>
- <{ height }>

Then, you can pass in variables like this:

from swarmsim import config_from_yaml
world_config = config_from_yaml('world.yaml', width=6)

The <{ width }> and <{ height }> are Jinja blocks which are converted to text before the YAML file is loaded. The #%> is a line statement prefix, which allows you to set Jinja variables which can be referenced in the blocks. You can also write statements like <% set height = width / 2 %> in the YAML file.

Note

The <% statement %>, <{ expression }>, and #%> line_statement delimiters are slightly different from the usual Jinja syntax. This is because YAML files may also contain specifications for modules that use Jinja templating inside them, such as the JinjaMetric. The different delimiters allow us to distinguish between Jinja at the YAML level and Jinja templates and expressions that are used to specify other behavior.

See also

For information on our custom additions to the Jinja2 API, see swarmsim.util.jinja.

Jinja templating is a powerful tool for creating parametric configurations. Here’s some examples of what you can do:

Dynamic Includes

Since Jinja templates are evaluated down to pure YAML, you can dynamically import other YAML files:

world.yaml#
type: RectangularWorld
agents:
- !include <{ agent_file }>.yaml
from swarmsim import config_from_yaml
world_config = config_from_yaml('world.yaml', agent_file='drone')

Default Values

You can set default values for variables in the YAML file with the default() filter:

world.yaml#
#%> set width = width|default(10)
type: RectangularWorld
size: [ <{ width }>, <{ width }> ]
time_step: <{ time_step|default('!np 1 / 40') }>

This will set the width to 10 if you don’t pass in a value for width. You can also use the filter inside blocks. Note that we force it to become a string so that YAML can parse the !np tag.

There are a lot more operators like this; see the full list of Jinja filters and Jinja tests.

Swappable Sections

You can use Jinja templating to swap out sections of a YAML file. Here’s an example where we can swap between a binary and a human controller:

world.yaml#
type: RectangularWorld
agents:
- type: MazeAgent
  #%> set controller_type = controller_type|default('binary')
  <% if evader == 'binary' %>
  sensors:
  - type: BinaryFOVSensor
  controller:
    type: BinaryController
    a: [0.2, 0.2]
    b: [0.2, 0]
    sensor_id: 0
  <% elif evader == 'human' %>
  controller:
    type: HumanController
    joystick: null
    keys: arrowkeys
  <% endif %>

Jinja2 offers a full set of control structures that haven’t been covered here. See the List of Control Structures for more information.

See also

Jinja2 is a very powerful and widely used templating engine, and we’ve only covered a small subset of its features here. For more information, see the official Template Designer Documentation guide.