-
Notifications
You must be signed in to change notification settings - Fork 8
Building a simulation
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.
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).
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.
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
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.
Currently, supported shapes are:
-
From General shapes
- Rectangles
- Ellipse
- Circle
- Square
- Arrows (connectors)
-
From Floorplans
- Wall (+ vertical)
- Wall Corner (any variation)
- Wall U
- Room
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.
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
- All components must export a
class
with the same name as the component file.- 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).- The Position and Collision components are inferred from the object's shape and position on the map.
- To remove the Collision component of an object (making it not collidable), add a property
collidable
to is, with valueFalse
.
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 themxCell
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.
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 😅.