Skip to content

Building a simulation

Gguidini edited this page May 13, 2021 · 5 revisions

A simulation in HMR Sim has 2 important stages:

  • Parse stage. During this stage the simulation config and map are parsed, creating a Simulation instance. Then the systems definitions to be used are added to the instance, completing the simulation build.
  • Run stage. During this stage the simulation instance created is effectively executed.

This page gives details about the parse stage (to learn more about running the simulation visit Running a simulation). Note in the image below that the parse stage requires mainly 4 inputs: the configuration object, and definitions for models, components and systems. The result of this stage is a Simulator instance, which is a Python class like any other.

A simulation can be defined using a map, programmatically via the configuration object, or both. Maps are XML files defined with the JGraph library (usually using diagrams.net). Check the examples directory for some simulation examples.

HMR Sim architecture

Components, models and builders definitions are imported automatically from the directory structure. Simulations exporting any of those you must keep them in a folder struture as shown below. Simulations can also use core components, models and builders exported by the simulator (e.g. simulator.components.Position).

project_root
|
|- components/
|- models/
|- builders/

It's also suggested to have a systems/ folder, but systems definitions must be added to the simulator instance after parsing the simulation. To add systems the methods Simulator.add_system and Simulator.add_des_system should be used, depending on the system to be added (see systems for details).

Simulation Configuration

The simulation configuration is simply a Python dict (see Config at typehints), or a string to a json file with the config options. Below are the most important configuration values and their description.

💡
A config option followed by '?' (e.g. map?:) indicates it is optional.

  • context: str - The root directory of the project
  • map?: str - Path to the map XML file, relative to context
  • FPS?: float - When using esper systems, they'll be run every 1/FPS seconds in the simulation
  • DLW?: float - Default line width. Used for Floorplan shapes to denote wall thickness. Default 10.
  • duration?: float - If provided, simulation will run for duration seconds before stopping. Refers to simulation seconds.
  • verbose: bool - If true the simulation parsing report will be printed to stdin after simulation parsing. Can be used by systems to set themselves as verbose or not. Default False.
  • simulationComponents?: Dict[str, list]: Dictionary of components for the scene (e.g. the simulation itself, which is always entity 1). Usually used to hold shared information for all robots. Format for each component is <component_name>: [args to init component].
  • extraEntities?: List[EntityDefinition]: Entities that are not in the map to include in the simulation. See creating entities programmatically.

Creating Entities programmatically

Entities can be created programatically and added to the simulation using the extraEntities config option or using Simulator.add_entity method. They are declared using the EntityDefinition type, with the following fields:

  • entId: str - Identifier of the entity
  • components: Dict[str, list] - Components the entity initially has. Format for each component is <component_name>: [args to init component].
  • isObject: bool - If this entity is an object, with a type
  • isInteractive: bool - If this object is interactive (e.g. can be picked up by another entity)
  • type?: str - Object type

Creating a map

Maps are defined using the JGraph library, usually using diagrams.net diagrams. They are saved as compressed .xml files. Not all shapes are supported (see below) If you want to use a shape that's not supported you can create a model to parse it (see Builders and Models).

Maps are passed to the simulator using the simulation config.

Supported shapes

Currently, supported shapes are:

  • From General shapes

    • Rectangles
    • Ellipse
    • Circle
    • Square
    • Arrows (connectors)
  • From Floorplans

    • Wall (+ vertical)
    • Wall Corner (any variation)
    • Wall U
    • Room

Shapes vs Objects

Every shape you draw in the map turns out to be just that: a shape. By default they are immovable in your simulation, like a wall. If your object doesn't need to move or be directly interacted with, you can leave them as that.

To differentiate your shapes, you can add properties to them using the Edit Data command in draw.io. With added properties, your shapes become objects and can be moved around, receive commands, etc.

See an example of annotated object in the image below. type is the most important argument, indicating what builder should be used to parse this object. To add a component to the object use the format component_<componentName>: [args for component], as you can see in the image.

Annotated object

Also, some shapes in the map don't turn out to be shapes or objects, but rather components. That is the case for those with the type property set to path or map-path, for example. It will depend on what builder parses that shape.

ℹ️  Components guidelines

  1. All components must export a class with the same name as the component file.
  2. To use a component on a simulation, add an attribute to your object with the key component_<componentName> and as a value, an array of arguments > to the component's constructor. This array needs to be JSON parsable (e.g. follow JSON syntax).
  3. The Position and Collision components are inferred from the object's shape and position on the map.
  4. To remove the Collision component of an object (making it not collidable), add a property collidable to is, with value False.

Builders and models

The maps used in simulations represent objects within mxCell. Different shapes have different contents in their mxCell. models are functions that trnaslate these shapes into a list of components for the simulation. Each model should be in its own file, inside the models/ directory. Each model file must contain:

  • A function from_mxCell, which receives the mxCell and returns a list of components;
  • A constant MODEL with the name of the shape that this model translates (e.g. mxgraph.floorplan.wall).

💡
You can export the map file as uncompressed xml to help you debug when developing new models.

  • builders are similar to models, but they parse XML within <object>. Any <mxCell> with annotations (such as the image above depicts) is wrapped with an <object> tag. So builders must translate both the annotations and the XML of the <mxCell> inside that object. They may use models to do that, obviously.

Builders are stored in the builders/ directory, also one per file. A builder file must contain:

  • A build_object function that builds the object, see details below;
  • A TYPE constant, to indicate that the builder should parse objects annotated with that type.

build_object function

The signature of build_object function is def build_object(cell, world: World, window_options, draw2entity) -> Tuple[dict, list, dict]:. cell is the <object> tag, world is the esper World of the simulator instance. It is expected that the builder adds a new entity (if necessary) directly to the world. window_options are details such as simulation width, height and the line width for the simulation. draw2entity is a dictionary with the name of the map objects (i.e. the ID for the object in the map) as key, and a pair <entityID, style> as value. entityId is the esper World ID of the entity that represents that entity. The style also comes from the map definition, but would better be used with the Skeleton component than from that. In any case draw2entity is included as an argument in case some object has some sort of dependency to another object in the map.

If during the parsing of the object it is the case that it depends on another object from the map that has not yet been parsed, build_object can raise a DependencyNotFound (from simulator.typehints.build_types) exception, which will cause the object's parsing to be deferred to a second pass.

The return of build_object function is a 3-uple with values that will update the Simulator instance internal reference of entites. Add the entity directly to the world if that's the case, or the entity will not be added in the simulation.

The first argument is a dict that will update draw2entity. Therefore it is in the format <map-id>: [entity-id, style]. Style is optional, but the value must be a list.

The second argument updates the list of objects in the simulation. The semantic of 'object' in the simulation is an enity that has a type (e.g. robot, drone, person, etc), denoting specialized objects. The list is a list of tuples in the form (entity-id, map-id).

The last argument is a dict that will update the interactive dictionary, a dict of objects within the simulation that can be picked up and dropped back into the simulation. This is optional, of course. Interactive objects should have a name, the key of the dictionary, and the value is the entity-id.

Strongly advised to check out the builders already created for the simulator. This will likely change a little in future releases to be a more structured and straight forward process. It's clear that a lot of ad-hoc decisions were made so far 😅.