Worlds¶
Each world is a directory under arena_simulation_setup/worlds/<name>/. The
directory name is the canonical identifier consumed by WorldIdentifier.
Directory layout¶
Per-level world, one subdirectory per level named by level id. Every shipped
world uses this layout, including single-level worlds (a lone 0/):
worlds/<name>/
├── <level_id>/ # one per level, e.g. 0/, 1/, 2/
│ ├── world.yaml # LevelDescription for this level (zones:/microphones:)
│ ├── map.png # per-level occupancy map
│ └── map.yaml
├── scenarios/
│ └── <scenario>/
│ ├── scenario.yaml # Scenario: static/dynamic obstacles, robot goals
│ ├── scenario.json # alternative; ScenarioView tries .yaml first
│ └── <agent_type>.yaml # optional scenario-local arena_humansim agent types
└── assets/
└── <domain>/
└── <asset_type>/
└── <name>/ # world-local asset; overrides network-fetched assets
World.load() picks a mode by what's on disk: any subdirectory holding a
world.yaml (other than scenarios/, assets/, map/) is treated as a
level, named by that directory, and the root world.yaml is ignored.
touch_world --all normalizes whatever is on disk back into this canonical
per-level-subdirectory form.
Legacy single-file world (fallback)¶
With no level subdirectories, World.load() falls back to a root
world.yaml, either a single-level zones: file (synthesized as level
'0') or a multi-level file with a top-level levels: {<level_id>:
{zones:, microphones:}} dict:
worlds/<name>/
├── world.yaml # WorldDescription: zones, walls, doors, entities
├── map/
│ ├── map.png # binary occupancy map (regenerated by touch_world)
│ ├── map.yaml # ROS map-server metadata (resolution, origin)
│ └── map.world.yaml # optional: Gazebo world file
├── scenarios/
└── assets/
No world shipped under arena_simulation_setup/worlds/ uses this layout.
world.yaml schema¶
Top-level key is zones, a list of zone objects. Each zone:
zones:
- name: central_hallway # required; used as floor name
description: Main Central Hallway
material: # MaterialIdentifier for the floor
- Concrete_Smooth # string form: [domain/]name
- {} # or [name, modifiers_dict] tuple form
corners: # polygon corners of the zone floor
- {x: 8.0, y: 0.0, z: 0.0}
walls: # list of Wall entries
- start: {x: 8.0, y: 0.0, z: 0.0}
end: {x: 9.25, y: 0.0, z: 0.0}
material: # optional: inline material (when kind is absent)
name: Plaster_Wall
domain: Common
_modifiers: null
- start: {x: 0.0, y: 25.1, z: 0.0}
end: {x: 8.0, y: 25.1, z: 0.0}
kind: patient_room_walls # optional: WallIdentifier name; overrides material
doors:
- name: main_hallway_entrance
start: {x: 9.25, y: 0.0, z: 0.0}
end: {x: 10.75, y: 0.0, z: 0.0}
kind: sliding
width: 0.1
height: 2.0
material:
- Aluminum_Anodized
- {}
elevators: []
schedules: [] # list of Schedule (M2 standalone), see AUTHORING.md
signals: [] # list of Signal (M2 standalone), see AUTHORING.md
sounds: [] # list of Sound (M2 standalone), see AUTHORING.md
lights: [] # list of local Light fixtures, see AUTHORING.md#lights
entities:
static: [] # list of Obstacle, same schema as scenario static:
dynamic: [] # list of DynamicObstacle, same schema as scenario dynamic:
ceiling: true # true by default, false to leave the zone open
ceiling_height: 2.0 # top height (m), derived from wall stack when absent
ceiling_cast_shadows: false # false by default, true to occlude light
ceiling_lights: null # ceiling rig {fixture, spacing, lumens, ...}, see AUTHORING.md#lights
ceiling_material: # MaterialIdentifier, defaults to Concrete_Smooth
- Concrete_Smooth
- {}
wall_material: # MaterialIdentifier for auto-generated occupancy walls
- Concrete_Smooth # (walls the map builder infers, not the walls: list)
- {}
Field aliases: material/mat, ceiling_material/ceiling_mat,
wall_material/wall_mat. schedules:, signals:, sounds:, lights:,
ceiling_lights: and the semantics preset: mechanism (gate,
pressure_plate, occupancy_cap, signal, schedule, sound, light) are
documented in
AUTHORING.md. The scenario
static:/dynamic: schema referenced above is in
scenarios/<name>/ schema.
A Wall entry with no kind field generates a plain wall segment using the
material field (or the wall default material). A Wall with kind: <name>
is resolved via WallIdentifier(name) at simulation time,
see tree/Wall.py
and shared/walls.py.
Door fields¶
| key | default | meaning |
|---|---|---|
name |
required | unique within the world |
start, end |
required | opening endpoints |
kind |
sliding |
sliding, sliding_top, teleport implemented. hinged is accepted but falls back to teleport at runtime with a warning |
width |
0.1 |
metres |
height |
2.0 |
metres |
material |
door default | MaterialIdentifier |
activation_distance |
3.0 |
scalar or [a, b], stored as a 2-tuple, only max() of the two drives the trigger radius |
transition_time |
1.0 |
seconds to fully open/close |
hold_time |
2.0 |
seconds held open after trigger clears |
semantics |
[] |
attaches a scripted kind, see AUTHORING.md |
Elevator fields¶
| key | default | meaning |
|---|---|---|
name |
required | unique across every level in the world |
position |
required | cabin center |
size |
[2.0, 2.0, 2.0] |
cabin [x, y, z] |
door_side |
+x |
cabin face the door opens on, auto-inferred from the nearest wall (<=1 m) at startup when left at the default |
material |
elevator default | MaterialIdentifier |
destination |
"" |
comma-separated <level_id>.<elevator_name> targets |
activation_distance |
3.0 |
metres |
transition_time |
1.0 |
seconds |
hold_time |
2.0 |
seconds |
travel_time |
3.0 |
seconds between levels |
accept_outside_calls |
true |
whether hallway buttons can summon the cabin |
recall_on |
null |
regime name. While asserted, refuses calls and holds the cabin door open |
semantics |
[] |
attaches a scripted kind, see AUTHORING.md |
microphones¶
A sibling of zones: at the level root, a list of audio listeners for the
acoustics pipeline:
zones: [...]
microphones:
- zone: lobby # required, must reference a zone in this level
placement: ceiling # required, free-form. "ceiling" is validated against the zone's ceiling
position: {x: 4.0, y: 2.0, z: 2.4}
frame: map # default map. with map, position must fall inside the zone polygon
index: 1 # default 1, disambiguates duplicates, must be >= 1
(zone, placement, index) forms the listener id and must be unique across
the whole world. See WorldMicrophone in
tree/World/World.py.
lights¶
A sibling of zones: at the level root, the ambient lights of the level
(fixture: dome or sun, with lux). Zone-local fixtures go in a zone's
lights: instead. See AUTHORING.md.
Semantic annotations¶
Door and elevator state is intrinsic: every spawned door and elevator
publishes its full state/predicate vocabulary (the kind's
DISCRETE/CONTINUOUS/PREDICATES in
arena_runtime _semantics.py)
with no annotation required. An Elevator's fire-recall regime is a
first-class recall_on: <regime> field, not a semantics annotation:
elevators:
- name: 1_elevator
position: {x: 5.0, y: 0.0, z: 0.0}
destination: "2.2_elevator"
recall_on: alarm
An optional semantics: list can still be added to any doors: entry,
elevators: entry, or to a zone itself (LevelDescription.Zone). On a door
or elevator entry it attaches a scripted kind (gate, pressure_plate),
not door/elevator state. Each item is either a preset or a primitive, parsed
into a SemanticCfg
(shared/semantics.py):
doors:
- name: north_fire_door
start: {x: 4.0, y: 1.55, z: 0.0}
end: {x: 4.0, y: 2.45, z: 0.0}
semantics:
- {preset: gate, params: {authorized: [], unlock_on: alarm}}
zones:
- name: lobby
corners: [...]
semantics:
- state: max_speed
value: 1.5
- predicate: quiet
value: true
- predicate: restricted
value: false
A preset (e.g. {preset: gate}) expands to the fixed set of states and
predicates that kind's runtime handler tracks. Any other keys on the preset
item (e.g. distance) apply as overrides to every expanded primitive, except
value, which is rejected on a multi-primitive preset: a params: key named
after one of the expanded primitives becomes that primitive's value instead.
Expansion happens at parse time, so SemanticCfgs never retain the preset
name. Annotating a door/elevator entry with a field from that mechanism's own
published vocabulary (e.g. {state: progress} on a door) is rejected at
world-load time: remove the annotation, the field publishes on its own.
A primitive is {state: <name>, ...} or {predicate: <name>, ...}, with
optional keys:
| key | default | meaning |
|---|---|---|
value |
null |
literal for zone annotations; omitted/null means runtime-derived for doors/elevators |
binding |
kinematic |
joint is reserved and raises a parse error in v1 |
trigger |
proximity |
contact is reserved and raises a parse error in v1 |
distance |
-1.0 |
proximity radius in metres; < 0 inherits the entity's activation_distance |
Zone semantics have a seed vocabulary (max_speed continuous state,
quiet/restricted predicates) that is inert in v1: parsed and published,
with no runtime behavior. Author-defined state/predicate names beyond the
seed are accepted verbatim.
Door/Elevator omit an empty semantics: list on serialize (opt-in,
consistent with their other omit-if-default fields). Zone is plain attrs and
always emits semantics: [] when empty.
Presets with no door/elevator geometry of their own (signal, schedule,
sound) instead attach to a standalone signals:/schedules:/sounds:
entry, a zone-level sibling list to doors:/elevators:. occupancy_cap
attaches directly to a zones: entry.
Annotated reference worlds¶
hospital_1 and reception carry zone semantics: annotations and are the
reference for authoring new ones: throughput corridors/lobbies get
max_speed, wards/waiting/rest rooms get quiet, staff-only or hazard rooms
get restricted. Their doors carry no annotation: door state publishes on
its own.
| World | Zones |
|---|---|
hospital_1 |
max_speed: central_hallway, reception, sub_hallway. quiet: patient_ward, nurse_resting_room, waiting_area, doctors_resting_room. restricted: exam_room_1, pharmacy, operating_room, laboratory |
reception |
restricted: destination (behind the counter). quiet: entrance (waiting chairs) |
map/ directory¶
A preview map.png + map.yaml can be rendered with touch_world (see
scripts/touch_world) for inspection. These are not
required at runtime: the runtime renders its own map from world.yaml in-process,
so editing world.yaml is enough. Use touch_world --all to also regenerate the
canonical per-level world.yaml.
map.world.yaml is a Gazebo world file and is not produced by touch_world,
it is hand-authored or produced by the generate_world pipeline.
scenarios/<name>/ schema¶
Resolved by World.scenario property which returns a per-world
ScenarioIdentifier class
(tree/World/World.py:1003).
ScenarioView.load() tries scenario.yaml first, then scenario.json.
Current (structured) format:
static:
- name: desk_1
model: office_desk # ObjectIdentifier
pose: [x, y, yaw]
dynamic:
- name: agent_1
model: female_adult_medical_01 # PedestrianIdentifier
agent: {agent_type: adult, desired_velocity: 1.4}
pose: [4.0, 2.0, 0.0] # yaw in radians
waypoints:
- [4.0, 2.0, 0.0]
robots:
- start: [x, y, yaw] # required. [x,y], [x,y,yaw], {x,y,yaw} or a zone name
start_floor: "" # optional, multi-level worlds
goal_floor: "" # optional, floor for goto phases
phases: # ordered. each entry is one of:
- {goto: [x, y, yaw]} # navigate to pose (same formats as start)
- {goto: kitchen} # navigate into a zone, door, elevator or to a pedestrian by name
- {gesture: wave} # arm gesture, "random" allowed, optional instance: <mount>
- {reach: stow} # arm reach: named target, "random", or a pose (optional frame:)
- {until: "door_1.open == true"} # hold where the robot is until the atom holds
conditions: [] # optional, clauses judged over this robot's whole phase list
Every phase also takes until: "<atom>" (completes once the atom holds, after
arrival for a goto), hold_time: <s> (park time at a goto goal), conditions:
(clauses judged over this phase only), text: (instruction for
language-conditioned planners) and on_failure: continue | stop_task | abort_episode.
A pose given as a bare string samples a point inside that zone with identity
orientation, and a named goto is judged met by zone membership rather than by
distance to the sampled point. goal: [x, y, yaw] is deprecated and rewritten to a single
goto phase with a warning. Which robot model and adapter runs is not part of
the scenario, see
arena_robots setup.
Schema: tree/World/Scenario.py.
Legacy format (deprecated) uses obstacles.static and obstacles.dynamic keys instead.
The agent: block configures the arena_humansim backend. agent_type is
either a built-in type (adult, elder) or a scenario-local agent-type YAML
file referenced by relative path (agent_type: ./doctor.yaml) for scripted
behavior (sequences, interactions, needs) — see
humansim/arena_humansim/config/agent_types/README.md and the
hospital_1/scenarios/showcase scenario for a worked example.
static (Obstacle) also takes light: {lit, light_on, level} for the lights its
object annotation carries (see AUTHORING.md), scale: [x, y, z] (default identity) and
level_id (multi-level worlds). dynamic (DynamicObstacle) also takes
velocity (m/s, default 1.0) and level_id. pos: is a deprecated alias
for pose: on both, rewritten on parse. Any other key on a static/dynamic
entry is kept verbatim in extra and forwarded to the sim backend rather
than validated by the scenario parser, e.g. a static entry's type,
capacity, interaction_radius, formation, satisfies become its
humansim WorldObject info.
sounds is a list of Sound entries, same schema as a world zone's
sounds: but episode-scoped (attached at reset, gone at the next one).
timeline is a list of scripted set actions fired on at/every/when
triggers. See AUTHORING.md, Scenario timelines.
regions is a mapping of name to source/sink spawner consumed by the
arena_humansim backend:
regions:
entrance_source:
type: source # source | sink
polygon: # CCW corners {x,y[,z]}
- {x: 9.25, y: 0.2}
- {x: 10.75, y: 0.2}
- {x: 10.75, y: 1.2}
- {x: 9.25, y: 1.2}
config:
rate_profile: # source only: list of {t, rate} keyframes
- {t: 0.0, rate: 1.5}
max_concurrent: 5 # source only, default -1 (unbounded)
max_total: 50 # source only, default -1 (unbounded)
agent: # source only, spawn template
agent_type: adult # built-in name or ./relative.yaml
desired_velocity: {min: 1.0, max: 1.5} # or a scalar
agent_radius: 0.35
behavior_tree: default
sink_affinity:
- {sink: reception_sink, weight: 1.0}
reception_sink:
type: sink
ref: lobby # instead of polygon:, takes the named zone's corners
config:
absorption_radius: 0.5 # sink only, default 0.5
capacity: -1 # sink only, default -1 (unbounded)
An unrecognized type is rejected when the region is spawned.
conditions is a list of episode success/failure clauses:
conditions:
- {op: eventually, p: "robot in pharmacy", text: "Reach the pharmacy."}
- {op: never, p: "robot in operating_room", text: "Stay out of the operating room."}
- {op: before, p: "robot in central_hallway", q: "robot in pharmacy", text: "Take the hallway before the pharmacy."}
- {op: never_during, p: "robot in exam_room_1", q: "exam_room_1_door/0.open == false", text: "Never be in the exam room while its door is closed."}
| key | meaning |
|---|---|
op |
always, never, eventually, before, or never_during |
p |
first atom, required |
q |
second atom, required for before/never_during, rejected (parse error) with any other op |
text |
human-readable description, optional |
An atom is one of these bare forms (no env_N/ prefix): <entity>.<field> ==
<value> (an entity field test), <subject> in <zone> (zone membership),
<subject> within <r> of <subject> (planar distance) and not <atom>. A subject
is robot (the robot being judged), another robot's name or a pedestrian's name.
Every robot is also an entity of kind robot with the fields phase, met,
failed, dropped and violated, so robot_1.phase == 2 is a valid atom.
A world entity (zone, door, elevator, schedule, sound) is named
<name>/<level>, or <name> alone when no other level has one.
Clauses take an optional on_failure (continue by default, stop_task,
abort_episode). Conditions are judged online at the node tick and replayed
offline by arena_evaluation with the same code.
Schema: shared/conditions.py.
assets/ directory¶
World-local assets take precedence over network-fetched assets because
DynamicPaths.WORLD / 'assets' is registered before NetResolver for every
domain asset type (see DynamicPaths.as_resolvers,
tree/init.py:77).
Layout mirrors the network structure: assets/<domain>/<asset_type>/<name>/.
A world that ships a custom Wall preset places it at
assets/Common/Wall/<name>/<name>.yaml.
How WorldIdentifier resolves a name¶
Resolvers run in order, first hit wins (tree/World/World.py, bottom of file):
- Each
ARENA_WORLD_PATHroot (colon-separated env var), if set. ASS_DIR / 'worlds', the worlds shipped in this package, existence-checked.- One
NetResolverperWORLD_BUCKETSprovider (defaultarena-worlds-prod-public), fetching into$ARENA_ASSETS_DIR/<bucket>/<name>/. A world keys off its bare name, so the worlds bucket is itself the namespace. - A
FallbackResolverrooted atASS_DIR / 'worlds'. It never answers reads and so can never shadow an earlier source. It only supplies a write target (resolve_write_path()/resolve_write_sync()) for programmatic world creation.
Worlds not shipped in this package are hosted in the public bucket(s) named
by WORLD_BUCKETS and pulled on demand. arena asset find world <name>
prints every resolver's verdict for a name (which one hit, which were
shadowed). arena asset pull world <name> fetches one into the local cache.
arena asset push world <name> publishes a locally-authored world after a
preflight that checks every asset it references would resolve for someone
else.
WorldIdentifier('hospital_1').resolve() returns a MultiLevelWorldView
wrapping whichever of the above resolved first. Call world.load() to parse
world.yaml into a WorldDescription.
Shipped worlds¶
| Name | Notes |
|---|---|
airport |
Airport terminal layout |
hospital_1 |
Multi-zone hospital with patient rooms, reception, hallways |
hospital_2 |
Hospital variant |
map_empty |
Single open zone |
office_1 |
Office floor plan |
reception |
Reception area |